From eb0befa451f7ae0228be48dfffba13bb3ed5a006 Mon Sep 17 00:00:00 2001 From: Nicolas Schier Date: Thu, 27 Aug 2020 14:43:50 +0200 Subject: [PATCH] [pullreqs/73] Add support for GPG-signed commits (#46) GPG-sign commits if option -S/--gpg-sign is given, or if configurations revise.gpgSign or commit.gpgSign are set to true. The keyid used for signing can be specified by an optional argument to '-S' or '--gpg-sign' as for git-commit; if not specified explicitly, default key id is taken from git configuration (user.signingKey) or the committer signature as fallback. A call of `git-revise COMMIT --gpg-sign` w/o any changes in the index causes creation of GPG commit signatures for all commits since COMMIT. Signed-off-by: Nicolas Schier --- docs/man.rst | 12 +++++++++ git-revise.1 | 16 +++++++++++- gitrevise/odb.py | 65 +++++++++++++++++++++++++++++++++++++++++++--- gitrevise/tui.py | 23 ++++++++++++++++ gitrevise/utils.py | 8 ++++++ 5 files changed, 120 insertions(+), 4 deletions(-) diff --git a/docs/man.rst b/docs/man.rst index e47e285..c7c1742 100644 --- a/docs/man.rst +++ b/docs/man.rst @@ -58,6 +58,11 @@ General options Working branch to update; defaults to ``HEAD``. +.. option:: -S, --gpg-sign, --no-gpg-sign + + GPG-sign commits. To override the ``commit.gpgSign`` git configuration, use + ``--no-gpg-sign`` + Main modes of operation ----------------------- @@ -126,6 +131,13 @@ Configuration is managed by :manpage:`git-config(1)`. is specified. Overridden by :option:`--no-autosquash`. Defaults to false. If not set, the value of ``rebase.autoSquash`` is used instead. +.. gitconfig:: revise.gpgSign + + If set to true, GPG-sign new commits; defaults to false. This setting + overrides the original git configuration ``Bcommit.gpgSign`` and may be + overridden by the command line options ``--gpg-sign`` and + ``--no-gpg-sign``. + CONFLICT RESOLUTION =================== diff --git a/git-revise.1 b/git-revise.1 index ed1e0f6..bea5741 100644 --- a/git-revise.1 +++ b/git-revise.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "GIT-REVISE" "1" "Jun 07, 2020" "0.6.0" "git-revise" +.TH "GIT-REVISE" "1" "Oct 05, 2020" "0.6.0" "git-revise" .SH NAME git-revise \- Efficiently update, split, and rearrange git commits . @@ -79,6 +79,12 @@ Reset target commit\(aqs author to the current user. .B \-\-ref Working branch to update; defaults to \fBHEAD\fP\&. .UNINDENT +.INDENT 0.0 +.TP +.B \-S, \-\-gpg\-sign, \-\-no\-gpg\-sign +GPG\-sign commits. To override the \fBcommit.gpgSign\fP git configuration, use +\fB\-\-no\-gpg\-sign\fP +.UNINDENT .SS Main modes of operation .INDENT 0.0 .TP @@ -147,6 +153,14 @@ If set to true, imply \fI\%\-\-autosquash\fP whenever \fI\%\-\-interactive\fP is specified. Overridden by \fI\%\-\-no\-autosquash\fP\&. Defaults to false. If not set, the value of \fBrebase.autoSquash\fP is used instead. .UNINDENT +.INDENT 0.0 +.TP +.B revise.gpgSign +If set to true, GPG\-sign new commits; defaults to false. This setting +overrides the original git configuration \fBBcommit.gpgSign\fP and may be +overridden by the command line options \fB\-\-gpg\-sign\fP and +\fB\-\-no\-gpg\-sign\fP\&. +.UNINDENT .SH CONFLICT RESOLUTION .sp When a conflict is encountered, \fBgit revise\fP will attempt to resolve diff --git a/gitrevise/odb.py b/gitrevise/odb.py index 038204a..44c6ae1 100644 --- a/gitrevise/odb.py +++ b/gitrevise/odb.py @@ -17,6 +17,7 @@ Tuple, cast, ) +import sys from types import TracebackType from pathlib import Path from enum import Enum @@ -134,6 +135,15 @@ class Repository: index: "Index" """current index state""" + sign_commits: bool + """sign commits with gpg""" + + key_id: bytes + """key ID to be used for commit signing""" + + gpg: bytes + """path to GnuPG binary""" + _objects: Dict[int, Dict[Oid, "GitObj"]] _catfile: Popen _tempdir: Optional[TemporaryDirectory] @@ -144,6 +154,9 @@ class Repository: "default_author", "default_committer", "index", + "sign_commits", + "key_id", + "gpg", "_objects", "_catfile", "_tempdir", @@ -162,6 +175,22 @@ def __init__(self, cwd: Optional[Path] = None) -> None: self.index = Index(self) + self.sign_commits = self.bool_config( + "revise.gpgSign", default=self.bool_config("commit.gpgSign", default=False) + ) + + self.key_id = self.default_committer.name + if self.default_committer.email: + if self.key_id: + self.key_id += b" " + self.key_id += b"<" + self.default_committer.email + b">" + else: + self.key_id = self.default_committer.email + + self.key_id = self.config("user.signingKey", default=self.key_id) + + self.gpg = self.config("gpg.program", default=b"gpg") + self._catfile = Popen( ["git", "cat-file", "--batch"], bufsize=-1, @@ -270,8 +299,33 @@ def new_commit( body += b"parent " + parent.oid.hex().encode() + b"\n" body += b"author " + author + b"\n" body += b"committer " + committer + b"\n" - body += b"\n" - body += message + + body_tail = b"\n" + message + + if self.sign_commits: + try: + gpg = run( + [self.gpg, "--status-fd=2", "-bsau", self.key_id], + stdout=PIPE, + stderr=PIPE, + input=body + body_tail, + check=True, + ) + + body += b"gpgsig" + for line in gpg.stdout.splitlines(): + body += b" " + line + b"\n" + + if not b"\n[GNUPG:] SIG_CREATED " in gpg.stdout: + raise CalledProcessError() + + except CalledProcessError as gpg: + print(gpg.stderr.decode(), file=sys.stderr, end="") + print("gpg failed to sign commit", file=sys.stderr) + sys.exit(1) + + body += body_tail + return Commit(self, body) def new_tree(self, entries: Mapping[bytes, "Entry"]) -> "Tree": @@ -472,10 +526,13 @@ class Commit(GitObj): committer: Signature """:class:`Signature` of this commit's committer""" + gpgsig: bytes + """GPG signature of this commit""" + message: bytes """Body of this commit's message""" - __slots__ = ("tree_oid", "parent_oids", "author", "committer", "message") + __slots__ = ("tree_oid", "parent_oids", "author", "committer", "gpgsig", "message") def _parse_body(self) -> None: # Split the header from the core commit message. @@ -497,6 +554,8 @@ def _parse_body(self) -> None: self.author = Signature(value) elif key == b"committer": self.committer = Signature(value) + elif key == b"gpgsig": + self.gpgsig = value def tree(self) -> "Tree": """``tree`` object corresponding to this commit""" diff --git a/gitrevise/tui.py b/gitrevise/tui.py index 8734522..85f2a0a 100644 --- a/gitrevise/tui.py +++ b/gitrevise/tui.py @@ -10,6 +10,7 @@ edit_commit_message, update_head, cut_commit, + sign_commit, local_commits, ) from .todo import apply_todos, build_todos, edit_todos, autosquash_todos @@ -88,6 +89,18 @@ def build_parser() -> ArgumentParser: action="store_true", help="interactively cut a commit into two smaller commits", ) + + mode_group = parser.add_mutually_exclusive_group() + mode_group.add_argument( + "--gpg-sign", + "-S", + help="GPG sign commits", + ) + mode_group.add_argument( + "--no-gpg-sign", + action="store_true", + help="do not GPG sign commits", + ) return parser @@ -172,6 +185,9 @@ def noninteractive( if args.cut: current = cut_commit(current) + if repo.sign_commits and not hasattr(current, "gpgsig"): + current = sign_commit(current) + if current != replaced: print(f"{current.oid.short()} {current.summary()}") @@ -192,6 +208,13 @@ def inner_main(args: Namespace, repo: Repository) -> None: if args.patch: repo.git("add", "-p") + if args.gpg_sign is not None: + repo.sign_commits = True + if args.gpg_sign: + repo.key_id = args.gpg_sign + if args.no_gpg_sign: + repo.sign_commits = False + # Create a commit with changes from the index staged = None if not args.no_index: diff --git a/gitrevise/utils.py b/gitrevise/utils.py index 2311b02..d63a6c3 100644 --- a/gitrevise/utils.py +++ b/gitrevise/utils.py @@ -297,3 +297,11 @@ def cut_commit(commit: Commit) -> Commit: part2 = edit_commit_message(part2) return part2 + + +def sign_commit(commit: Commit) -> Commit: + """Sign the given commit with GPG and return the modified commit.""" + + return commit.repo.new_commit( + commit.tree(), commit.parents(), message=commit.message + )