Skip to content

Commit

Permalink
Add opt-in support for the commit-msg hook
Browse files Browse the repository at this point in the history
Enabled via the boolean config option revise.run-hooks.commit-msg.
Works in git worktrees and respects core.hooksPath.
  • Loading branch information
totph committed May 4, 2021
1 parent e27bc16 commit 244af95
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 6 deletions.
7 changes: 7 additions & 0 deletions docs/man.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===================
Expand Down
10 changes: 9 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" "May 04, 2021" "0.6.0" "git-revise"
.SH NAME
git-revise \- Efficiently update, split, and rearrange git commits
.
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion gitrevise/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .odb import Repository, Commit, Reference
from .utils import (
EditorError,
HookError,
commit_range,
edit_commit_message,
update_head,
Expand Down Expand Up @@ -217,14 +218,20 @@ 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}")
sys.exit(1)
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)
58 changes: 54 additions & 4 deletions gitrevise/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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``"""
Expand Down Expand Up @@ -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
Expand All @@ -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()


Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -181,6 +193,7 @@ def run_editor(
text=text,
comments=comments,
allow_empty=allow_empty,
post_fn=post_fn,
)


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


Expand Down

0 comments on commit 244af95

Please sign in to comment.