Repository: VirtusLab/git-machete
Pull request #476 (2022-03-26 19:53:55)

Determine whether a rebase/merge/revert/cherry-pick is ongoing on per-worktree basis
![image](https://user-images.githubusercontent.com/3383210/160255119-37f950bd-75bf-436a-beec-ce5b69c08daf.png)

(before that change, git-machete wouldn't see the rebase going on in the worktree)


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 55a95ef..6197b0f 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -4,6 +4,7 @@
 ## New in git-machete 3.8.0
 
 - added: `--all`, `--mine`, `--by` flags and parameter `<PR-number-1> ... <PR-number-N>` to `git machete github checkout-prs`
+- fixed: cherry-pick/merge/rebase/revert is detected on a per-worktree basis
 
 ## New in git-machete 3.7.2
 
diff --git a/docs/source/cli_help/hooks.rst b/docs/source/cli_help/hooks.rst
index d2bcbb0..ed99c23 100644
--- a/docs/source/cli_help/hooks.rst
+++ b/docs/source/cli_help/hooks.rst
@@ -3,6 +3,7 @@
 hooks
 -----
 As with the standard git hooks, git machete looks for its own specific hooks in ``$GIT_DIR/hooks/*`` (or ``$(git config core.hooksPath)/*``, if set).
+All hooks are executed from the top-level folder of the repository (or top-level folder of worktree/submodule, if applicable).
 
 Note: ``hooks`` is not a command as such, just a help topic (there is no ``git machete hooks`` command).
 
diff --git a/git_machete/client.py b/git_machete/client.py
index 0e44ae6..9866045 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -36,7 +36,7 @@ class MacheteClient:
 
     def __init__(self, git: GitContext) -> None:
         self.__git: GitContext = git
-        self._definition_file_path: str = self.__git.get_git_subpath("machete")
+        self._definition_file_path: str = self.__git.get_main_git_subpath("machete")
         self._managed_branches: List[LocalBranchShortName] = []
         self._up_branch: Dict[LocalBranchShortName, Optional[LocalBranchShortName]] = {}  # TODO (#110): default dict with None
         self.__down_branches: Dict[LocalBranchShortName, Optional[List[LocalBranchShortName]]] = {}  # TODO (#110): default dict with []
@@ -2071,7 +2071,7 @@ class MacheteClient:
         if not fork_point:
             raise MacheteException(f"Could not find a fork-point for branch {head}.")
         commits: List[GitLogEntry] = self.__git.get_commits_between(fork_point, head)
-        description_path = self.__git.get_git_subpath('info', 'description')
+        description_path = self.__git.get_main_git_subpath('info', 'description')
         description: str = utils.slurp_file_or_empty(description_path)
 
         ok_str = ' <green><b>OK</b></green>'
@@ -2080,7 +2080,7 @@ class MacheteClient:
                                                     description=description, draft=opt_draft)
         print(fmt(f'{ok_str}, see <b>{pr.html_url}</b>'))
 
-        milestone_path: str = self.__git.get_git_subpath('info', 'milestone')
+        milestone_path: str = self.__git.get_main_git_subpath('info', 'milestone')
         milestone: str = utils.slurp_file_or_empty(milestone_path).strip()
         if milestone:
             print(fmt(f'Setting milestone of PR #{pr.number} to {milestone}...'), end='', flush=True)
@@ -2092,7 +2092,7 @@ class MacheteClient:
             add_assignees_to_pull_request(org, repo, pr.number, [current_user])
             print(fmt(ok_str))
 
-        reviewers_path = self.__git.get_git_subpath('info', 'reviewers')
+        reviewers_path = self.__git.get_main_git_subpath('info', 'reviewers')
         reviewers: List[str] = utils.get_non_empty_lines(utils.slurp_file_or_empty(reviewers_path))
         if reviewers:
             print(
@@ -2102,7 +2102,7 @@ class MacheteClient:
                 add_reviewers_to_pull_request(org, repo, pr.number, reviewers)
             except UnprocessableEntityHTTPError as e:
                 if 'Reviews may only be requested from collaborators.' in e.msg:
-                    warn(f"There are some invalid reviewers in {self.__git.get_git_subpath('info', 'reviewers')} file.\n"
+                    warn(f"There are some invalid reviewers in {self.__git.get_main_git_subpath('info', 'reviewers')} file.\n"
                          "Skipping adding reviewers to pull request.")
                 else:
                     raise e
diff --git a/git_machete/docs.py b/git_machete/docs.py
index 22493c4..fb978da 100644
--- a/git_machete/docs.py
+++ b/git_machete/docs.py
@@ -375,6 +375,7 @@ long_docs: Dict[str, str] = {
     """,
     "hooks": """
         As with the standard git hooks, git-machete looks for its own specific hooks in `$GIT_DIR/hooks/*` (or `$(git config core.hooksPath)/*`, if set).
+        All hooks are executed from the top-level folder of the repository (or top-level folder of worktree/submodule, if applicable).
 
         Note: `hooks` is not a command as such, just a help topic (there is no `git machete hooks` command).
 
diff --git a/git_machete/git_operations.py b/git_machete/git_operations.py
index 01c64b2..4ddc162 100644
--- a/git_machete/git_operations.py
+++ b/git_machete/git_operations.py
@@ -173,7 +173,8 @@ class GitContext:
     def __init__(self) -> None:
         self._git_version: Optional[Tuple[int, ...]] = None
         self._root_dir: Optional[str] = None
-        self._git_dir: Optional[str] = None
+        self._main_git_dir: Optional[str] = None
+        self._worktree_git_dir: Optional[str] = None
         self.__fetch_done_for: Set[str] = set()
         self.__config_cached: Optional[Dict[str, str]] = None
         self.__remotes_cached: Optional[List[str]] = None
@@ -265,23 +266,34 @@ class GitContext:
                 raise MacheteException("Not a git repository")
         return self._root_dir
 
-    def __get_git_dir(self) -> str:
-        if not self._git_dir:
+    def __get_worktree_git_dir(self) -> str:
+        if not self._worktree_git_dir:
+            try:
+                self._worktree_git_dir = self._popen_git("rev-parse", "--git-dir").strip()
+            except MacheteException:
+                raise MacheteException("Not a git repository")
+        return self._worktree_git_dir
+
+    def __get_main_git_dir(self) -> str:
+        if not self._main_git_dir:
             try:
                 git_dir: str = self._popen_git("rev-parse", "--git-dir").strip()
                 git_dir_parts = Path(git_dir).parts
                 if len(git_dir_parts) >= 3 and git_dir_parts[-3] == '.git' and git_dir_parts[-2] == 'worktrees':
-                    self._git_dir = os.path.join(*git_dir_parts[:-2])
+                    self._main_git_dir = os.path.join(*git_dir_parts[:-2])
                     debug(f'git dir pointing to {git_dir} - we are in a worktree; '
-                          f'using {self._git_dir} as the effective git dir instead')
+                          f'using {self._main_git_dir} as the effective git dir instead')
                 else:
-                    self._git_dir = git_dir
+                    self._main_git_dir = git_dir
             except MacheteException:
                 raise MacheteException("Not a git repository")
-        return self._git_dir
+        return self._main_git_dir
+
+    def get_worktree_git_subpath(self, *fragments: str) -> str:
+        return os.path.join(self.__get_worktree_git_dir(), *fragments)
 
-    def get_git_subpath(self, *fragments: str) -> str:
-        return os.path.join(self.__get_git_dir(), *fragments)
+    def get_main_git_subpath(self, *fragments: str) -> str:
+        return os.path.join(self.__get_main_git_dir(), *fragments)
 
     def get_git_timespec_parsed_to_unix_timestamp(self, date: str) -> int:
         try:
@@ -441,18 +453,21 @@ class GitContext:
         # Since many people don't use '--set-upstream' flag of 'push' or 'branch', we try to infer the remote if the tracking data is missing.
         return self.get_strict_counterpart_for_fetching_of_branch(branch) or self.__get_inferred_counterpart_for_fetching_of_branch(branch)
 
+    # Note that rebase/cherry-pick/merge/revert all happen on per-worktree basis,
+    # so we need to check .git/worktrees/<worktree>/<file> rather than .git/<file>
+
     def is_am_in_progress(self) -> bool:
         # As of git 2.24.1, this is how 'cmd_rebase()' in builtin/rebase.c checks whether am is in progress.
-        return os.path.isfile(self.get_git_subpath("rebase-apply", "applying"))
+        return os.path.isfile(self.get_worktree_git_subpath("rebase-apply", "applying"))
 
     def is_cherry_pick_in_progress(self) -> bool:
-        return os.path.isfile(self.get_git_subpath("CHERRY_PICK_HEAD"))
+        return os.path.isfile(self.get_worktree_git_subpath("CHERRY_PICK_HEAD"))
 
     def is_merge_in_progress(self) -> bool:
-        return os.path.isfile(self.get_git_subpath("MERGE_HEAD"))
+        return os.path.isfile(self.get_worktree_git_subpath("MERGE_HEAD"))
 
     def is_revert_in_progress(self) -> bool:
-        return os.path.isfile(self.get_git_subpath("REVERT_HEAD"))
+        return os.path.isfile(self.get_worktree_git_subpath("REVERT_HEAD"))
 
     def checkout(self, branch: LocalBranchShortName) -> None:
         self._run_git("checkout", "--quiet", branch, "--")
@@ -597,12 +612,12 @@ class GitContext:
 
         # .git/rebase-merge directory exists during cherry-pick-powered rebases,
         # e.g. all interactive ones and the ones where '--strategy=' or '--keep-empty' option has been passed
-        rebase_merge_head_name_file = self.get_git_subpath("rebase-merge", "head-name")
+        rebase_merge_head_name_file = self.get_worktree_git_subpath("rebase-merge", "head-name")
         if os.path.isfile(rebase_merge_head_name_file):
             head_name_file = rebase_merge_head_name_file
 
         # .git/rebase-apply directory exists during the remaining, i.e. am-powered rebases, but also during am sessions.
-        rebase_apply_head_name_file = self.get_git_subpath("rebase-apply", "head-name")
+        rebase_apply_head_name_file = self.get_worktree_git_subpath("rebase-apply", "head-name")
         # Most likely .git/rebase-apply/head-name can't exist during am sessions, but it's better to be safe.
         if not self.is_am_in_progress() and os.path.isfile(rebase_apply_head_name_file):
             head_name_file = rebase_apply_head_name_file
@@ -737,7 +752,7 @@ class GitContext:
         ))
 
     def get_hook_path(self, hook_name: str) -> str:
-        hook_dir: str = self.get_config_attr_or_none("core.hooksPath") or self.get_git_subpath("hooks")
+        hook_dir: str = self.get_config_attr_or_none("core.hooksPath") or self.get_main_git_subpath("hooks")
         return os.path.join(hook_dir, hook_name)
 
     def check_hook_executable(self, hook_path: str) -> bool:
@@ -786,7 +801,7 @@ class GitContext:
 
                 # No need to fix <git-dir>/rebase-apply/author-script,
                 # only <git-dir>/rebase-merge/author-script (i.e. interactive rebases, for the most part) is affected.
-                author_script = self.get_git_subpath("rebase-merge", "author-script")
+                author_script = self.get_worktree_git_subpath("rebase-merge", "author-script")
                 if os.path.isfile(author_script):
                     faulty_line_regex = re.compile("[A-Z0-9_]+='[^']*")
 


Test patch:
diff --git a/git_machete/tests/functional/test_machete.py b/git_machete/tests/functional/test_machete.py
index 7718ce8..5f5c487 100644
--- a/git_machete/tests/functional/test_machete.py
+++ b/git_machete/tests/functional/test_machete.py
@@ -380,7 +380,7 @@ class MacheteTester(unittest.TestCase):
 
     @staticmethod
     def rewrite_definition_file(new_body: str) -> None:
-        definition_file_path = git.get_git_subpath("machete")
+        definition_file_path = git.get_main_git_subpath("machete")
         with open(os.path.join(os.getcwd(), definition_file_path), 'w') as def_file:
             def_file.writelines(new_body)