diff --git a/repo b/repo index c78fcacd..66a2a07d 100755 --- a/repo +++ b/repo @@ -463,6 +463,34 @@ class CloneFailure(Exception): """ +def check_repo_verify(repo_verify, quiet=False): + """Check the --repo-verify state.""" + if not repo_verify: + print('repo: warning: verification of repo code has been disabled;\n' + 'repo will not be able to verify the integrity of itself.\n', + file=sys.stderr) + return False + + if NeedSetupGnuPG(): + return SetupGnuPG(quiet) + + return True + + +def check_repo_rev(dst, rev, repo_verify=True, quiet=False): + """Check that |rev| is valid.""" + do_verify = check_repo_verify(repo_verify, quiet=quiet) + remote_ref, local_rev = resolve_repo_rev(dst, rev) + if not quiet and not remote_ref.startswith('refs/heads/'): + print('warning: repo is not tracking a remote branch, so it will not ' + 'receive updates', file=sys.stderr) + if do_verify: + rev = verify_rev(dst, remote_ref, local_rev, quiet) + else: + rev = local_rev + return (remote_ref, rev) + + def _Init(args, gitc_init=False): """Installs repo by cloning it over the network. """ @@ -510,30 +538,12 @@ def _Init(args, gitc_init=False): _CheckGitVersion() try: - if not opt.repo_verify: - do_verify = False - print('repo: warning: verification of repo code has been disabled;\n' - 'repo will not be able to verify the integrity of itself.\n', - file=sys.stderr) - else: - if NeedSetupGnuPG(): - do_verify = SetupGnuPG(opt.quiet) - else: - do_verify = True - if not opt.quiet: print('Downloading Repo source from', url) dst = os.path.abspath(os.path.join(repodir, S_repo)) _Clone(url, dst, opt.clone_bundle, opt.quiet, opt.verbose) - remote_ref, local_rev = resolve_repo_rev(dst, rev) - if not opt.quiet and not remote_ref.startswith('refs/heads/'): - print('warning: repo is not tracking a remote branch, so it will not ' - 'receive updates', file=sys.stderr) - if do_verify: - rev = _Verify(dst, remote_ref, local_rev, opt.quiet) - else: - rev = local_rev + remote_ref, rev = check_repo_rev(dst, rev, opt.repo_verify, quiet=opt.quiet) _Checkout(dst, remote_ref, rev, opt.quiet) if not os.path.isfile(os.path.join(dst, 'repo')): @@ -907,7 +917,7 @@ def resolve_repo_rev(cwd, committish): raise CloneFailure() -def _Verify(cwd, remote_ref, rev, quiet): +def verify_rev(cwd, remote_ref, rev, quiet): """Verify the commit has been signed by a tag.""" ret = run_git('describe', rev, cwd=cwd) cur = ret.stdout.strip() diff --git a/subcmds/init.py b/subcmds/init.py index 431165d4..ce8b0187 100644 --- a/subcmds/init.py +++ b/subcmds/init.py @@ -38,6 +38,7 @@ from project import SyncBuffer from git_config import GitConfig from git_command import git_require, MIN_GIT_VERSION_SOFT, MIN_GIT_VERSION_HARD import platform_utils +from wrapper import Wrapper class Init(InteractiveCommand, MirrorSafeCommand): @@ -499,6 +500,16 @@ to update the working directory files. remote.url = opt.repo_url remote.Save() + # Handle new --repo-rev requests. + if opt.repo_rev: + wrapper = Wrapper() + remote_ref, rev = wrapper.check_repo_rev( + rp.gitdir, opt.repo_rev, repo_verify=opt.repo_verify, quiet=opt.quiet) + branch = rp.GetBranch('default') + branch.merge = remote_ref + rp.work_git.update_ref('refs/heads/default', rev) + branch.Save() + if opt.worktree: # Older versions of git supported worktree, but had dangerous gc bugs. git_require((2, 15, 0), fail=True, msg='git gc worktree corruption') diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index 73c62cc1..136f7f11 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -18,12 +18,14 @@ from __future__ import print_function +import contextlib import os import re import shutil import tempfile import unittest +import platform_utils from pyversion import is_python3 import wrapper @@ -36,6 +38,18 @@ else: from StringIO import StringIO +@contextlib.contextmanager +def TemporaryDirectory(): + """Create a new empty git checkout for testing.""" + # TODO(vapier): Convert this to tempfile.TemporaryDirectory once we drop + # Python 2 support entirely. + try: + tempdir = tempfile.mkdtemp(prefix='repo-tests') + yield tempdir + finally: + platform_utils.rmtree(tempdir) + + def fixture(*paths): """Return a path relative to tests/fixtures. """ @@ -243,8 +257,93 @@ class CheckGitVersion(RepoWrapperTestCase): self.wrapper._CheckGitVersion() -class ResolveRepoRev(RepoWrapperTestCase): - """Check resolve_repo_rev behavior.""" +class NeedSetupGnuPG(RepoWrapperTestCase): + """Check NeedSetupGnuPG behavior.""" + + def test_missing_dir(self): + """The ~/.repoconfig tree doesn't exist yet.""" + with TemporaryDirectory() as tempdir: + self.wrapper.home_dot_repo = os.path.join(tempdir, 'foo') + self.assertTrue(self.wrapper.NeedSetupGnuPG()) + + def test_missing_keyring(self): + """The keyring-version file doesn't exist yet.""" + with TemporaryDirectory() as tempdir: + self.wrapper.home_dot_repo = tempdir + self.assertTrue(self.wrapper.NeedSetupGnuPG()) + + def test_empty_keyring(self): + """The keyring-version file exists, but is empty.""" + with TemporaryDirectory() as tempdir: + self.wrapper.home_dot_repo = tempdir + with open(os.path.join(tempdir, 'keyring-version'), 'w'): + pass + self.assertTrue(self.wrapper.NeedSetupGnuPG()) + + def test_old_keyring(self): + """The keyring-version file exists, but it's old.""" + with TemporaryDirectory() as tempdir: + self.wrapper.home_dot_repo = tempdir + with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp: + fp.write('1.0\n') + self.assertTrue(self.wrapper.NeedSetupGnuPG()) + + def test_new_keyring(self): + """The keyring-version file exists, and is up-to-date.""" + with TemporaryDirectory() as tempdir: + self.wrapper.home_dot_repo = tempdir + with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp: + fp.write('1000.0\n') + self.assertFalse(self.wrapper.NeedSetupGnuPG()) + + +class SetupGnuPG(RepoWrapperTestCase): + """Check SetupGnuPG behavior.""" + + def test_full(self): + """Make sure it works completely.""" + with TemporaryDirectory() as tempdir: + self.wrapper.home_dot_repo = tempdir + self.assertTrue(self.wrapper.SetupGnuPG(True)) + with open(os.path.join(tempdir, 'keyring-version'), 'r') as fp: + data = fp.read() + self.assertEqual('.'.join(str(x) for x in self.wrapper.KEYRING_VERSION), + data.strip()) + + +class VerifyRev(RepoWrapperTestCase): + """Check verify_rev behavior.""" + + def test_verify_passes(self): + """Check when we have a valid signed tag.""" + desc_result = self.wrapper.RunResult(0, 'v1.0\n', '') + gpg_result = self.wrapper.RunResult(0, '', '') + with mock.patch.object(self.wrapper, 'run_git', + side_effect=(desc_result, gpg_result)): + ret = self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True) + self.assertEqual('v1.0^0', ret) + + def test_unsigned_commit(self): + """Check we fall back to signed tag when we have an unsigned commit.""" + desc_result = self.wrapper.RunResult(0, 'v1.0-10-g1234\n', '') + gpg_result = self.wrapper.RunResult(0, '', '') + with mock.patch.object(self.wrapper, 'run_git', + side_effect=(desc_result, gpg_result)): + ret = self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True) + self.assertEqual('v1.0^0', ret) + + def test_verify_fails(self): + """Check we fall back to signed tag when we have an unsigned commit.""" + desc_result = self.wrapper.RunResult(0, 'v1.0-10-g1234\n', '') + gpg_result = Exception + with mock.patch.object(self.wrapper, 'run_git', + side_effect=(desc_result, gpg_result)): + with self.assertRaises(Exception): + self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True) + + +class GitCheckoutTestCase(RepoWrapperTestCase): + """Tests that use a real/small git checkout.""" GIT_DIR = None REV_LIST = None @@ -274,6 +373,10 @@ class ResolveRepoRev(RepoWrapperTestCase): shutil.rmtree(cls.GIT_DIR) + +class ResolveRepoRev(GitCheckoutTestCase): + """Check resolve_repo_rev behavior.""" + def test_explicit_branch(self): """Check refs/heads/branch argument.""" rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/heads/stable') @@ -328,5 +431,54 @@ class ResolveRepoRev(RepoWrapperTestCase): self.wrapper.resolve_repo_rev(self.GIT_DIR, 'boooooooya') +class CheckRepoVerify(RepoWrapperTestCase): + """Check check_repo_verify behavior.""" + + def test_no_verify(self): + """Always fail with --no-repo-verify.""" + self.assertFalse(self.wrapper.check_repo_verify(False)) + + def test_gpg_initialized(self): + """Should pass if gpg is setup already.""" + with mock.patch.object(self.wrapper, 'NeedSetupGnuPG', return_value=False): + self.assertTrue(self.wrapper.check_repo_verify(True)) + + def test_need_gpg_setup(self): + """Should pass/fail based on gpg setup.""" + with mock.patch.object(self.wrapper, 'NeedSetupGnuPG', return_value=True): + with mock.patch.object(self.wrapper, 'SetupGnuPG') as m: + m.return_value = True + self.assertTrue(self.wrapper.check_repo_verify(True)) + + m.return_value = False + self.assertFalse(self.wrapper.check_repo_verify(True)) + + +class CheckRepoRev(GitCheckoutTestCase): + """Check check_repo_rev behavior.""" + + def test_verify_works(self): + """Should pass when verification passes.""" + with mock.patch.object(self.wrapper, 'check_repo_verify', return_value=True): + with mock.patch.object(self.wrapper, 'verify_rev', return_value='12345'): + rrev, lrev = self.wrapper.check_repo_rev(self.GIT_DIR, 'stable') + self.assertEqual('refs/heads/stable', rrev) + self.assertEqual('12345', lrev) + + def test_verify_fails(self): + """Should fail when verification fails.""" + with mock.patch.object(self.wrapper, 'check_repo_verify', return_value=True): + with mock.patch.object(self.wrapper, 'verify_rev', side_effect=Exception): + with self.assertRaises(Exception): + self.wrapper.check_repo_rev(self.GIT_DIR, 'stable') + + def test_verify_ignore(self): + """Should pass when verification is disabled.""" + with mock.patch.object(self.wrapper, 'verify_rev', side_effect=Exception): + rrev, lrev = self.wrapper.check_repo_rev(self.GIT_DIR, 'stable', repo_verify=False) + self.assertEqual('refs/heads/stable', rrev) + self.assertEqual(self.REV_LIST[1], lrev) + + if __name__ == '__main__': unittest.main()