Repository: timbrel/gitsavvy
Issue: Use correct `gitdir` to support git worktrees

To support multiple *work~~spaces~~trees*  we need to virtualize the location of the actual *gitdir*.  This is to support the work~~spaces~~trees feature of git.  

For example,  when we see `os.path.join(self.repo_path, ".git", "MERGE_MSG")` in our code, we rather want to write it like `self.gitpath("MERGE_MSG")` (at least it's called `rev-parse --git-path MERGE_MSG` iirc in git speak). 

We could expose a property `self.gitdir` as well. 

Changed files:
core/git_command.py, core/commands/commit.py, core/git_mixins/rewrite.py, core/git_mixins/status.py

diff --git a/core/commands/commit.py b/core/commands/commit.py
index 660fa917..78c7c84c 100644
--- a/core/commands/commit.py
+++ b/core/commands/commit.py
@@ -120,7 +120,7 @@ class gs_commit(WindowCommand, GitCommand):
 
     def initialize_view(self, view, amend, initial_text):
         # type: (sublime.View, bool, str) -> None
-        merge_msg_path = os.path.join(self.repo_path, ".git", "MERGE_MSG")
+        merge_msg_path = os.path.join(self.git_dir, "MERGE_MSG")
 
         help_text = (
             COMMIT_HELP_TEXT_ALT
diff --git a/core/git_command.py b/core/git_command.py
index d034d199..9a3daec0 100755
--- a/core/git_command.py
+++ b/core/git_command.py
@@ -36,6 +36,7 @@ if MYPY:
 git_path = None
 error_message_displayed = False
 repo_paths = {}  # type: Dict[str, str]
+git_dirs = {}  # type: Dict[str, str]
 
 DECODE_ERROR_MESSAGE = """
 The Git command returned data that is unparsable.  This may happen
@@ -496,6 +497,7 @@ class _GitCommand(SettingsMixin):
 
     @property
     def repo_path(self):
+        # type: () -> str
         """
         Return the absolute path to the git repo that contains the file that this
         view interacts with.  Like `file_path`, this can be overridden by setting
@@ -510,6 +512,26 @@ class _GitCommand(SettingsMixin):
         else:
             return self.repo_path
 
+    @property
+    def git_dir(self):
+        # type: () -> str
+        repo_path = self.repo_path
+        try:
+            return git_dirs[repo_path]
+        except KeyError:
+            # Note: per contract `{self.repo_path}/.git` exists.
+            gitdir = os.path.join(repo_path, ".git")
+            if os.path.isfile(gitdir):
+                try:
+                    with open(gitdir) as f:
+                        content = f.read()
+                        if content.startswith("gitdir: "):
+                            gitdir = content[8:].strip()
+                except OSError:
+                    pass
+            git_dirs[repo_path] = gitdir
+            return gitdir
+
     @property
     def file_path(self):
         # type: () -> Optional[str]
diff --git a/core/git_mixins/rewrite.py b/core/git_mixins/rewrite.py
index e56638ab..7b017150 100644
--- a/core/git_mixins/rewrite.py
+++ b/core/git_mixins/rewrite.py
@@ -204,10 +204,11 @@ class RewriteMixin(mixin_base):
 
     @property
     def _rebase_replay_dir(self):
+        # type: () -> str
         """
         A directory to store meta data for `rewrite_active_branch`
         """
-        return os.path.join(self.repo_path, ".git", "rebase-replay")
+        return os.path.join(self.git_dir, "rebase-replay")
 
     def rebase_rewritten(self):
         if self.in_rebase_merge():
diff --git a/core/git_mixins/status.py b/core/git_mixins/status.py
index eeb9952d..da1a9144 100644
--- a/core/git_mixins/status.py
+++ b/core/git_mixins/status.py
@@ -265,11 +265,11 @@ class StatusMixin(mixin_base):
 
     @property
     def _rebase_apply_dir(self):
-        return os.path.join(self.repo_path, ".git", "rebase-apply")
+        return os.path.join(self.git_dir, "rebase-apply")
 
     @property
     def _rebase_merge_dir(self):
-        return os.path.join(self.repo_path, ".git", "rebase-merge")
+        return os.path.join(self.git_dir, "rebase-merge")
 
     @property
     def _rebase_dir(self):
@@ -341,7 +341,7 @@ class StatusMixin(mixin_base):
 
     def _read_git_file(self, fname):
         # type: (str) -> str
-        path = os.path.join(self.repo_path, ".git", fname)
+        path = os.path.join(self.git_dir, fname)
         try:
             with open(path, "r") as f:
                 return f.read().strip()
@@ -350,11 +350,11 @@ class StatusMixin(mixin_base):
 
     def in_merge(self):
         # type: () -> bool
-        return os.path.exists(os.path.join(self.repo_path, ".git", "MERGE_HEAD"))
+        return os.path.exists(os.path.join(self.git_dir, "MERGE_HEAD"))
 
     def merge_head(self):
         # type: () -> str
-        path = os.path.join(self.repo_path, ".git", "MERGE_HEAD")
+        path = os.path.join(self.git_dir, "MERGE_HEAD")
         with open(path, "r") as f:
             commit_hash = f.read().strip()
         return self.get_short_hash(commit_hash)