diff --git a/tests/samples/projects/git-worktree/dot_git b/tests/samples/projects/git-worktree/dot_git new file mode 100644 index 0000000..7b35afb --- /dev/null +++ b/tests/samples/projects/git-worktree/dot_git @@ -0,0 +1 @@ +gitdir: ../git/.git/worktrees/git-worktree diff --git a/tests/samples/projects/git-worktree/emptyfile.txt b/tests/samples/projects/git-worktree/emptyfile.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/samples/projects/git/dot_git/worktrees/git-worktree/HEAD b/tests/samples/projects/git/dot_git/worktrees/git-worktree/HEAD new file mode 100644 index 0000000..4fb6052 --- /dev/null +++ b/tests/samples/projects/git/dot_git/worktrees/git-worktree/HEAD @@ -0,0 +1 @@ +ref: refs/heads/worktree-detection-branch diff --git a/tests/samples/projects/git/dot_git/worktrees/git-worktree/ORIG_HEAD b/tests/samples/projects/git/dot_git/worktrees/git-worktree/ORIG_HEAD new file mode 100644 index 0000000..f878fc7 --- /dev/null +++ b/tests/samples/projects/git/dot_git/worktrees/git-worktree/ORIG_HEAD @@ -0,0 +1 @@ +68d846314cd2c2fd51502924a7b644d6cf3d8904 diff --git a/tests/samples/projects/git/dot_git/worktrees/git-worktree/commondir b/tests/samples/projects/git/dot_git/worktrees/git-worktree/commondir new file mode 100644 index 0000000..aab0408 --- /dev/null +++ b/tests/samples/projects/git/dot_git/worktrees/git-worktree/commondir @@ -0,0 +1 @@ +../.. diff --git a/tests/samples/projects/git/dot_git/worktrees/git-worktree/gitdir b/tests/samples/projects/git/dot_git/worktrees/git-worktree/gitdir new file mode 100644 index 0000000..cf79e88 --- /dev/null +++ b/tests/samples/projects/git/dot_git/worktrees/git-worktree/gitdir @@ -0,0 +1 @@ +../../../../git-worktree/.git diff --git a/tests/test_project.py b/tests/test_project.py index 399294a..a006738 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -452,8 +452,23 @@ class ProjectTestCase(TestCase): expected = 'WakaTime WARNING Regex error (unbalanced parenthesis at position 15) for disable git submodules pattern: \\(invalid regex)' self.assertEquals(expected, actual) + def test_git_worktree_detected(self): + tempdir = tempfile.mkdtemp() + shutil.copytree('tests/samples/projects/git-worktree', os.path.join(tempdir, 'git-wt')) + shutil.copytree('tests/samples/projects/git', os.path.join(tempdir, 'git')) + shutil.move(os.path.join(tempdir, 'git-wt', 'dot_git'), os.path.join(tempdir, 'git-wt', '.git')) + shutil.move(os.path.join(tempdir, 'git', 'dot_git'), os.path.join(tempdir, 'git', '.git')) + + entity = os.path.join(tempdir, 'git-wt', 'emptyfile.txt') + + self.shared( + expected_project='git', + expected_branch='worktree-detection-branch', + entity=entity, + ) + @log_capture() - def test_git_find_path_from_submodule(self, logs): + def test_git_path_from_gitdir_link_file(self, logs): logging.disable(logging.NOTSET) tempdir = tempfile.mkdtemp() @@ -464,7 +479,7 @@ class ProjectTestCase(TestCase): path = os.path.join(tempdir, 'git', 'asubmodule') git = Git(None) - result = git._find_path_from_submodule(path) + result = git._path_from_gitdir_link_file(path) expected = os.path.realpath(os.path.join(tempdir, 'git', '.git', 'modules', 'asubmodule')) self.assertEquals(expected, result) @@ -472,7 +487,7 @@ class ProjectTestCase(TestCase): self.assertNothingLogged(logs) @log_capture() - def test_git_find_path_from_submodule_handles_exceptions(self, logs): + def test_git_path_from_gitdir_link_file_handles_exceptions(self, logs): logging.disable(logging.NOTSET) tempdir = tempfile.mkdtemp() @@ -485,7 +500,7 @@ class ProjectTestCase(TestCase): git = Git(None) path = os.path.join(tempdir, 'git', 'asubmodule') - result = git._find_path_from_submodule(path) + result = git._path_from_gitdir_link_file(path) self.assertIsNone(result) self.assertNothingPrinted() @@ -498,7 +513,7 @@ class ProjectTestCase(TestCase): git = Git(None) path = os.path.join(tempdir, 'git', 'asubmodule') - result = git._find_path_from_submodule(path) + result = git._path_from_gitdir_link_file(path) self.assertIsNone(result) self.assertNothingPrinted() diff --git a/wakatime/projects/git.py b/wakatime/projects/git.py index 5435d80..ea9933d 100644 --- a/wakatime/projects/git.py +++ b/wakatime/projects/git.py @@ -35,17 +35,9 @@ class Git(BaseProject): def branch(self): head = self._head_file if head: - try: - with open(head, 'r', encoding='utf-8') as fh: - return self._get_branch_from_head_file(fh.readline()) - except UnicodeDecodeError: # pragma: nocover - try: - with open(head, 'r', encoding=sys.getfilesystemencoding()) as fh: - return self._get_branch_from_head_file(fh.readline()) - except: - log.traceback(logging.WARNING) - except IOError: # pragma: nocover - log.traceback(logging.WARNING) + line = self._first_line_of_file(head) + if line is not None: + return self._get_branch_from_head_file(line) return u('master') def _find_git_config_file(self, path): @@ -56,12 +48,22 @@ class Git(BaseProject): self._project_name = os.path.basename(path) self._head_file = os.path.join(path, '.git', 'HEAD') return True - if self._submodules_supported_for_path(path): - submodule_path = self._find_path_from_submodule(path) - if submodule_path: - self._project_name = os.path.basename(path) - self._head_file = os.path.join(submodule_path, 'HEAD') + + link_path = self._path_from_gitdir_link_file(path) + if link_path: + + # first check if this is a worktree + if self._is_worktree(link_path): + self._project_name = self._project_from_worktree(link_path) + self._head_file = os.path.join(link_path, 'HEAD') return True + + # next check if this is a submodule + if self._submodules_supported_for_path(path): + self._project_name = os.path.basename(path) + self._head_file = os.path.join(link_path, 'HEAD') + return True + split_path = os.path.split(path) if split_path[1] == '': return False @@ -99,30 +101,50 @@ class Git(BaseProject): return True - def _find_path_from_submodule(self, path): + def _is_worktree(self, link_path): + return os.path.basename(os.path.dirname(link_path)) == 'worktrees' + + def _path_from_gitdir_link_file(self, path): link = os.path.join(path, '.git') if not os.path.isfile(link): return None + line = self._first_line_of_file(link) + if line is not None: + return self._path_from_gitdir_string(path, line) + + return None + + def _path_from_gitdir_string(self, path, line): + if line.startswith('gitdir: '): + subpath = line[len('gitdir: '):].strip() + if os.path.isfile(os.path.join(path, subpath, 'HEAD')): + return os.path.realpath(os.path.join(path, subpath)) + + return None + + def _project_from_worktree(self, link_path): + commondir = os.path.join(link_path, 'commondir') + if os.path.isfile(commondir): + line = self._first_line_of_file(commondir) + if line: + gitdir = os.path.abspath(os.path.join(link_path, line)) + if os.path.basename(gitdir) == '.git': + return os.path.basename(os.path.dirname(gitdir)) + + return None + + def _first_line_of_file(self, filepath): try: - with open(link, 'r', encoding='utf-8') as fh: - return self._get_path_from_submodule_link(path, fh.readline()) + with open(filepath, 'r', encoding='utf-8') as fh: + return fh.readline().strip() except UnicodeDecodeError: try: - with open(link, 'r', encoding=sys.getfilesystemencoding()) as fh: - return self._get_path_from_submodule_link(path, fh.readline()) + with open(filepath, 'r', encoding=sys.getfilesystemencoding()) as fh: + return fh.readline().strip() except: log.traceback(logging.WARNING) except IOError: log.traceback(logging.WARNING) return None - - def _get_path_from_submodule_link(self, path, line): - if line.startswith('gitdir: '): - subpath = line[len('gitdir: '):].strip() - if os.path.isfile(os.path.join(path, subpath, 'config')) and \ - os.path.isfile(os.path.join(path, subpath, 'HEAD')): - return os.path.realpath(os.path.join(path, subpath)) - - return None