diff --git a/src/pkgmgr/actions/release/retry.py b/src/pkgmgr/actions/release/retry.py index 88ab699..8147564 100644 --- a/src/pkgmgr/actions/release/retry.py +++ b/src/pkgmgr/actions/release/retry.py @@ -51,7 +51,7 @@ def retry_release( tag = max(tags, key=SemVer.parse) print(f"Re-pushing existing tag: {tag}") - push_branch(".", [], refs=(tag,), preview=preview) + push_branch(".", [], sign=True, refs=(tag,), preview=preview) try: if is_highest_version_tag(tag): diff --git a/src/pkgmgr/actions/release/workflow.py b/src/pkgmgr/actions/release/workflow.py index e9bd2cc..b801177 100644 --- a/src/pkgmgr/actions/release/workflow.py +++ b/src/pkgmgr/actions/release/workflow.py @@ -139,7 +139,7 @@ def _release_impl( commit(commit_msg, all=True, preview=True) sign_unpushed(".", preview=True) tag_annotated(new_tag, tag_msg, preview=True) - push_branch(".", [], refs=(new_tag,), preview=True) + push_branch(".", [], sign=True, refs=(new_tag,), preview=True) if is_highest_version_tag(new_tag): update_latest_tag(new_tag, preview=True) @@ -162,7 +162,7 @@ def _release_impl( sign_unpushed(".", preview=False) tag_annotated(new_tag, tag_msg, preview=False) - push_branch(".", [], refs=(new_tag,), preview=False) + push_branch(".", [], sign=True, refs=(new_tag,), preview=False) # Update 'latest' only if this is the highest version tag try: diff --git a/src/pkgmgr/actions/repository/push.py b/src/pkgmgr/actions/repository/push.py index 9266bfe..f62a272 100644 --- a/src/pkgmgr/actions/repository/push.py +++ b/src/pkgmgr/actions/repository/push.py @@ -129,6 +129,7 @@ def push_branch( repo_dir: str, extra_args: list[str], *, + sign: bool, refs: tuple[str, ...] = (), rewrite: bool = False, preview: bool = False, @@ -144,18 +145,20 @@ def push_branch( Args: repo_dir: checkout whose current branch is pushed. extra_args: flags appended to every `git push`. + sign: refuse unsigned commits in the push range; False pushes them as they are. refs: extra refs (e.g. a release tag) pushed to every URL. rewrite: replace commits of the old canonical tip; the fetch URL skips the cherry check, mirrors only refuse commits the old tip never had. preview: print the pushes instead of running them. """ branch, remote = _push_target(repo_dir) - _base, signatures = _unpushed_signatures(repo_dir, remote) - if not preview and any(s != "G" for s in signatures): - raise PushRefusedError( - f"{repo_dir}: {sum(s != 'G' for s in signatures)} unsigned commit(s) " - "in the push range; run `pkgmgr push` to sign them" - ) + if sign: + _base, signatures = _unpushed_signatures(repo_dir, remote) + if not preview and any(s != "G" for s in signatures): + raise PushRefusedError( + f"{repo_dir}: {sum(s != 'G' for s in signatures)} unsigned commit(s) " + "in the push range; run `pkgmgr push` to sign them" + ) head = run(["rev-parse", "HEAD"], cwd=repo_dir) fetch_url = run(["remote", "get-url", remote], cwd=repo_dir) @@ -216,11 +219,12 @@ def push_branch( def _push_one( - repo_dir: str, extra_args: list[str], rewrite: bool, preview: bool + repo_dir: str, extra_args: list[str], sign: bool, rewrite: bool, preview: bool ) -> tuple[bool, str]: try: - sign_unpushed(repo_dir, preview=preview) - push_branch(repo_dir, extra_args, rewrite=rewrite, preview=preview) + if sign: + sign_unpushed(repo_dir, preview=preview) + push_branch(repo_dir, extra_args, sign=sign, rewrite=rewrite, preview=preview) return (True, "") except (GitRunError, PushRefusedError) as exc: return (False, str(exc)) @@ -230,6 +234,7 @@ def push_repo_dirs( repos: list[RepoRef], extra_args: list[str], preview: bool, + sign: bool, jobs: int = 1, rewrite: bool = False, ) -> None: @@ -238,7 +243,7 @@ def push_repo_dirs( """ run_on_repos( repos, - lambda rd: _push_one(rd, extra_args, rewrite, preview), + lambda rd: _push_one(rd, extra_args, sign, rewrite, preview), jobs=jobs, op_name="push", ) @@ -250,6 +255,7 @@ def push_in_parallel( all_repos: list[Repository], extra_args: list[str], preview: bool, + sign: bool, jobs: int = 1, rewrite: bool = False, ) -> None: @@ -260,6 +266,7 @@ def push_in_parallel( resolve_repos(selected_repos, repositories_base_dir, all_repos), extra_args, preview, + sign, jobs=jobs, rewrite=rewrite, ) diff --git a/src/pkgmgr/cli/proxy.py b/src/pkgmgr/cli/proxy.py index 7ec831f..eefa066 100644 --- a/src/pkgmgr/cli/proxy.py +++ b/src/pkgmgr/cli/proxy.py @@ -196,6 +196,15 @@ def register_proxy_commands( "already has (amend, squash, drop); leases still apply." ), ) + parser.add_argument( + "--no-sign", + action="store_true", + default=False, + help=( + "Push unsigned commits as they are instead of re-signing " + "them; leases still apply." + ), + ) if subcommand == "clone": parser.add_argument( "--clone-mode", @@ -230,6 +239,7 @@ def maybe_handle_proxy(args: argparse.Namespace, ctx: CLIContext) -> bool: [(toplevel, toplevel)], args.extra_args, args.preview, + not args.no_sign, jobs=args.jobs, rewrite=args.rewrite, ) @@ -277,6 +287,7 @@ def maybe_handle_proxy(args: argparse.Namespace, ctx: CLIContext) -> bool: ctx.all_repositories, args.extra_args, args.preview, + not args.no_sign, jobs=args.jobs, rewrite=args.rewrite, ) diff --git a/tests/integration/test_push_mirror_leases.py b/tests/integration/test_push_mirror_leases.py index f398779..7032fa2 100644 --- a/tests/integration/test_push_mirror_leases.py +++ b/tests/integration/test_push_mirror_leases.py @@ -80,9 +80,27 @@ class TestPushMirrorLeases(unittest.TestCase): def _push(self, rewrite: bool = False) -> str: with redirect_stdout(io.StringIO()) as out: sign_unpushed(self.repo) - push_branch(self.repo, [], rewrite=rewrite) + 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], @@ -164,22 +182,9 @@ class TestPushMirrorLeases(unittest.TestCase): worktree = os.path.join(self.root, "wt") git(self.repo, "worktree", "add", "-q", "-b", "feature", worktree) head_before = self._commit("feature", worktree) - 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"]) - cwd = os.getcwd() - os.chdir(worktree) - self.addCleanup(os.chdir, cwd) - with redirect_stdout(io.StringIO()), self.assertRaises(SystemExit) as cm: - maybe_handle_proxy(args, ctx) + self._cli_push(worktree) - self.assertEqual(cm.exception.code, 0) head = git(worktree, "rev-parse", "HEAD") self.assertNotEqual(head, head_before) self.assertEqual(self._tip(self.canonical, "refs/heads/feature"), head) @@ -188,6 +193,16 @@ class TestPushMirrorLeases(unittest.TestCase): 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_release_retry_refuses_unsigned_range(self) -> None: self._commit("release") git(self.repo, "tag", "-a", "v1.0.0", "-m", "v1.0.0") diff --git a/tests/unit/pkgmgr/actions/release/test_retry.py b/tests/unit/pkgmgr/actions/release/test_retry.py index b3bbecc..c6a4274 100644 --- a/tests/unit/pkgmgr/actions/release/test_retry.py +++ b/tests/unit/pkgmgr/actions/release/test_retry.py @@ -83,7 +83,9 @@ class TestRetryRelease(unittest.TestCase): retry_release(pyproject_path="pyproject.toml", preview=False) - mock_push.assert_called_once_with(".", [], refs=("v1.13.4",), preview=False) + mock_push.assert_called_once_with( + ".", [], sign=True, refs=("v1.13.4",), preview=False + ) mock_update_latest.assert_called_once_with("v1.13.4", preview=False) @patch("pkgmgr.actions.release.retry.update_latest_tag") @@ -143,7 +145,9 @@ class TestRetryRelease(unittest.TestCase): retry_release(pyproject_path="pyproject.toml", preview=False) - mock_push.assert_called_once_with(".", [], refs=("v2.0.0",), preview=False) + mock_push.assert_called_once_with( + ".", [], sign=True, refs=("v2.0.0",), preview=False + ) if __name__ == "__main__":