Skip to content

Commit

Permalink
[pullreqs/73] Add support for GPG-signed commits (mystor#46)
Browse files Browse the repository at this point in the history
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 <nicolas@fjasle.eu>
  • Loading branch information
Nicolas Schier authored and krobelus committed Mar 21, 2021
1 parent 543863f commit eb0befa
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 4 deletions.
12 changes: 12 additions & 0 deletions docs/man.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------------

Expand Down Expand Up @@ -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
===================
Expand Down
16 changes: 15 additions & 1 deletion git-revise.1
Original file line number Diff line number Diff line change
@@ -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
.
Expand Down Expand Up @@ -79,6 +79,12 @@ Reset target commit\(aqs author to the current user.
.B \-\-ref <gitref>
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
Expand Down Expand Up @@ -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
Expand Down
65 changes: 62 additions & 3 deletions gitrevise/odb.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Tuple,
cast,
)
import sys
from types import TracebackType
from pathlib import Path
from enum import Enum
Expand Down Expand Up @@ -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]
Expand All @@ -144,6 +154,9 @@ class Repository:
"default_author",
"default_committer",
"index",
"sign_commits",
"key_id",
"gpg",
"_objects",
"_catfile",
"_tempdir",
Expand All @@ -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,
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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.
Expand All @@ -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"""
Expand Down
23 changes: 23 additions & 0 deletions gitrevise/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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()}")

Expand All @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions gitrevise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

0 comments on commit eb0befa

Please sign in to comment.