From 244af956388561415abd621fc6665ab5151300ff Mon Sep 17 00:00:00 2001 From: Thomas Otto Date: Thu, 11 Feb 2021 16:29:59 +0100 Subject: [PATCH] Add opt-in support for the commit-msg hook Enabled via the boolean config option revise.run-hooks.commit-msg. Works in git worktrees and respects core.hooksPath. --- docs/man.rst | 7 ++++++ git-revise.1 | 10 +++++++- gitrevise/tui.py | 9 ++++++- gitrevise/utils.py | 58 ++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/docs/man.rst b/docs/man.rst index e47e285..ad8ef82 100644 --- a/docs/man.rst +++ b/docs/man.rst @@ -126,6 +126,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.run-hooks.commit-msg + + If set to true the **commit-msg** hook will be run after exiting the + editor. Defaults to false, because (unlike with :manpage:`git-rebase(1)`) + the worktree state might not reflect the commit state. If the hook takes + the worktree state into account, it might behave differently. + CONFLICT RESOLUTION =================== diff --git a/git-revise.1 b/git-revise.1 index ed1e0f6..7810725 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" "May 04, 2021" "0.6.0" "git-revise" .SH NAME git-revise \- Efficiently update, split, and rearrange git commits . @@ -147,6 +147,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.run\-hooks.commit\-msg +If set to true the \fBcommit\-msg\fP hook will be run after exiting the +editor. Defaults to false, because (unlike with \fBgit\-rebase(1)\fP) +the worktree state might not reflect the commit state. If the hook takes +the worktree state into account, it might behave differently. +.UNINDENT .SH CONFLICT RESOLUTION .sp When a conflict is encountered, \fBgit revise\fP will attempt to resolve diff --git a/gitrevise/tui.py b/gitrevise/tui.py index 8734522..7fec8ab 100644 --- a/gitrevise/tui.py +++ b/gitrevise/tui.py @@ -6,6 +6,7 @@ from .odb import Repository, Commit, Reference from .utils import ( EditorError, + HookError, commit_range, edit_commit_message, update_head, @@ -217,7 +218,10 @@ def main(argv: Optional[List[str]] = None) -> None: with Repository() as repo: inner_main(args, repo) except CalledProcessError as err: - print(f"subprocess exited with non-zero status: {err.returncode}") + if err.returncode != 0: + print(f"subprocess exited with non-zero status: {err.returncode}") + else: + print(f"subprocess error: {err}") sys.exit(1) except EditorError as err: print(f"editor error: {err}") @@ -225,6 +229,9 @@ def main(argv: Optional[List[str]] = None) -> None: except MergeConflict as err: print(f"merge conflict: {err}") sys.exit(1) + except HookError as err: + print(f"{err} hook declined") + sys.exit(1) except ValueError as err: print(f"invalid value: {err}") sys.exit(1) diff --git a/gitrevise/utils.py b/gitrevise/utils.py index 2311b02..f18c9c5 100644 --- a/gitrevise/utils.py +++ b/gitrevise/utils.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import Callable, List, Optional, Tuple from subprocess import run, CalledProcessError from pathlib import Path import textwrap @@ -14,6 +14,10 @@ class EditorError(Exception): pass +class HookError(Exception): + pass + + def commit_range(base: Commit, tip: Commit) -> List[Commit]: """Oldest-first iterator over the given commit range, not including the commit ``base``""" @@ -57,7 +61,9 @@ def local_commits(repo: Repository, tip: Commit) -> Tuple[Commit, List[Commit]]: return base, commits -def edit_file_with_editor(editor: str, path: Path) -> bytes: +def edit_file_with_editor( + editor: str, path: Path, post_fn: Optional[Callable[[Path], None]] = None +) -> bytes: try: if os.name == "nt": # The popular "Git for Windows" distribution uses a bundled msys @@ -71,6 +77,10 @@ def edit_file_with_editor(editor: str, path: Path) -> bytes: run(cmd, check=True, cwd=path.parent) except CalledProcessError as err: raise EditorError(f"Editor exited with status {err}") from err + + if post_fn: + post_fn(path) + return path.read_bytes() @@ -127,6 +137,7 @@ def run_specific_editor( comments: Optional[str] = None, allow_empty: bool = False, allow_whitespace_before_comments: bool = False, + post_fn: Optional[Callable[[Path], None]] = None, ) -> bytes: """Run the editor configured for git to edit the given text""" path = repo.get_tempdir() / filename @@ -144,7 +155,7 @@ def run_specific_editor( handle.write(b"\n") # Invoke the editor - data = edit_file_with_editor(editor, path) + data = edit_file_with_editor(editor, path, post_fn) if comments: data = strip_comments( data, @@ -172,6 +183,7 @@ def run_editor( text: bytes, comments: Optional[str] = None, allow_empty: bool = False, + post_fn: Optional[Callable[[Path], None]] = None, ) -> bytes: """Run the editor configured for git to edit the given text""" return run_specific_editor( @@ -181,6 +193,7 @@ def run_editor( text=text, comments=comments, allow_empty=allow_empty, + post_fn=post_fn, ) @@ -215,6 +228,31 @@ def run_sequence_editor( ) +def create_commit_msg_caller(repo: Repository) -> Optional[Callable[[Path], None]]: + """Return a function which calls the "commit-msg" hook if it is + present""" + + hooks_path = repo.config("core.hooksPath", b"") + + if not hooks_path: + commit_msg_hook = os.path.join( + repo.git("rev-parse", "--git-common-dir"), b"hooks/commit-msg" + ) + else: + commit_msg_hook = os.path.join(hooks_path, b"commit-msg") + + def run_commit_msg(filepath: Path) -> None: + # If the hook script is present but not executable git would warn + # (unless `git config advice.ignoredHook false` is set), but git-revise + # silently ignores that file. + if os.access(commit_msg_hook, os.X_OK): + # stderr/stdout of the hook are passed through + if run([commit_msg_hook, filepath]).returncode != 0: + raise HookError("commit-msg") + + return run_commit_msg + + def edit_commit_message(commit: Commit) -> Commit: """Launch an editor to edit the commit message of ``commit``, returning a modified commit""" @@ -231,7 +269,19 @@ def edit_commit_message(commit: Commit) -> Commit: tree_b = commit.tree().persist().hex() comments += "\n" + repo.git("diff-tree", "--stat", tree_a, tree_b).decode() - message = run_editor(repo, "COMMIT_EDITMSG", commit.message, comments=comments) + if repo.bool_config("revise.run-hooks.commit-msg", default=False): + hook = create_commit_msg_caller(commit.repo) + else: + hook = None + + message = run_editor( + repo, + "COMMIT_EDITMSG", + commit.message, + comments=comments, + post_fn=hook, + ) + return commit.update(message=message)