Skip to content

Commit

Permalink
Support pathspecs in --cut
Browse files Browse the repository at this point in the history
Sometimes I want to extract a small number of changes from a commit
that touches loads of files. Since the cut mode prompts about every
single file, there is no convenient way to do this.

Teach --cut about pathspec arguments.  This allows to use

	git revise --cut my-commit -- path/to/my/file
	a

to extract to a separate commit all changes in the given file.

In future we could support pathspecs in other subcommands.
  • Loading branch information
krobelus committed Dec 23, 2023
1 parent ccdc958 commit 9d8d89e
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 14 deletions.
6 changes: 4 additions & 2 deletions docs/man.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ SYNOPSIS
========

*git revise* [<options>] [<target>]
*git revise* [<options>] --cut <target> [--] [<pathspec>...]

DESCRIPTION
===========
Expand Down Expand Up @@ -93,8 +94,9 @@ Main modes of operation

.. option:: -c, --cut

Interactively select hunks from <target>. The chosen hunks are split into
a second commit immediately after the target.
Interactively select hunks from <target>, optionally limited by <pathspec>.
The chosen hunks are split into a second commit immediately after the
target.

After splitting is complete, both commits' messages are edited.

Expand Down
14 changes: 11 additions & 3 deletions gitrevise/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)


def build_parser() -> ArgumentParser:
def build_parser(allow_pathspecs: bool) -> ArgumentParser:
parser = ArgumentParser(
description="""\
Rebase staged changes onto the given commit, and rewrite history to
Expand All @@ -36,6 +36,12 @@ def build_parser() -> ArgumentParser:
nargs="?",
help="target commit to apply fixups to",
)
if allow_pathspecs:
parser.add_argument(
"pathspecs",
nargs="*",
help="make --cut select only from matching files",
)
parser.add_argument("--ref", default="HEAD", help="reference to update")
parser.add_argument(
"--reauthor",
Expand Down Expand Up @@ -201,7 +207,7 @@ def noninteractive(

# If the commit should be cut, prompt the user to perform the cut.
if args.cut:
current = cut_commit(current)
current = cut_commit(current, args.pathspecs)

# Add or remove GPG signatures.
if repo.sign_commits != bool(current.gpgsig):
Expand Down Expand Up @@ -258,7 +264,9 @@ def inner_main(args: Namespace, repo: Repository) -> None:


def main(argv: Optional[List[str]] = None) -> None:
args = build_parser().parse_args(argv)
args = build_parser(allow_pathspecs=True).parse_args(argv)
if not args.cut:
args = build_parser(allow_pathspecs=False).parse_args(argv)
try:
with Repository() as repo:
inner_main(args, repo)
Expand Down
12 changes: 10 additions & 2 deletions gitrevise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def update_head(ref: Reference[Commit], new: Commit, expected: Optional[Tree]) -
)


def cut_commit(commit: Commit) -> Commit:
def cut_commit(commit: Commit, pathspecs: Optional[List[str]] = None) -> Commit:
"""Perform a ``cut`` operation on the given commit, and return the
modified commit."""

Expand All @@ -274,7 +274,15 @@ def cut_commit(commit: Commit) -> Commit:

# Run an interactive git-reset to allow picking which pieces of the
# patch should go into the first part.
index.git("reset", "--patch", final_tree.persist().hex(), "--", ".", stdout=None)
index.git(
"reset",
"--patch",
final_tree.persist().hex(),
"--",
*(pathspecs if pathspecs else []),
cwd=Path.cwd(),
stdout=None,
)

# Write out the newly created tree.
mid_tree = index.tree()
Expand Down
66 changes: 66 additions & 0 deletions tests/test_cut.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from textwrap import dedent

import pytest

from gitrevise.odb import Repository

from .conftest import bash, editor_main
Expand Down Expand Up @@ -84,3 +88,65 @@ def test_cut_root(repo: Repository) -> None:

assert new_u != new
assert new_u != prev


@pytest.mark.parametrize("pathspec", ["subdir/file2", "file2"])
def test_cut_pathspec(
repo: Repository, monkeypatch: pytest.MonkeyPatch, pathspec: str
) -> None:
bash(
"""
echo "Hello, World" >> file1
git add file1
git commit -m "commit 1"
echo "Append f1" >> file1
mkdir subdir
echo "Make f2" >> subdir/file2
git add file1 subdir/file2
git commit -m "commit 2"
"""
)

if pathspec == "file2":
monkeypatch.chdir(repo.workdir / "subdir")
with editor_main(["--cut", "HEAD", pathspec], input=b"y\n") as ed:
with ed.next_file() as f:
assert f.startswith_dedent("[1] commit 2\n")
f.replace_dedent("extracted changes\n")

with ed.next_file() as f:
assert f.startswith_dedent("[2] commit 2\n")
f.replace_dedent("remaining changes\n")

assert (
repo.git("show", "HEAD~", "--format=%s").decode()
== dedent(
"""
extracted changes
diff --git a/subdir/file2 b/subdir/file2
new file mode 100644
index 0000000..93350fe
--- /dev/null
+++ b/subdir/file2
@@ -0,0 +1 @@
+Make f2"""
)[1:]
)

assert (
repo.git("show", "HEAD", "--format=%s").decode()
== dedent(
"""
remaining changes
diff --git a/file1 b/file1
index 3fa0d4b..ada44cf 100644
--- a/file1
+++ b/file1
@@ -1 +1,2 @@
Hello, World
+Append f1"""
)[1:]
)
12 changes: 5 additions & 7 deletions tests/test_edit.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from subprocess import CalledProcessError

import pytest

from gitrevise.odb import Repository

from .conftest import main


# pylint: disable=unused-argument
def test_edit_illegal_extra_argument(repo: Repository) -> None:
p = main(["--edit", "HEAD", "HEAD"], check=False, capture_output=True)
assert b'error: unrecognized arguments' in p.stderr
assert p.returncode != 0
process = main(["--edit", "HEAD", "HEAD"], check=False, capture_output=True)
assert b"error: unrecognized arguments" in process.stderr
assert process.returncode != 0

0 comments on commit 9d8d89e

Please sign in to comment.