Repository: VirtusLab/git-machete
Pull request #1303 (2024-08-17 21:42:58)

Allow for pass-through arguments to `git machete diff` and `git machete log`


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 703dc1a..8339afb 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,9 @@
 # Release notes
 
+## New in git-machete 3.28.0
+
+- added: ability to specify pass-through flags in `diff` and `log`, for example `git machete diff -- file.txt`, `git machete log -- --patch` (partly contributed by @tdyas)
+
 ## New in git-machete 3.27.0
 
 - added: git config key `http.sslVerify` is honored when connecting to GitHub and GitLab APIs (suggested by @scamden)
diff --git a/ci/checks/prohibit-fork-point-in-git-context.sh b/ci/checks/prohibit-fork-point-in-git-context.sh
new file mode 100755
index 0000000..393deee
--- /dev/null
+++ b/ci/checks/prohibit-fork-point-in-git-context.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail -u
+
+file=git_machete/git_operations.py
+if git grep -n 'fork.point' $file; then
+  echo
+  echo "The above lines in $file refer to 'fork point'."
+  echo "$file should be oblivious to git-machete-specific concepts like fork point, please rename or move the code"
+  exit 1
+fi
diff --git a/ci/checks/run-all-checks.sh b/ci/checks/run-all-checks.sh
index 4a98e64..e847130 100755
--- a/ci/checks/run-all-checks.sh
+++ b/ci/checks/run-all-checks.sh
@@ -28,6 +28,7 @@ enforce-shell-scripts-pass-shellcheck.sh
 prohibit-bash-usages-from-python.sh
 prohibit-deploy-step-in-circleci.sh
 prohibit-exempli-gratia-in-rst.sh
+prohibit-fork-point-in-git-context.sh
 prohibit-id-est-in-rst.sh
 prohibit-double-backticks-in-python.sh
 prohibit-markdown-links-in-rst.sh
diff --git a/docs/man/git-machete.1 b/docs/man/git-machete.1
index 1fcf784..ed1d172 100644
--- a/docs/man/git-machete.1
+++ b/docs/man/git-machete.1
@@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "GIT-MACHETE" "1" "Aug 08, 2024" "" "git-machete"
+.TH "GIT-MACHETE" "1" "Aug 17, 2024" "" "git-machete"
 .SH NAME
-git-machete \- git-machete 3.27.0
+git-machete \- git-machete 3.28.0
 .sp
 git machete is a robust tool that \fBsimplifies your git workflows\fP\&.
 .sp
@@ -627,7 +627,7 @@ Don\(aqt ask for confirmation.
 .INDENT 3.5
 .sp
 .EX
-git machete d[iff] [\-s|\-\-stat] [<branch>]
+git machete d[iff] [\-s|\-\-stat] [<branch>] [\-\- <pass\-through\-arguments>]
 .EE
 .UNINDENT
 .UNINDENT
@@ -639,11 +639,9 @@ See help for \fI\%fork\-point\fP for more details on the meaning of \fIfork poin
 Note: the branch in question does not need to occur in the branch layout file.
 .sp
 \fBOptions:\fP
-.INDENT 0.0
-.TP
-.B  \-s\fP,\fB  \-\-stat
-Makes \fBgit machete diff\fP pass \fB\-\-stat\fP option to \fBgit diff\fP, so that only summary (diffstat) is printed.
-.UNINDENT
+.sp
+\-\- <pass\-through\-arguments>    Arguments to pass directly to the underlying \fBgit diff\fP, for example \fBgit machete diff \-\- \-\-name\-only\fP\&.
+\-s, \-\-stat                     Makes \fBgit machete diff\fP pass \fB\-\-stat\fP option to \fBgit diff\fP, so that only summary (diffstat) is printed.
 .SH DISCOVER
 .sp
 \fBUsage:\fP
@@ -1490,7 +1488,7 @@ This command is generally not meant for a day\-to\-day use, it\(aqs mostly neede
 .INDENT 3.5
 .sp
 .EX
-git machete l[og] [<branch>]
+git machete l[og] [<branch>] [\-\- <pass\-through\-arguments>]
 .EE
 .UNINDENT
 .UNINDENT
@@ -1499,6 +1497,10 @@ Runs \fBgit log\fP for the range of commits from tip of the given branch (or cur
 See help for \fI\%fork\-point\fP for more details on meaning of the \fIfork point\fP\&.
 .sp
 Note: the branch in question does not need to occur in the branch layout file.
+.sp
+\fBOptions:\fP
+.sp
+\-\- <pass\-through\-arguments>    Arguments to pass directly to the underlying \fBgit log\fP, for example \fBgit machete log \-\- \-\-patch\fP\&.
 .SH REAPPLY
 .sp
 \fBUsage:\fP
diff --git a/docs/source/cli/diff.rst b/docs/source/cli/diff.rst
index af37551..deddab3 100644
--- a/docs/source/cli/diff.rst
+++ b/docs/source/cli/diff.rst
@@ -6,7 +6,7 @@ diff
 
 .. code-block:: shell
 
-    git machete d[iff] [-s|--stat] [<branch>]
+    git machete d[iff] [-s|--stat] [<branch>] [-- <pass-through-arguments>]
 
 Runs ``git diff`` of the given branch tip against its fork point or, if none specified,
 of the current working tree against the fork point of the currently checked out branch.
@@ -16,4 +16,5 @@ Note: the branch in question does not need to occur in the branch layout file.
 
 **Options:**
 
--s, --stat    Makes ``git machete diff`` pass ``--stat`` option to ``git diff``, so that only summary (diffstat) is printed.
+-- <pass-through-arguments>    Arguments to pass directly to the underlying ``git diff``, for example ``git machete diff -- --name-only``.
+-s, --stat                     Makes ``git machete diff`` pass ``--stat`` option to ``git diff``, so that only summary (diffstat) is printed.
diff --git a/docs/source/cli/log.rst b/docs/source/cli/log.rst
index cb6716d..35683c4 100644
--- a/docs/source/cli/log.rst
+++ b/docs/source/cli/log.rst
@@ -6,9 +6,13 @@ log
 
 .. code-block:: shell
 
-    git machete l[og] [<branch>]
+    git machete l[og] [<branch>] [-- <pass-through-arguments>]
 
 Runs ``git log`` for the range of commits from tip of the given branch (or current branch, if none specified) back to its fork point.
 See help for :ref:`fork-point` for more details on meaning of the *fork point*.
 
 Note: the branch in question does not need to occur in the branch layout file.
+
+**Options:**
+
+-- <pass-through-arguments>    Arguments to pass directly to the underlying ``git log``, for example ``git machete log -- --patch``.
diff --git a/git_machete/__init__.py b/git_machete/__init__.py
index 5a85b26..89c638d 100644
--- a/git_machete/__init__.py
+++ b/git_machete/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '3.27.0'
+__version__ = '3.28.0'
diff --git a/git_machete/cli.py b/git_machete/cli.py
index 142e056..afda4d3 100644
--- a/git_machete/cli.py
+++ b/git_machete/cli.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 
 import argparse
+import itertools
 import os
 import pkgutil
 import re
@@ -567,8 +568,10 @@ def launch(orig_args: List[str]) -> None:
         cli_opts = git_machete.options.CommandLineOptions()
         git = GitContext()
 
+        direct_args = list(itertools.takewhile(lambda arg: arg != "--", orig_args))
+        pass_through_args = list(itertools.dropwhile(lambda arg: arg != "--", orig_args))
         cli_parser: argparse.ArgumentParser = create_cli_parser()
-        parsed_cli: argparse.Namespace = cli_parser.parse_args(orig_args)
+        parsed_cli: argparse.Namespace = cli_parser.parse_args(direct_args)
         parsed_cli_as_dict: Dict[str, Any] = vars(parsed_cli)
 
         # Let's set up options like debug/verbose before we first start reading `git config`.
@@ -577,12 +580,18 @@ def launch(orig_args: List[str]) -> None:
         update_cli_options_using_parsed_args(cli_opts, parsed_cli)
         cli_opts.validate()
 
-        if not orig_args:
+        if not direct_args:
             print(get_help_description(display_help_topics=False))
             sys.exit(ExitCode.ARGUMENT_ERROR)
 
         cmd = parsed_cli.command
 
+        if cmd not in ("d", "diff", "l", "log") and pass_through_args:
+            print(fmt("Extra arguments after `--` are only allowed after `diff` and `log`"))
+            sys.exit(ExitCode.ARGUMENT_ERROR)
+        if pass_through_args and pass_through_args[0] == "--":
+            pass_through_args = pass_through_args[1:]
+
         if cmd == "completion":
             completion_shell = parsed_cli.shell
 
@@ -670,7 +679,7 @@ def launch(orig_args: List[str]) -> None:
         elif cmd in {"diff", alias_by_command["diff"]}:
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             diff_branch = get_local_branch_short_name_from_arg(cli_opts.opt_branch) if (cli_opts.opt_branch is not None) else None
-            machete_client.diff(branch=diff_branch, opt_stat=cli_opts.opt_stat)
+            machete_client.display_diff(branch=diff_branch, opt_stat=cli_opts.opt_stat, extra_git_diff_args=pass_through_args)
         elif cmd == "discover":
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             machete_client.discover_tree(
@@ -822,7 +831,7 @@ def launch(orig_args: List[str]) -> None:
         elif cmd in {"log", alias_by_command["log"]}:
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             branch = get_local_branch_short_name_from_arg_or_current_branch(cli_opts.opt_branch, git)
-            machete_client.log(branch)
+            machete_client.display_log(branch, extra_git_log_args=pass_through_args)
         elif cmd == "reapply":
             machete_client.read_branch_layout_file(perform_interactive_slide_out=should_perform_interactive_slide_out)
             git.expect_no_operation_in_progress()
diff --git a/git_machete/client.py b/git_machete/client.py
index e5786af..7f1e27f 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -1421,19 +1421,16 @@ class MacheteClient:
         except MacheteException:
             return None
 
-    def diff(self, *, branch: Optional[LocalBranchShortName], opt_stat: bool) -> None:
+    def display_diff(self, *, branch: Optional[LocalBranchShortName], opt_stat: bool, extra_git_diff_args: List[str]) -> None:
         diff_branch = branch or self.__git.get_current_branch()
         fork_point = self.fork_point(diff_branch, use_overrides=True)
 
-        self.__git.display_diff(
-            # In case no param has been supplied, we want to diff against the current working directory, not the current branch.
-            branch=branch,
-            fork_point=fork_point,
-            format_with_stat=opt_stat)
+        self.__git.display_diff(branch=branch, against=fork_point, opt_stat=opt_stat, extra_git_diff_args=extra_git_diff_args)
 
-    def log(self, branch: LocalBranchShortName) -> None:
+    def display_log(self, branch: LocalBranchShortName, extra_git_log_args: List[str]) -> None:
         fork_point = self.fork_point(branch, use_overrides=True)
-        self.__git.display_branch_history_from_fork_point(branch.full_name(), fork_point)
+        self.__git.display_log_between(from_inclusive=branch.full_name(), until_exclusive=fork_point,
+                                       extra_git_log_args=extra_git_log_args)
 
     def down(self, branch: LocalBranchShortName, pick_mode: bool) -> List[LocalBranchShortName]:
         self.expect_in_managed_branches(branch)
diff --git a/git_machete/generated_docs.py b/git_machete/generated_docs.py
index ea1a285..7e58566 100644
--- a/git_machete/generated_docs.py
+++ b/git_machete/generated_docs.py
@@ -375,7 +375,7 @@ long_docs: Dict[str, str] = {
    """,
     "diff": """
         <b>Usage:</b><b>
-           git machete d[iff] [-s|--stat] [<branch>]</b>
+           git machete d[iff] [-s|--stat] [<branch>] [-- <pass-through-arguments>]</b>
 
         Runs `git diff` of the given branch tip against its fork point or, if none specified,
         of the current working tree against the fork point of the currently checked out branch.
@@ -384,8 +384,9 @@ long_docs: Dict[str, str] = {
         Note: the branch in question does not need to occur in the branch layout file.
 
         <b>Options:</b>
-           <b>-s</b>, <b>--stat</b>
-              Makes `git machete diff` pass `--stat` option to `git diff`, so that only summary (diffstat) is printed.
+
+        -- <pass-through-arguments>    Arguments to pass directly to the underlying `git diff`, for example `git machete diff -- --name-only`.
+        -s, --stat                     Makes `git machete diff` pass `--stat` option to `git diff`, so that only summary (diffstat) is printed.
    """,
     "discover": """
         <b>Usage:</b><b>
@@ -1020,12 +1021,16 @@ long_docs: Dict[str, str] = {
    """,
     "log": """
         <b>Usage:</b><b>
-           git machete l[og] [<branch>]</b>
+           git machete l[og] [<branch>] [-- <pass-through-arguments>]</b>
 
         Runs `git log` for the range of commits from tip of the given branch (or current branch, if none specified) back to its fork point.
         See help for `fork-point` for more details on meaning of the fork point.
 
         Note: the branch in question does not need to occur in the branch layout file.
+
+        <b>Options:</b>
+
+        -- <pass-through-arguments>    Arguments to pass directly to the underlying `git log`, for example `git machete log -- --patch`.
    """,
     "reapply": """
         <b>Usage:</b><b>
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index fca6108..4b22991 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -1048,8 +1048,9 @@ class GitContext:
 
         return self._popen_git("log", "-1", f"--format={pattern.value}", commit).stdout.strip()
 
-    def display_branch_history_from_fork_point(self, branch: LocalBranchFullName, fork_point: FullCommitHash) -> int:
-        return self._run_git("log", f"^{fork_point}", branch, flush_caches=False)
+    def display_log_between(self, from_inclusive: LocalBranchFullName,
+                            until_exclusive: FullCommitHash, extra_git_log_args: List[str]) -> int:
+        return self._run_git("log", f"^{until_exclusive}", from_inclusive, *extra_git_log_args, flush_caches=False)
 
     def commit_tree_with_given_parent_and_message_and_env(
             self, parent_revision: AnyRevision, msg: str, env: Dict[str, str]) -> FullCommitHash:
@@ -1064,14 +1065,18 @@ class GitContext:
     def delete_remote_branch(self, branch_name: RemoteBranchShortName) -> int:
         return self._run_git('branch', '-d', '-r', branch_name, flush_caches=True)
 
-    def display_diff(self, fork_point: AnyRevision, format_with_stat: bool, branch: Optional[LocalBranchShortName] = None) -> int:
+    def display_diff(self, branch: Optional[LocalBranchShortName], against: AnyRevision,
+                     opt_stat: bool, extra_git_diff_args: List[str]) -> int:
         params = []
-        if format_with_stat:
+        if opt_stat:
             params.append("--stat")
-        params.append(fork_point)
+        params.append(against)
         if branch:
             params.append(branch.full_name())
-        params.append("--")
+        if extra_git_diff_args:
+            params += extra_git_diff_args
+        else:
+            params.append("--")
 
         return self._run_git("diff", *params, flush_caches=False)
 


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 6675267..0763836 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -11,10 +11,7 @@ from .base_test import BaseTest
 
 
 class TestCLI(BaseTest):
-    def test_aliases(self) -> None:
-        """
-        Verify that each command alias is unique
-        """
+    def test_aliases_unique(self) -> None:
         assert len(alias_by_command.values()) == len(set(alias_by_command.values()))
 
     def test_main(self, mocker: MockerFixture) -> None:
@@ -23,6 +20,11 @@ class TestCLI(BaseTest):
             main()
         assert ExitCode.ARGUMENT_ERROR == e.value.code
 
+        with pytest.raises(SystemExit) as e:
+            self.patch_symbol(mocker, "sys.argv", ["", "status", "--", "--patch"])
+            main()
+        assert ExitCode.ARGUMENT_ERROR == e.value.code
+
         os.chdir(mkdtemp())
 
         with pytest.raises(SystemExit) as e:
diff --git a/tests/test_diff.py b/tests/test_diff.py
index d1dc022..2710036 100644
--- a/tests/test_diff.py
+++ b/tests/test_diff.py
@@ -54,7 +54,18 @@ class TestDiff(BaseTest):
         assert_success(["diff", "refs/heads/develop"], expected_status_output)
 
         assert_success(
-            ["diff", "--stat", "refs/heads/develop"],
+            ["diff", "--stat", "refs/heads/develop", "--"],
             "develop_file_name.txt | 1 +\n"
             "1 file changed, 1 insertion(+)\n"
         )
+
+        assert_success(
+            ["diff", "--", "--name-only"],
+            "develop_file_name.txt\n"
+            "file_name.txt\n"
+        )
+
+        assert_success(
+            ["diff", "develop", "--", "--name-only"],
+            "develop_file_name.txt\n"
+        )
diff --git a/tests/test_log.py b/tests/test_log.py
index 726c3bb..2aa56f4 100644
--- a/tests/test_log.py
+++ b/tests/test_log.py
@@ -1,7 +1,8 @@
 from pytest_mock import MockerFixture
 
 from .base_test import BaseTest
-from .mockers import launch_command, mock__run_cmd_and_forward_stdout
+from .mockers import (assert_success, fixed_author_and_committer_date_in_past,
+                      launch_command, mock__run_cmd_and_forward_stdout)
 
 
 class TestLog(BaseTest):
@@ -9,15 +10,16 @@ class TestLog(BaseTest):
     def test_log(self, mocker: MockerFixture) -> None:
         self.patch_symbol(mocker, 'git_machete.utils._run_cmd', mock__run_cmd_and_forward_stdout)
 
-        self.repo_sandbox.new_branch('root')
-        self.repo_sandbox.commit()
-        roots_only_commit_hash = self.repo_sandbox.get_current_commit_hash()
+        with fixed_author_and_committer_date_in_past():
+            self.repo_sandbox.new_branch('root')
+            self.repo_sandbox.commit()
+            roots_only_commit_hash = self.repo_sandbox.get_current_commit_hash()
 
-        self.repo_sandbox.new_branch('child')
-        self.repo_sandbox.commit()
-        child_first_commit_hash = self.repo_sandbox.get_current_commit_hash()
-        self.repo_sandbox.commit()
-        child_second_commit_hash = self.repo_sandbox.get_current_commit_hash()
+            self.repo_sandbox.new_branch('child')
+            self.repo_sandbox.commit()
+            child_first_commit_hash = self.repo_sandbox.get_current_commit_hash()
+            self.repo_sandbox.commit()
+            child_second_commit_hash = self.repo_sandbox.get_current_commit_hash()
 
         log_contents = [launch_command('log'), launch_command('log', 'child'), launch_command('log', 'refs/heads/child')]
 
@@ -30,3 +32,15 @@ class TestLog(BaseTest):
         assert all(roots_only_commit_hash not in log_content for log_content in log_contents), \
             ("Verify that commits from parent branch are not visible when "
              "executing `git machete log`.")
+
+        assert_success(
+            ["log", "--", "--oneline"],
+            "47d565d Some commit message.\n"
+            "dcd2db5 Some commit message.\n"
+        )
+
+        assert_success(
+            ["log", "child", "--", "--oneline"],
+            "47d565d Some commit message.\n"
+            "dcd2db5 Some commit message.\n"
+        )