| # Copyright (C) 2015 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Unittests for the wrapper.py module.""" |
| |
| import io |
| import os |
| import re |
| import subprocess |
| import sys |
| from unittest import mock |
| |
| import pytest |
| import utils_for_test |
| |
| import main |
| import wrapper |
| |
| |
| @pytest.fixture(autouse=True) |
| def reset_wrapper() -> None: |
| """Reset the wrapper module every time.""" |
| wrapper.Wrapper.cache_clear() |
| |
| |
| @pytest.fixture |
| def repo_wrapper() -> wrapper.Wrapper: |
| """Fixture for the wrapper module.""" |
| return wrapper.Wrapper() |
| |
| |
| class GitCheckout: |
| """Class to hold git checkout info for tests.""" |
| |
| def __init__(self, git_dir, rev_list): |
| self.git_dir = git_dir |
| self.rev_list = rev_list |
| |
| |
| @pytest.fixture(scope="module") |
| def git_checkout(tmp_path_factory) -> GitCheckout: |
| """Fixture for tests that use a real/small git checkout. |
| |
| Create a repo to operate on, but do it once per-test-run. |
| """ |
| tempdir = tmp_path_factory.mktemp("repo-rev-tests") |
| run_git = wrapper.Wrapper().run_git |
| |
| remote = os.path.join(tempdir, "remote") |
| os.mkdir(remote) |
| |
| utils_for_test.init_git_tree(remote) |
| run_git("commit", "--allow-empty", "-minit", cwd=remote) |
| run_git("branch", "stable", cwd=remote) |
| run_git("tag", "v1.0", cwd=remote) |
| run_git("commit", "--allow-empty", "-m2nd commit", cwd=remote) |
| rev_list = run_git("rev-list", "HEAD", cwd=remote).stdout.splitlines() |
| |
| run_git("init", cwd=tempdir) |
| run_git( |
| "fetch", |
| remote, |
| "+refs/heads/*:refs/remotes/origin/*", |
| cwd=tempdir, |
| ) |
| yield GitCheckout(tempdir, rev_list) |
| |
| |
| class TestRepoWrapper: |
| """Tests helper functions in the repo wrapper""" |
| |
| def test_version(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Make sure _Version works.""" |
| with pytest.raises(SystemExit) as e: |
| with mock.patch("sys.stdout", new_callable=io.StringIO) as stdout: |
| with mock.patch( |
| "sys.stderr", new_callable=io.StringIO |
| ) as stderr: |
| repo_wrapper._Version() |
| assert e.value.code == 0 |
| assert stderr.getvalue() == "" |
| assert "repo launcher version" in stdout.getvalue() |
| |
| def test_python_constraints(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """The launcher should never require newer than main.py.""" |
| assert ( |
| main.MIN_PYTHON_VERSION_HARD >= repo_wrapper.MIN_PYTHON_VERSION_HARD |
| ) |
| assert ( |
| main.MIN_PYTHON_VERSION_SOFT >= repo_wrapper.MIN_PYTHON_VERSION_SOFT |
| ) |
| # Make sure the versions are themselves in sync. |
| assert ( |
| repo_wrapper.MIN_PYTHON_VERSION_SOFT |
| >= repo_wrapper.MIN_PYTHON_VERSION_HARD |
| ) |
| |
| def test_repo_script_is_executable(self) -> None: |
| """The repo launcher script should be executable.""" |
| repo_path = utils_for_test.THIS_DIR.parent / "repo" |
| assert os.access(repo_path, os.X_OK), f"{repo_path} is not executable" |
| |
| def test_init_parser(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Make sure 'init' GetParser works.""" |
| parser = repo_wrapper.GetParser() |
| opts, args = parser.parse_args([]) |
| assert args == [] |
| assert opts.manifest_url is None |
| |
| |
| class TestSetGitTrace2ParentSid: |
| """Check SetGitTrace2ParentSid behavior.""" |
| |
| KEY = "GIT_TRACE2_PARENT_SID" |
| VALID_FORMAT = re.compile(r"^repo-[0-9]{8}T[0-9]{6}Z-P[0-9a-f]{8}$") |
| |
| def test_first_set(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Test env var not yet set.""" |
| env = {} |
| repo_wrapper.SetGitTrace2ParentSid(env) |
| assert self.KEY in env |
| value = env[self.KEY] |
| assert self.VALID_FORMAT.match(value) |
| |
| def test_append(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Test env var is appended.""" |
| env = {self.KEY: "pfx"} |
| repo_wrapper.SetGitTrace2ParentSid(env) |
| assert self.KEY in env |
| value = env[self.KEY] |
| assert value.startswith("pfx/") |
| assert self.VALID_FORMAT.match(value[4:]) |
| |
| def test_global_context(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check os.environ gets updated by default.""" |
| os.environ.pop(self.KEY, None) |
| repo_wrapper.SetGitTrace2ParentSid() |
| assert self.KEY in os.environ |
| value = os.environ[self.KEY] |
| assert self.VALID_FORMAT.match(value) |
| |
| |
| class TestRunCommand: |
| """Check run_command behavior.""" |
| |
| def test_capture(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check capture_output handling.""" |
| ret = repo_wrapper.run_command(["echo", "hi"], capture_output=True) |
| # echo command appends OS specific linesep, but on Windows + Git Bash |
| # we get UNIX ending, so we allow both. |
| assert ret.stdout in ["hi" + os.linesep, "hi\n"] |
| |
| def test_check(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check check handling.""" |
| repo_wrapper.run_command(["true"], check=False) |
| repo_wrapper.run_command(["true"], check=True) |
| repo_wrapper.run_command(["false"], check=False) |
| with pytest.raises(subprocess.CalledProcessError): |
| repo_wrapper.run_command(["false"], check=True) |
| |
| |
| class TestRunGit: |
| """Check run_git behavior.""" |
| |
| def test_capture(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check capture_output handling.""" |
| ret = repo_wrapper.run_git("--version") |
| assert "git" in ret.stdout |
| |
| def test_check(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check check handling.""" |
| with pytest.raises(repo_wrapper.CloneFailure): |
| repo_wrapper.run_git("--version-asdfasdf") |
| repo_wrapper.run_git("--version-asdfasdf", check=False) |
| |
| |
| class TestParseGitVersion: |
| """Check ParseGitVersion behavior.""" |
| |
| def test_autoload(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check we can load the version from the live git.""" |
| assert repo_wrapper.ParseGitVersion() is not None |
| |
| def test_bad_ver(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check handling of bad git versions.""" |
| assert repo_wrapper.ParseGitVersion(ver_str="asdf") is None |
| |
| def test_normal_ver(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check handling of normal git versions.""" |
| ret = repo_wrapper.ParseGitVersion(ver_str="git version 2.25.1") |
| assert ret.major == 2 |
| assert ret.minor == 25 |
| assert ret.micro == 1 |
| assert ret.full == "2.25.1" |
| |
| def test_extended_ver(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check handling of extended distro git versions.""" |
| ret = repo_wrapper.ParseGitVersion( |
| ver_str="git version 1.30.50.696.g5e7596f4ac-goog" |
| ) |
| assert ret.major == 1 |
| assert ret.minor == 30 |
| assert ret.micro == 50 |
| assert ret.full == "1.30.50.696.g5e7596f4ac-goog" |
| |
| |
| class TestCheckGitVersion: |
| """Check _CheckGitVersion behavior.""" |
| |
| def test_unknown(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Unknown versions should abort.""" |
| with mock.patch.object( |
| repo_wrapper, "ParseGitVersion", return_value=None |
| ): |
| with pytest.raises(repo_wrapper.CloneFailure): |
| repo_wrapper._CheckGitVersion() |
| |
| def test_old(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Old versions should abort.""" |
| with mock.patch.object( |
| repo_wrapper, |
| "ParseGitVersion", |
| return_value=repo_wrapper.GitVersion(1, 0, 0, "1.0.0"), |
| ): |
| with pytest.raises(repo_wrapper.CloneFailure): |
| repo_wrapper._CheckGitVersion() |
| |
| def test_new(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Newer versions should run fine.""" |
| with mock.patch.object( |
| repo_wrapper, |
| "ParseGitVersion", |
| return_value=repo_wrapper.GitVersion(100, 0, 0, "100.0.0"), |
| ): |
| repo_wrapper._CheckGitVersion() |
| |
| |
| class TestRequirements: |
| """Check Requirements handling.""" |
| |
| def test_missing_file(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Don't crash if the file is missing (old version).""" |
| assert ( |
| repo_wrapper.Requirements.from_dir(utils_for_test.THIS_DIR) is None |
| ) |
| assert ( |
| repo_wrapper.Requirements.from_file( |
| utils_for_test.THIS_DIR / "xxxxxxxxxxxxxxxxxxxxxxxx" |
| ) |
| is None |
| ) |
| |
| def test_corrupt_data(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """If the file can't be parsed, don't blow up.""" |
| assert repo_wrapper.Requirements.from_file(__file__) is None |
| assert repo_wrapper.Requirements.from_data(b"x") is None |
| |
| def test_valid_data(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Make sure we can parse the file we ship.""" |
| assert repo_wrapper.Requirements.from_data(b"{}") is not None |
| rootdir = utils_for_test.THIS_DIR.parent |
| assert repo_wrapper.Requirements.from_dir(rootdir) is not None |
| assert ( |
| repo_wrapper.Requirements.from_file(rootdir / "requirements.json") |
| is not None |
| ) |
| |
| def test_format_ver(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check format_ver can format.""" |
| assert repo_wrapper.Requirements._format_ver((1, 2, 3)) == "1.2.3" |
| assert repo_wrapper.Requirements._format_ver([1]) == "1" |
| |
| def test_assert_all_unknown(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check assert_all works with incompatible file.""" |
| reqs = repo_wrapper.Requirements({}) |
| reqs.assert_all() |
| |
| def test_assert_all_new_repo(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check assert_all accepts new enough repo.""" |
| reqs = repo_wrapper.Requirements({"repo": {"hard": [1, 0]}}) |
| reqs.assert_all() |
| |
| def test_assert_all_old_repo(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check assert_all rejects old repo.""" |
| reqs = repo_wrapper.Requirements({"repo": {"hard": [99999, 0]}}) |
| with pytest.raises(SystemExit): |
| reqs.assert_all() |
| |
| def test_assert_all_new_python(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check assert_all accepts new enough python.""" |
| reqs = repo_wrapper.Requirements({"python": {"hard": sys.version_info}}) |
| reqs.assert_all() |
| |
| def test_assert_all_old_python(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check assert_all rejects old python.""" |
| reqs = repo_wrapper.Requirements({"python": {"hard": [99999, 0]}}) |
| with pytest.raises(SystemExit): |
| reqs.assert_all() |
| |
| def test_assert_ver_unknown(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check assert_ver works with incompatible file.""" |
| reqs = repo_wrapper.Requirements({}) |
| reqs.assert_ver("xxx", (1, 0)) |
| |
| def test_assert_ver_new(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check assert_ver allows new enough versions.""" |
| reqs = repo_wrapper.Requirements( |
| {"git": {"hard": [1, 0], "soft": [2, 0]}} |
| ) |
| reqs.assert_ver("git", (1, 0)) |
| reqs.assert_ver("git", (1, 5)) |
| reqs.assert_ver("git", (2, 0)) |
| reqs.assert_ver("git", (2, 5)) |
| |
| def test_assert_ver_old(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check assert_ver rejects old versions.""" |
| reqs = repo_wrapper.Requirements( |
| {"git": {"hard": [1, 0], "soft": [2, 0]}} |
| ) |
| with pytest.raises(SystemExit): |
| reqs.assert_ver("git", (0, 5)) |
| |
| |
| class TestNeedSetupGnuPG: |
| """Check NeedSetupGnuPG behavior.""" |
| |
| def test_missing_dir(self, tmp_path, repo_wrapper: wrapper.Wrapper) -> None: |
| """The ~/.repoconfig tree doesn't exist yet.""" |
| repo_wrapper.home_dot_repo = str(tmp_path / "foo") |
| assert repo_wrapper.NeedSetupGnuPG() |
| |
| def test_missing_keyring( |
| self, tmp_path, repo_wrapper: wrapper.Wrapper |
| ) -> None: |
| """The keyring-version file doesn't exist yet.""" |
| repo_wrapper.home_dot_repo = str(tmp_path) |
| assert repo_wrapper.NeedSetupGnuPG() |
| |
| def test_empty_keyring( |
| self, tmp_path, repo_wrapper: wrapper.Wrapper |
| ) -> None: |
| """The keyring-version file exists, but is empty.""" |
| repo_wrapper.home_dot_repo = str(tmp_path) |
| (tmp_path / "keyring-version").write_text("") |
| assert repo_wrapper.NeedSetupGnuPG() |
| |
| def test_old_keyring(self, tmp_path, repo_wrapper: wrapper.Wrapper) -> None: |
| """The keyring-version file exists, but it's old.""" |
| repo_wrapper.home_dot_repo = str(tmp_path) |
| (tmp_path / "keyring-version").write_text("1.0\n") |
| assert repo_wrapper.NeedSetupGnuPG() |
| |
| def test_new_keyring(self, tmp_path, repo_wrapper: wrapper.Wrapper) -> None: |
| """The keyring-version file exists, and is up-to-date.""" |
| repo_wrapper.home_dot_repo = str(tmp_path) |
| (tmp_path / "keyring-version").write_text("1000.0\n") |
| assert not repo_wrapper.NeedSetupGnuPG() |
| |
| |
| class TestSetupGnuPG: |
| """Check SetupGnuPG behavior.""" |
| |
| def test_full(self, tmp_path, repo_wrapper: wrapper.Wrapper) -> None: |
| """Make sure it works completely.""" |
| repo_wrapper.home_dot_repo = str(tmp_path) |
| repo_wrapper.gpg_dir = str(tmp_path / "gnupg") |
| assert repo_wrapper.SetupGnuPG(True) |
| data = (tmp_path / "keyring-version").read_text() |
| assert ( |
| ".".join(str(x) for x in repo_wrapper.KEYRING_VERSION) |
| == data.strip() |
| ) |
| |
| |
| class TestVerifyRev: |
| """Check verify_rev behavior.""" |
| |
| def test_verify_passes(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check when we have a valid signed tag.""" |
| desc_result = subprocess.CompletedProcess([], 0, "v1.0\n", "") |
| gpg_result = subprocess.CompletedProcess([], 0, "", "") |
| with mock.patch.object( |
| repo_wrapper, "run_git", side_effect=(desc_result, gpg_result) |
| ): |
| ret = repo_wrapper.verify_rev( |
| "/", "refs/heads/stable", "1234", True |
| ) |
| assert ret == "v1.0^0" |
| |
| def test_unsigned_commit(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check we fall back to signed tag when we have an unsigned commit.""" |
| desc_result = subprocess.CompletedProcess([], 0, "v1.0-10-g1234\n", "") |
| gpg_result = subprocess.CompletedProcess([], 0, "", "") |
| with mock.patch.object( |
| repo_wrapper, "run_git", side_effect=(desc_result, gpg_result) |
| ): |
| ret = repo_wrapper.verify_rev( |
| "/", "refs/heads/stable", "1234", True |
| ) |
| assert ret == "v1.0^0" |
| |
| def test_verify_fails(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Check we fall back to signed tag when we have an unsigned commit.""" |
| desc_result = subprocess.CompletedProcess([], 0, "v1.0-10-g1234\n", "") |
| gpg_result = RuntimeError |
| with mock.patch.object( |
| repo_wrapper, "run_git", side_effect=(desc_result, gpg_result) |
| ): |
| with pytest.raises(RuntimeError): |
| repo_wrapper.verify_rev("/", "refs/heads/stable", "1234", True) |
| |
| |
| class TestResolveRepoRev: |
| """Check resolve_repo_rev behavior.""" |
| |
| def test_explicit_branch( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Check refs/heads/branch argument.""" |
| rrev, lrev = repo_wrapper.resolve_repo_rev( |
| git_checkout.git_dir, "refs/heads/stable" |
| ) |
| assert rrev == "refs/heads/stable" |
| assert lrev == git_checkout.rev_list[1] |
| |
| with pytest.raises(repo_wrapper.CloneFailure): |
| repo_wrapper.resolve_repo_rev( |
| git_checkout.git_dir, "refs/heads/unknown" |
| ) |
| |
| def test_explicit_tag( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Check refs/tags/tag argument.""" |
| rrev, lrev = repo_wrapper.resolve_repo_rev( |
| git_checkout.git_dir, "refs/tags/v1.0" |
| ) |
| assert rrev == "refs/tags/v1.0" |
| assert lrev == git_checkout.rev_list[1] |
| |
| with pytest.raises(repo_wrapper.CloneFailure): |
| repo_wrapper.resolve_repo_rev( |
| git_checkout.git_dir, "refs/tags/unknown" |
| ) |
| |
| def test_branch_name( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Check branch argument.""" |
| rrev, lrev = repo_wrapper.resolve_repo_rev( |
| git_checkout.git_dir, "stable" |
| ) |
| assert rrev == "refs/heads/stable" |
| assert lrev == git_checkout.rev_list[1] |
| |
| rrev, lrev = repo_wrapper.resolve_repo_rev(git_checkout.git_dir, "main") |
| assert rrev == "refs/heads/main" |
| assert lrev == git_checkout.rev_list[0] |
| |
| def test_tag_name( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Check tag argument.""" |
| rrev, lrev = repo_wrapper.resolve_repo_rev(git_checkout.git_dir, "v1.0") |
| assert rrev == "refs/tags/v1.0" |
| assert lrev == git_checkout.rev_list[1] |
| |
| def test_full_commit( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Check specific commit argument.""" |
| commit = git_checkout.rev_list[0] |
| rrev, lrev = repo_wrapper.resolve_repo_rev(git_checkout.git_dir, commit) |
| assert rrev == commit |
| assert lrev == commit |
| |
| def test_partial_commit( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Check specific (partial) commit argument.""" |
| commit = git_checkout.rev_list[0][0:20] |
| rrev, lrev = repo_wrapper.resolve_repo_rev(git_checkout.git_dir, commit) |
| assert rrev == git_checkout.rev_list[0] |
| assert lrev == git_checkout.rev_list[0] |
| |
| def test_unknown( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Check unknown ref/commit argument.""" |
| with pytest.raises(repo_wrapper.CloneFailure): |
| repo_wrapper.resolve_repo_rev(git_checkout.git_dir, "boooooooya") |
| |
| |
| class TestCheckRepoVerify: |
| """Check check_repo_verify behavior.""" |
| |
| def test_no_verify(self, repo_wrapper: wrapper.Wrapper) -> None: |
| """Always fail with --no-repo-verify.""" |
| assert not repo_wrapper.check_repo_verify(False) |
| |
| def test_gpg_initialized( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| ) -> None: |
| """Should pass if gpg is setup already.""" |
| with mock.patch.object( |
| repo_wrapper, "NeedSetupGnuPG", return_value=False |
| ): |
| assert repo_wrapper.check_repo_verify(True) |
| |
| def test_need_gpg_setup( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| ) -> None: |
| """Should pass/fail based on gpg setup.""" |
| with mock.patch.object( |
| repo_wrapper, "NeedSetupGnuPG", return_value=True |
| ): |
| with mock.patch.object(repo_wrapper, "SetupGnuPG") as m: |
| m.return_value = True |
| assert repo_wrapper.check_repo_verify(True) |
| |
| m.return_value = False |
| assert not repo_wrapper.check_repo_verify(True) |
| |
| |
| class TestCheckRepoRev: |
| """Check check_repo_rev behavior.""" |
| |
| def test_verify_works( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Should pass when verification passes.""" |
| with mock.patch.object( |
| repo_wrapper, "check_repo_verify", return_value=True |
| ): |
| with mock.patch.object( |
| repo_wrapper, "verify_rev", return_value="12345" |
| ): |
| rrev, lrev = repo_wrapper.check_repo_rev( |
| git_checkout.git_dir, "stable" |
| ) |
| assert rrev == "refs/heads/stable" |
| assert lrev == "12345" |
| |
| def test_verify_fails( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Should fail when verification fails.""" |
| with mock.patch.object( |
| repo_wrapper, "check_repo_verify", return_value=True |
| ): |
| with mock.patch.object( |
| repo_wrapper, "verify_rev", side_effect=RuntimeError |
| ): |
| with pytest.raises(RuntimeError): |
| repo_wrapper.check_repo_rev(git_checkout.git_dir, "stable") |
| |
| def test_verify_ignore( |
| self, |
| repo_wrapper: wrapper.Wrapper, |
| git_checkout: GitCheckout, |
| ) -> None: |
| """Should pass when verification is disabled.""" |
| with mock.patch.object( |
| repo_wrapper, "verify_rev", side_effect=RuntimeError |
| ): |
| rrev, lrev = repo_wrapper.check_repo_rev( |
| git_checkout.git_dir, "stable", repo_verify=False |
| ) |
| assert rrev == "refs/heads/stable" |
| assert lrev == git_checkout.rev_list[1] |