from __future__ import annotations import io import os import shutil import subprocess import tempfile import unittest from contextlib import redirect_stdout from pathlib import Path from unittest.mock import patch from pkgmgr.actions.release.retry import retry_release from pkgmgr.actions.release.workflow import _release_impl from pkgmgr.actions.repository.push import ( PushRefusedError, push_branch, sign_unpushed, ) from pkgmgr.cli.context import CLIContext from pkgmgr.cli.parser import create_parser from pkgmgr.cli.proxy import maybe_handle_proxy def git(cwd: str, *args: str) -> str: return subprocess.run( ["git", *args], cwd=cwd, check=True, capture_output=True, text=True ).stdout.strip() @unittest.skipUnless(shutil.which("ssh-keygen"), "ssh-keygen signs the commits") class TestPushMirrorLeases(unittest.TestCase): def setUp(self) -> None: self._tmp = tempfile.TemporaryDirectory() self.addCleanup(self._tmp.cleanup) root = Path(os.path.realpath(self._tmp.name)) self.root = str(root) key = root / "key" subprocess.run( ["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)], check=True, ) (root / "allowed").write_text(f"t@example.org {(root / 'key.pub').read_text()}") (root / "gitconfig").write_text( "[user]\n\tname = t\n\temail = t@example.org\n" f"\tsigningkey = {key}\n" "[gpg]\n\tformat = ssh\n" f'[gpg "ssh"]\n\tallowedSignersFile = {root / "allowed"}\n' "[commit]\n\tgpgsign = false\n" "[init]\n\tdefaultBranch = main\n" ) env = patch.dict( os.environ, {"GIT_CONFIG_GLOBAL": str(root / "gitconfig"), "GIT_CONFIG_NOSYSTEM": "1"}, ) env.start() self.addCleanup(env.stop) self.canonical = str(root / "canonical.git") self.mirror = str(root / "mirror.git") self.repo = str(root / "repo") git(self.root, "init", "-q", "--bare", self.canonical) git(self.root, "init", "-q", "--bare", self.mirror) git(self.root, "init", "-q", self.repo) git(self.repo, "remote", "add", "origin", self.canonical) git(self.repo, "remote", "set-url", "--add", "--push", "origin", self.canonical) git(self.repo, "remote", "set-url", "--add", "--push", "origin", self.mirror) self.init = self._commit("init") git(self.repo, "push", "-q", self.canonical, "main") git(self.repo, "push", "-q", self.mirror, "main") git(self.repo, "fetch", "-q", "origin") git(self.repo, "branch", "-q", "--set-upstream-to=origin/main") def _commit(self, name: str, cwd: str | None = None, *flags: str) -> str: cwd = cwd or self.repo Path(cwd, f"{name}.txt").write_text(name) git(cwd, "add", f"{name}.txt") git(cwd, "commit", "-q", *flags, "-m", name) return git(cwd, "rev-parse", "HEAD") def _push(self, rewrite: bool = False) -> str: with redirect_stdout(io.StringIO()) as out: sign_unpushed(self.repo) push_branch(self.repo, [], sign=True, rewrite=rewrite) return out.getvalue() def _cli_push(self, cwd: str, *argv: str) -> None: ctx = CLIContext( config_merged={}, repositories_base_dir=os.path.join(self.root, "base"), all_repositories=[], binaries_dir=os.path.join(self.root, "bin"), user_config_path=os.path.join(self.root, "config.yaml"), ) args = create_parser("pkgmgr").parse_args(["push", "-j", "1", *argv]) prev = os.getcwd() os.chdir(cwd) self.addCleanup(os.chdir, prev) with redirect_stdout(io.StringIO()), self.assertRaises(SystemExit) as cm: maybe_handle_proxy(args, ctx) self.assertEqual(cm.exception.code, 0) def _tip(self, bare: str, ref: str = "refs/heads/main") -> str: return subprocess.run( ["git", "rev-parse", "--verify", "--quiet", ref], cwd=bare, check=False, capture_output=True, text=True, ).stdout.strip() def _assert_shipped_signed(self) -> None: head = git(self.repo, "rev-parse", "HEAD") self.assertEqual(self._tip(self.canonical), head) self.assertEqual(self._tip(self.mirror), head) signatures = git(self.repo, "log", "--format=%G?", f"{self.init}..HEAD") self.assertEqual(set(signatures.split()), {"G"}) def test_mirror_equal_gets_the_signed_commit(self) -> None: self._commit("feature") self._push() self._assert_shipped_signed() def test_mirror_behind_is_fast_forwarded(self) -> None: self._commit("first", None, "-S") git(self.repo, "push", "-q", self.canonical, "main") git(self.repo, "fetch", "-q", "origin") self._commit("second") self._push() self._assert_shipped_signed() def test_mirror_on_older_rewrite_is_replaced_after_resign(self) -> None: unsigned = self._commit("feature") git(self.repo, "push", "-q", self.mirror, "main") self._push() self._assert_shipped_signed() self.assertNotEqual(self._tip(self.mirror), unsigned) def test_mirror_only_commit_is_refused_and_kept(self) -> None: other = os.path.join(self.root, "other") git(self.root, "clone", "-q", self.mirror, other) foreign = self._commit("foreign", other) git(other, "push", "-q", "origin", "main") self._commit("feature") with self.assertRaises(PushRefusedError) as cm: self._push() with self.assertRaises(PushRefusedError): self._push(rewrite=True) self.assertIn(self.mirror, str(cm.exception)) self.assertEqual(self._tip(self.mirror), foreign) self.assertEqual(self._tip(self.canonical), git(self.repo, "rev-parse", "HEAD")) def test_content_rewrite_needs_the_rewrite_flag(self) -> None: self._commit("feature") self._push() shipped = git(self.repo, "rev-parse", "HEAD") Path(self.repo, "feature.txt").write_text("changed") git(self.repo, "commit", "-q", "-a", "--amend", "--no-edit") with self.assertRaises(PushRefusedError) as cm: self._push() self.assertIn(self.canonical, str(cm.exception)) self.assertEqual(self._tip(self.canonical), shipped) self.assertEqual(self._tip(self.mirror), shipped) self._push(rewrite=True) self._assert_shipped_signed() self.assertNotEqual(self._tip(self.mirror), shipped) def test_worktree_outside_base_dir_is_pushed(self) -> None: worktree = os.path.join(self.root, "wt") git(self.repo, "worktree", "add", "-q", "-b", "feature", worktree) head_before = self._commit("feature", worktree) self._cli_push(worktree) head = git(worktree, "rev-parse", "HEAD") self.assertNotEqual(head, head_before) self.assertEqual(self._tip(self.canonical, "refs/heads/feature"), head) self.assertEqual(self._tip(self.mirror, "refs/heads/feature"), head) self.assertEqual( git(worktree, "rev-parse", "--abbrev-ref", "@{u}"), "origin/feature" ) def test_no_sign_pushes_unsigned_commits_to_every_url(self) -> None: unsigned = self._commit("feature") self._cli_push(self.repo, "--no-sign") self.assertEqual(git(self.repo, "rev-parse", "HEAD"), unsigned) self.assertEqual(self._tip(self.canonical), unsigned) self.assertEqual(self._tip(self.mirror), unsigned) self.assertEqual(git(self.repo, "log", "-1", "--format=%G?"), "N") def test_only_head_is_signed_and_older_unsigned_commits_stay(self) -> None: older = self._commit("older") self._commit("head") out = self._push() head = git(self.repo, "rev-parse", "HEAD") self.assertEqual(git(self.repo, "rev-parse", "HEAD^"), older) self.assertEqual(git(self.repo, "log", "-1", "--format=%G?", head), "G") self.assertEqual(git(self.repo, "log", "-1", "--format=%G?", older), "N") self.assertIn("WARN 1 older unsigned commit(s)", out) self.assertEqual(self._tip(self.canonical), head) self.assertEqual(self._tip(self.mirror), head) def test_pushed_remote_branch_is_the_base_without_upstream(self) -> None: git(self.repo, "checkout", "-q", "-b", "feature") with patch.dict(os.environ, {"GIT_COMMITTER_DATE": "2001-01-01T00:00:00+0000"}): pushed = self._commit("pushed", None, "-S") git(self.repo, "push", "-q", "origin", "feature") git(self.repo, "fetch", "-q", "origin") self._commit("new") upstream = subprocess.run( ["git", "rev-parse", "--abbrev-ref", "@{u}"], cwd=self.repo, check=False, capture_output=True, ) self.assertNotEqual(upstream.returncode, 0) out = self._push() head = git(self.repo, "rev-parse", "HEAD") self.assertEqual(git(self.repo, "rev-parse", "HEAD^"), pushed) self.assertEqual(git(self.repo, "log", "-1", "--format=%G?", head), "G") self.assertNotIn("WARN", out) self.assertEqual(self._tip(self.canonical, "refs/heads/feature"), head) def test_repository_config_that_runs_a_program_is_refused(self) -> None: self._commit("feature") for key in ( "core.sshCommand", "core.gitProxy", "core.askPass", "core.fsmonitor", "core.alternateRefsCommand", "credential.helper", "credential.https://example.org.helper", "gpg.program", "gpg.ssh.program", "gpg.ssh.defaultKeyCommand", "protocol.ext.allow", "hook.probe.command", "gc.recentObjectsHook", "http.proxy", "http.sslVerify", "http.sslCAInfo", "http.sslCAPath", "http.cookieFile", "http.saveCookies", "remote.origin.receivepack", "remote.origin.uploadpack", "remote.origin.vcs", "remote.origin.proxy", "url.x.insteadOf", "url.x.pushInsteadOf", ): for push in (self._push, lambda: push_branch(self.repo, [], sign=False)): with self.subTest(key=key, push=push): git(self.repo, "config", key, "false") with self.assertRaises(PushRefusedError) as cm: push() git(self.repo, "config", "--unset", key) self.assertIn(key.lower(), str(cm.exception)) self.assertEqual(self._tip(self.canonical), self.init) self.assertEqual(self._tip(self.mirror), self.init) def test_ordinary_repository_config_is_pushed(self) -> None: for key, value in ( ("core.hooksPath", ".husky/_"), ("filter.lfs.process", "git-lfs filter-process"), ("gpg.format", "ssh"), ("credential.useHttpPath", "true"), ("include.path", "../shared.gitconfig"), ): git(self.repo, "config", key, value) self._commit("feature") self._push() self._assert_shipped_signed() def test_staged_gitlink_is_not_folded_into_the_signed_head(self) -> None: self._commit("feature") head = git(self.repo, "rev-parse", "HEAD") git(self.repo, "config", "diff.ignoreSubmodules", "all") git(self.repo, "update-index", "--add", "--cacheinfo", f"160000,{head},sub") with self.assertRaises(PushRefusedError) as cm: self._push() self.assertIn("staged changes", str(cm.exception)) self.assertEqual(git(self.repo, "rev-parse", "HEAD"), head) def test_unstaged_edit_stays_out_of_the_signed_head(self) -> None: self._commit("feature") Path(self.repo, "feature.txt").write_text("unstaged") self._push() self._assert_shipped_signed() self.assertEqual(git(self.repo, "status", "--porcelain"), "M feature.txt") def test_signing_amend_runs_no_repository_hook(self) -> None: marker = Path(self.root, "hooked") hook = Path(self.repo, ".git", "hooks", "post-commit") hook.write_text(f"#!/bin/sh\ntouch {marker}\n") hook.chmod(0o755) self._commit("feature") marker.unlink() self._push() self._assert_shipped_signed() self.assertFalse(marker.exists()) def test_signing_amend_runs_no_config_hook(self) -> None: marker = Path(self.root, "hooked") git(self.repo, "config", "hook.probe.command", f"touch {marker}") git(self.repo, "config", "hook.probe.event", "post-commit") self._commit("feature") hooks_fire = marker.exists() marker.unlink(missing_ok=True) try: self._push() except PushRefusedError: pass self.assertFalse(marker.exists(), f"config hooks fire on commit: {hooks_fire}") def test_mirror_lease_runs_no_textconv(self) -> None: marker = Path(self.root, "textconv") Path(self.repo, ".gitattributes").write_text("* diff=probe\n") git(self.repo, "add", ".gitattributes") git(self.repo, "commit", "-q", "-S", "-m", "attributes") git(self.repo, "push", "-q", self.mirror, "main") git(self.repo, "config", "diff.probe.textconv", f"touch {marker};cat") self._commit("feature") git(self.repo, "diff", "HEAD~1", "HEAD") self.assertTrue(marker.exists()) marker.unlink() self._push() self._assert_shipped_signed() self.assertFalse(marker.exists()) def test_remote_named_like_an_option_is_pushed(self) -> None: git(self.repo, "config", "remote.-r.url", self.canonical) git(self.repo, "config", "remote.-r.fetch", "+refs/heads/*:refs/remotes/-r/*") git(self.repo, "config", "branch.main.pushRemote", "-r") git(self.repo, "fetch", "-q", "--", "-r") self._commit("feature") self._push() self.assertEqual(self._tip(self.canonical), git(self.repo, "rev-parse", "HEAD")) def test_release_refuses_unsafe_config_before_any_change(self) -> None: head = git(self.repo, "rev-parse", "HEAD") git(self.repo, "config", "core.sshCommand", "false") cwd = os.getcwd() os.chdir(self.repo) self.addCleanup(os.chdir, cwd) with redirect_stdout(io.StringIO()), self.assertRaises(PushRefusedError): _release_impl( pyproject_path=os.path.join(self.repo, "pyproject.toml"), force=True ) self.assertEqual(git(self.repo, "rev-parse", "HEAD"), head) self.assertEqual(git(self.repo, "status", "--porcelain"), "") def test_release_retry_refuses_unsigned_range(self) -> None: self._commit("release") git(self.repo, "tag", "-a", "v1.0.0", "-m", "v1.0.0") cwd = os.getcwd() os.chdir(self.repo) self.addCleanup(os.chdir, cwd) with redirect_stdout(io.StringIO()), self.assertRaises(PushRefusedError): retry_release(pyproject_path=os.path.join(self.repo, "pyproject.toml")) self.assertEqual(self._tip(self.canonical), git(self.repo, "rev-parse", "@{u}")) self.assertEqual(self._tip(self.canonical, "refs/tags/v1.0.0"), "") if __name__ == "__main__": unittest.main()