blob: 7df4b6d616487b0e161c9e761b67005099029bf9 [file] [log] [blame]
# Copyright 2005-2018 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
import io
import logging
import subprocess
import portage
from portage import os
from portage.util import writemsg_level, shlex_split
from portage.util.futures import asyncio
from portage.output import create_color_func, EOutput
good = create_color_func("GOOD")
bad = create_color_func("BAD")
warn = create_color_func("WARN")
from portage.sync.syncbase import NewBase
try:
from gemato.exceptions import GematoException
import gemato.openpgp
except ImportError:
gemato = None
class GitSync(NewBase):
'''Git sync class'''
short_desc = "Perform sync operations on git based repositories"
@staticmethod
def name():
return "GitSync"
def __init__(self):
NewBase.__init__(self, "git", portage.const.GIT_PACKAGE_ATOM)
def exists(self, **kwargs):
'''Tests whether the repo actually exists'''
return os.path.exists(os.path.join(self.repo.location, '.git'))
def new(self, **kwargs):
'''Do the initial clone of the repository'''
if kwargs:
self._kwargs(kwargs)
if not self.has_bin:
return (1, False)
try:
if not os.path.exists(self.repo.location):
os.makedirs(self.repo.location)
self.logger(self.xterm_titles,
'Created new directory %s' % self.repo.location)
except IOError:
return (1, False)
sync_uri = self.repo.sync_uri
if sync_uri.startswith("file://"):
sync_uri = sync_uri[7:]
git_cmd_opts = ""
if self.repo.module_specific_options.get('sync-git-env'):
shlexed_env = shlex_split(self.repo.module_specific_options['sync-git-env'])
env = dict((k, v) for k, _, v in (assignment.partition('=') for assignment in shlexed_env) if k)
self.spawn_kwargs['env'].update(env)
if self.repo.module_specific_options.get('sync-git-clone-env'):
shlexed_env = shlex_split(self.repo.module_specific_options['sync-git-clone-env'])
clone_env = dict((k, v) for k, _, v in (assignment.partition('=') for assignment in shlexed_env) if k)
self.spawn_kwargs['env'].update(clone_env)
if self.settings.get("PORTAGE_QUIET") == "1":
git_cmd_opts += " --quiet"
if self.repo.clone_depth is not None:
if self.repo.clone_depth != 0:
git_cmd_opts += " --depth %d" % self.repo.clone_depth
elif self.repo.sync_depth is not None:
if self.repo.sync_depth != 0:
git_cmd_opts += " --depth %d" % self.repo.sync_depth
else:
# default
git_cmd_opts += " --depth 1"
if self.repo.module_specific_options.get('sync-git-clone-extra-opts'):
git_cmd_opts += " %s" % self.repo.module_specific_options['sync-git-clone-extra-opts']
git_cmd = "%s clone%s %s ." % (self.bin_command, git_cmd_opts,
portage._shell_quote(sync_uri))
writemsg_level(git_cmd + "\n")
exitcode = portage.process.spawn_bash("cd %s ; exec %s" % (
portage._shell_quote(self.repo.location), git_cmd),
**self.spawn_kwargs)
if exitcode != os.EX_OK:
msg = "!!! git clone error in %s" % self.repo.location
self.logger(self.xterm_titles, msg)
writemsg_level(msg + "\n", level=logging.ERROR, noiselevel=-1)
return (exitcode, False)
if not self.verify_head():
return (1, False)
return (os.EX_OK, True)
def update(self):
''' Update existing git repository, and ignore the syncuri. We are
going to trust the user and assume that the user is in the branch
that he/she wants updated. We'll let the user manage branches with
git directly.
'''
if not self.has_bin:
return (1, False)
git_cmd_opts = ""
quiet = self.settings.get("PORTAGE_QUIET") == "1"
if self.repo.module_specific_options.get('sync-git-env'):
shlexed_env = shlex_split(self.repo.module_specific_options['sync-git-env'])
env = dict((k, v) for k, _, v in (assignment.partition('=') for assignment in shlexed_env) if k)
self.spawn_kwargs['env'].update(env)
if self.repo.module_specific_options.get('sync-git-pull-env'):
shlexed_env = shlex_split(self.repo.module_specific_options['sync-git-pull-env'])
pull_env = dict((k, v) for k, _, v in (assignment.partition('=') for assignment in shlexed_env) if k)
self.spawn_kwargs['env'].update(pull_env)
if self.settings.get("PORTAGE_QUIET") == "1":
git_cmd_opts += " --quiet"
if self.repo.module_specific_options.get('sync-git-pull-extra-opts'):
git_cmd_opts += " %s" % self.repo.module_specific_options['sync-git-pull-extra-opts']
try:
remote_branch = portage._unicode_decode(
subprocess.check_output([self.bin_command, 'rev-parse',
'--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
cwd=portage._unicode_encode(self.repo.location))).rstrip('\n')
except subprocess.CalledProcessError as e:
msg = "!!! git rev-parse error in %s" % self.repo.location
self.logger(self.xterm_titles, msg)
writemsg_level(msg + "\n", level=logging.ERROR, noiselevel=-1)
return (e.returncode, False)
shallow = self.repo.sync_depth is not None and self.repo.sync_depth != 0
if shallow:
git_cmd_opts += " --depth %d" % self.repo.sync_depth
# For shallow fetch, unreachable objects may need to be pruned
# manually, in order to prevent automatic git gc calls from
# eventually failing (see bug 599008).
gc_cmd = ['git', '-c', 'gc.autodetach=false', 'gc', '--auto']
if quiet:
gc_cmd.append('--quiet')
exitcode = portage.process.spawn(gc_cmd,
cwd=portage._unicode_encode(self.repo.location),
**self.spawn_kwargs)
if exitcode != os.EX_OK:
msg = "!!! git gc error in %s" % self.repo.location
self.logger(self.xterm_titles, msg)
writemsg_level(msg + "\n", level=logging.ERROR, noiselevel=-1)
return (exitcode, False)
git_cmd = "%s fetch %s%s" % (self.bin_command,
remote_branch.partition('/')[0], git_cmd_opts)
writemsg_level(git_cmd + "\n")
rev_cmd = [self.bin_command, "rev-list", "--max-count=1", "HEAD"]
previous_rev = subprocess.check_output(rev_cmd,
cwd=portage._unicode_encode(self.repo.location))
exitcode = portage.process.spawn_bash("cd %s ; exec %s" % (
portage._shell_quote(self.repo.location), git_cmd),
**self.spawn_kwargs)
if exitcode != os.EX_OK:
msg = "!!! git fetch error in %s" % self.repo.location
self.logger(self.xterm_titles, msg)
writemsg_level(msg + "\n", level=logging.ERROR, noiselevel=-1)
return (exitcode, False)
if not self.verify_head(revision='refs/remotes/%s' % remote_branch):
return (1, False)
if shallow:
# Since the default merge strategy typically fails when
# the depth is not unlimited, `git reset --merge`.
merge_cmd = [self.bin_command, 'reset', '--merge']
else:
merge_cmd = [self.bin_command, 'merge']
merge_cmd.append('refs/remotes/%s' % remote_branch)
if quiet:
merge_cmd.append('--quiet')
exitcode = portage.process.spawn(merge_cmd,
cwd=portage._unicode_encode(self.repo.location),
**self.spawn_kwargs)
if exitcode != os.EX_OK:
msg = "!!! git merge error in %s" % self.repo.location
self.logger(self.xterm_titles, msg)
writemsg_level(msg + "\n", level=logging.ERROR, noiselevel=-1)
return (exitcode, False)
current_rev = subprocess.check_output(rev_cmd,
cwd=portage._unicode_encode(self.repo.location))
return (os.EX_OK, current_rev != previous_rev)
def verify_head(self, revision='-1'):
if (self.repo.module_specific_options.get(
'sync-git-verify-commit-signature', 'false') != 'true'):
return True
if self.repo.sync_openpgp_key_path is not None:
if gemato is None:
writemsg_level("!!! Verifying against specified key requires gemato-11.0+ installed\n",
level=logging.ERROR, noiselevel=-1)
return False
openpgp_env = gemato.openpgp.OpenPGPEnvironment()
else:
openpgp_env = None
try:
out = EOutput()
env = None
if openpgp_env is not None:
try:
out.einfo('Using keys from %s' % (self.repo.sync_openpgp_key_path,))
with io.open(self.repo.sync_openpgp_key_path, 'rb') as f:
openpgp_env.import_key(f)
self._refresh_keys(openpgp_env)
except (GematoException, asyncio.TimeoutError) as e:
writemsg_level("!!! Verification impossible due to keyring problem:\n%s\n"
% (e,),
level=logging.ERROR, noiselevel=-1)
return False
env = os.environ.copy()
env['GNUPGHOME'] = openpgp_env.home
rev_cmd = [self.bin_command, "log", "-n1", "--pretty=format:%G?", revision]
try:
status = (portage._unicode_decode(
subprocess.check_output(rev_cmd,
cwd=portage._unicode_encode(self.repo.location),
env=env))
.strip())
except subprocess.CalledProcessError:
return False
if status == 'G': # good signature is good
out.einfo('Trusted signature found on top commit')
return True
elif status == 'U': # untrusted
out.ewarn('Top commit signature is valid but not trusted')
return True
else:
if status == 'B':
expl = 'bad signature'
elif status == 'X':
expl = 'expired signature'
elif status == 'Y':
expl = 'expired key'
elif status == 'R':
expl = 'revoked key'
elif status == 'E':
expl = 'unable to verify signature (missing key?)'
elif status == 'N':
expl = 'no signature'
else:
expl = 'unknown issue'
out.eerror('No valid signature found: %s' % (expl,))
return False
finally:
if openpgp_env is not None:
openpgp_env.close()
def retrieve_head(self, **kwargs):
'''Get information about the head commit'''
if kwargs:
self._kwargs(kwargs)
if self.bin_command is None:
# return quietly so that we don't pollute emerge --info output
return (1, False)
rev_cmd = [self.bin_command, "rev-list", "--max-count=1", "HEAD"]
try:
ret = (os.EX_OK,
portage._unicode_decode(subprocess.check_output(rev_cmd,
cwd=portage._unicode_encode(self.repo.location))))
except subprocess.CalledProcessError:
ret = (1, False)
return ret