Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/revup.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ expressions that match all possible base branches. Used to determine
which branches are supported by base branch detection. See manpage of
git-for-each-ref/fnmatch(3) for more info on glob syntax.

**--gpg-sign, --no-gpg-sign**
: Sign commits that revup creates internally (cherry-picks, synthetic
commits, and virtual diff targets) with GPG or SSH. This is required
for repositories that enforce verified-signature rules on pushed
branches. If not specified, the value of `git config commit.gpgSign`
is used. Signing uses the key configured by `user.signingKey` and
the format from `gpg.format`.

Note: `git commit-tree` (the plumbing command revup uses to create
commits) intentionally ignores `commit.gpgSign`, which is why revup
must read it explicitly and pass `-S` itself.

# REVUP COMMANDS

Revup is comprised of several sub-commands.
Expand Down
15 changes: 15 additions & 0 deletions revup/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ async def make_git(
base_branch_globs: str = "",
keep_temp: bool = False,
editor: str = "",
gpg_sign: Optional[bool] = None,
) -> "Git":
if not git_path:
git_path = get_default_git()
Expand All @@ -157,6 +158,12 @@ async def get_editor() -> str:
ret = os.environ.get("GIT_EDITOR", os.environ.get("EDITOR", "nano"))
return ret

async def get_gpg_sign() -> bool:
if gpg_sign is not None:
return gpg_sign
val = await git_ctx.git_stdout("config", "--get", "commit.gpgSign", raiseonerror=False)
return val.strip().lower() in ("true", "yes", "on", "1")

(
repo_root,
git_dir,
Expand All @@ -172,6 +179,7 @@ async def get_editor() -> str:
get_editor(),
git_ctx.is_branch_or_commit(f"{remote_name}/{main_branch}"),
)
gpg_sign_enabled = await get_gpg_sign()

if git_version:
version_arr = [int(v) for v in git_version.split(".")]
Expand All @@ -190,6 +198,7 @@ async def get_editor() -> str:
git_ctx.email = email.lower()
git_ctx.author = git_ctx.email.split("@")[0]
git_ctx.editor = editor
git_ctx.gpg_sign = gpg_sign_enabled
if not main_exists:
if main_branch in COMMON_MAIN_BRANCHES:
git_ctx.main_branch = COMMON_MAIN_BRANCHES[1 - COMMON_MAIN_BRANCHES.index(main_branch)]
Expand Down Expand Up @@ -230,6 +239,7 @@ class Git:
email: str
author: str
editor: str
gpg_sign: bool

def __init__(
self,
Expand All @@ -245,6 +255,7 @@ def __init__(
self.remote_name = remote_name
self.keep_temp = keep_temp
self.main_branch = main_branch
self.gpg_sign = False
self.base_branch_globs = base_branch_globs.strip().splitlines()
self.temp_dir = tempfile.TemporaryDirectory( # pylint: disable=consider-using-with
prefix="revup_"
Expand Down Expand Up @@ -580,6 +591,10 @@ async def commit_tree(self, commit_info: CommitHeader) -> GitCommitHash:
"-m",
commit_info.commit_msg,
]
if self.gpg_sign:
# git commit-tree (a plumbing command) intentionally ignores
# commit.gpgSign, so we pass -S explicitly when signing is enabled.
commit_tree_args.insert(2, "-S")
for p in commit_info.parents:
commit_tree_args.extend(["-p", p])
ret = await self.git_stdout(*commit_tree_args, env=git_env)
Expand Down
2 changes: 2 additions & 0 deletions revup/revup.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def make_toplevel_parser() -> RevupArgParser:
revup_parser.add_argument("--main-branch", default="main")
revup_parser.add_argument("--base-branch-globs", default="")
revup_parser.add_argument("--git-version", default="2.43.0")
revup_parser.add_argument("--gpg-sign", action="store_true", default=None)
for s in ShellType:
revup_parser.add_argument(
f"--prompt-completion-{s.value}", default="", help=argparse.SUPPRESS
Expand Down Expand Up @@ -107,6 +108,7 @@ async def get_git(args: argparse.Namespace) -> git.Git:
args.base_branch_globs,
args.keep_temp,
args.editor,
args.gpg_sign,
)

return git_ctx
Expand Down
81 changes: 81 additions & 0 deletions tests/test_gpg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from git_env import GitTestEnvironment, async_test

from revup import git


async def _set_gpg_config(env, value):
await env.sh.sh(env._git_path, "config", "commit.gpgSign", value)


async def _make_git_in_env(env, gpg_sign=None):
return await git.make_git(
env.sh,
git_path=env._git_path,
remote_name="origin",
main_branch="main",
editor="true",
gpg_sign=gpg_sign,
)


class TestMakeGitGpgSign:
@async_test
async def test_reads_from_git_config_true(self):
async with GitTestEnvironment() as env:
await _set_gpg_config(env, "true")
git_ctx = await _make_git_in_env(env)
assert git_ctx.gpg_sign is True

@async_test
async def test_reads_from_git_config_false(self):
async with GitTestEnvironment() as env:
await _set_gpg_config(env, "false")
git_ctx = await _make_git_in_env(env)
assert git_ctx.gpg_sign is False

@async_test
async def test_cli_override_true_when_config_unset(self):
async with GitTestEnvironment() as env:
git_ctx = await _make_git_in_env(env, gpg_sign=True)
assert git_ctx.gpg_sign is True

@async_test
async def test_cli_override_false_wins_over_config(self):
async with GitTestEnvironment() as env:
await _set_gpg_config(env, "true")
git_ctx = await _make_git_in_env(env, gpg_sign=False)
assert git_ctx.gpg_sign is False


class TestCommitTreeSigning:
async def _get_real_commit_header(self, env):
commit_hash = await env.commit("base", {"a.txt": "hello"})
raw = await env.git_ctx.rev_list(commit_hash, max_revs=1, header=True)
return git.parse_rev_list(raw)[0]

async def _capture_commit_tree_args(self, env, gpg_sign, mocker):
commit_info = await self._get_real_commit_header(env)
env.git_ctx.gpg_sign = gpg_sign

async def fake_git_stdout(*args, **kwargs):
return "fakecommithash"

spy = mocker.patch.object(env.git_ctx, "git_stdout", side_effect=fake_git_stdout)

result = await env.git_ctx.commit_tree(commit_info)
assert result == "fakecommithash"
return spy.call_args.args

@async_test
async def test_dash_s_present_when_gpg_sign_enabled(self, mocker):
async with GitTestEnvironment() as env:
args = await self._capture_commit_tree_args(env, True, mocker)
assert "commit-tree" in args
assert "-S" in args

@async_test
async def test_dash_s_absent_when_gpg_sign_disabled(self, mocker):
async with GitTestEnvironment() as env:
args = await self._capture_commit_tree_args(env, False, mocker)
assert "commit-tree" in args
assert "-S" not in args