diff --git a/README.rst b/README.rst index 9c4ee4d..991b31c 100644 --- a/README.rst +++ b/README.rst @@ -80,6 +80,8 @@ format. An example config file with all available options:: [projectmap] projects/foo = new project name ^/home/user/projects/bar(\d+)/ = project{0} + [git] + disable_submodules = false For commonly used configuration options, see examples in the `FAQ `_. diff --git a/tests/samples/configs/git-submodules-disabled-using-regex.cfg b/tests/samples/configs/git-submodules-disabled-using-regex.cfg new file mode 100644 index 0000000..17d8f97 --- /dev/null +++ b/tests/samples/configs/git-submodules-disabled-using-regex.cfg @@ -0,0 +1,8 @@ +[settings] +debug = false +api_key = 1090a6ae-855f-4be7-b8fb-3edbaf1aa3ec +[git] +submodules_disabled = + this/path/does/not/exist + git/asubmodule + that/other/path diff --git a/tests/samples/configs/git-submodules-disabled.cfg b/tests/samples/configs/git-submodules-disabled.cfg new file mode 100644 index 0000000..d802c4c --- /dev/null +++ b/tests/samples/configs/git-submodules-disabled.cfg @@ -0,0 +1,5 @@ +[settings] +debug = false +api_key = 1090a6ae-855f-4be7-b8fb-3edbaf1aa3ec +[git] +submodules_disabled = true diff --git a/tests/samples/configs/git-submodules-enabled-using-regex.cfg b/tests/samples/configs/git-submodules-enabled-using-regex.cfg new file mode 100644 index 0000000..6c64c8d --- /dev/null +++ b/tests/samples/configs/git-submodules-enabled-using-regex.cfg @@ -0,0 +1,7 @@ +[settings] +debug = false +api_key = 1090a6ae-855f-4be7-b8fb-3edbaf1aa3ec +[git] +submodules_disabled = + this/path/does/not/exist + that/other/path diff --git a/tests/samples/configs/git-submodules-enabled.cfg b/tests/samples/configs/git-submodules-enabled.cfg new file mode 100644 index 0000000..e27b531 --- /dev/null +++ b/tests/samples/configs/git-submodules-enabled.cfg @@ -0,0 +1,5 @@ +[settings] +debug = false +api_key = 1090a6ae-855f-4be7-b8fb-3edbaf1aa3ec +[git] +submodules_disabled = false diff --git a/tests/samples/projects/git-with-submodule/asubmodule/dot_git b/tests/samples/projects/git-with-submodule/asubmodule/dot_git new file mode 100644 index 0000000..cda2fde --- /dev/null +++ b/tests/samples/projects/git-with-submodule/asubmodule/dot_git @@ -0,0 +1 @@ +gitdir: ../.git/modules/asubmodule diff --git a/tests/samples/projects/git-with-submodule/asubmodule/emptyfile.txt b/tests/samples/projects/git-with-submodule/asubmodule/emptyfile.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/samples/projects/git-with-submodule/dot_git/HEAD b/tests/samples/projects/git-with-submodule/dot_git/HEAD new file mode 100644 index 0000000..cb089cd --- /dev/null +++ b/tests/samples/projects/git-with-submodule/dot_git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/samples/projects/git-with-submodule/dot_git/config b/tests/samples/projects/git-with-submodule/dot_git/config new file mode 100644 index 0000000..6c9406b --- /dev/null +++ b/tests/samples/projects/git-with-submodule/dot_git/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true diff --git a/tests/samples/projects/git-with-submodule/dot_git/modules/asubmodule/HEAD b/tests/samples/projects/git-with-submodule/dot_git/modules/asubmodule/HEAD new file mode 100644 index 0000000..82b0b29 --- /dev/null +++ b/tests/samples/projects/git-with-submodule/dot_git/modules/asubmodule/HEAD @@ -0,0 +1 @@ +ref: refs/heads/asubbranch diff --git a/tests/samples/projects/git-with-submodule/dot_git/modules/asubmodule/config b/tests/samples/projects/git-with-submodule/dot_git/modules/asubmodule/config new file mode 100644 index 0000000..6c9406b --- /dev/null +++ b/tests/samples/projects/git-with-submodule/dot_git/modules/asubmodule/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true diff --git a/tests/samples/projects/git-with-submodule/emptyfile.txt b/tests/samples/projects/git-with-submodule/emptyfile.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project.py b/tests/test_project.py index b90622e..b5d1b92 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -291,16 +291,20 @@ class ProjectTestCase(utils.TestCase): with utils.mock.patch('wakatime.projects.subversion.Popen') as mock_popen: stdout = open('tests/samples/output/svn').read() stderr = '' + class Dynamic(object): def __init__(self): self.called = 0 + def communicate(self): self.called += 1 if self.called == 2: return (stdout, stderr) + def wait(self): if self.called == 1: return 0 + mock_popen.return_value = Dynamic() execute(args) @@ -347,6 +351,111 @@ class ProjectTestCase(utils.TestCase): self.assertEquals('hg', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) self.assertEquals('default', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['branch']) + def test_git_submodule_detected(self): + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + tempdir = tempfile.mkdtemp() + shutil.copytree('tests/samples/projects/git-with-submodule', os.path.join(tempdir, 'git')) + shutil.move(os.path.join(tempdir, 'git', 'dot_git'), os.path.join(tempdir, 'git', '.git')) + shutil.move(os.path.join(tempdir, 'git', 'asubmodule', 'dot_git'), os.path.join(tempdir, 'git', 'asubmodule', '.git')) + + now = u(int(time.time())) + entity = os.path.join(tempdir, 'git', 'asubmodule', 'emptyfile.txt') + config = 'tests/samples/configs/good_config.cfg' + + args = ['--file', entity, '--config', config, '--time', now] + + execute(args) + + self.assertEquals('asubmodule', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) + self.assertNotIn('asubbranch', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]) + + def test_git_submodule_detected_and_enabled_globally(self): + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + tempdir = tempfile.mkdtemp() + shutil.copytree('tests/samples/projects/git-with-submodule', os.path.join(tempdir, 'git')) + shutil.move(os.path.join(tempdir, 'git', 'dot_git'), os.path.join(tempdir, 'git', '.git')) + shutil.move(os.path.join(tempdir, 'git', 'asubmodule', 'dot_git'), os.path.join(tempdir, 'git', 'asubmodule', '.git')) + + now = u(int(time.time())) + entity = os.path.join(tempdir, 'git', 'asubmodule', 'emptyfile.txt') + config = 'tests/samples/configs/git-submodules-enabled.cfg' + + args = ['--file', entity, '--config', config, '--time', now] + + execute(args) + + self.assertEquals('asubmodule', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) + self.assertNotIn('asubbranch', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]) + + def test_git_submodule_detected_but_disabled_globally(self): + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + tempdir = tempfile.mkdtemp() + shutil.copytree('tests/samples/projects/git-with-submodule', os.path.join(tempdir, 'git')) + shutil.move(os.path.join(tempdir, 'git', 'dot_git'), os.path.join(tempdir, 'git', '.git')) + shutil.move(os.path.join(tempdir, 'git', 'asubmodule', 'dot_git'), os.path.join(tempdir, 'git', 'asubmodule', '.git')) + + now = u(int(time.time())) + entity = os.path.join(tempdir, 'git', 'asubmodule', 'emptyfile.txt') + config = 'tests/samples/configs/git-submodules-disabled.cfg' + + args = ['--file', entity, '--config', config, '--time', now] + + execute(args) + + self.assertEquals('git', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) + self.assertNotIn('master', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]) + + def test_git_submodule_detected_but_disabled_using_regex(self): + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + tempdir = tempfile.mkdtemp() + shutil.copytree('tests/samples/projects/git-with-submodule', os.path.join(tempdir, 'git')) + shutil.move(os.path.join(tempdir, 'git', 'dot_git'), os.path.join(tempdir, 'git', '.git')) + shutil.move(os.path.join(tempdir, 'git', 'asubmodule', 'dot_git'), os.path.join(tempdir, 'git', 'asubmodule', '.git')) + + now = u(int(time.time())) + entity = os.path.join(tempdir, 'git', 'asubmodule', 'emptyfile.txt') + config = 'tests/samples/configs/git-submodules-disabled-using-regex.cfg' + + args = ['--file', entity, '--config', config, '--time', now] + + execute(args) + + self.assertEquals('git', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) + self.assertNotIn('master', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]) + + def test_git_submodule_detected_but_enabled_using_regex(self): + response = Response() + response.status_code = 0 + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response + + tempdir = tempfile.mkdtemp() + shutil.copytree('tests/samples/projects/git-with-submodule', os.path.join(tempdir, 'git')) + shutil.move(os.path.join(tempdir, 'git', 'dot_git'), os.path.join(tempdir, 'git', '.git')) + shutil.move(os.path.join(tempdir, 'git', 'asubmodule', 'dot_git'), os.path.join(tempdir, 'git', 'asubmodule', '.git')) + + now = u(int(time.time())) + entity = os.path.join(tempdir, 'git', 'asubmodule', 'emptyfile.txt') + config = 'tests/samples/configs/git-submodules-enabled-using-regex.cfg' + + args = ['--file', entity, '--config', config, '--time', now] + + execute(args) + + self.assertEquals('asubmodule', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) + self.assertNotIn('asubbranch', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]) + @log_capture() def test_project_map(self, logs): logging.disable(logging.NOTSET) diff --git a/wakatime/projects/git.py b/wakatime/projects/git.py index 7263a4c..5b59a61 100644 --- a/wakatime/projects/git.py +++ b/wakatime/projects/git.py @@ -11,6 +11,7 @@ import logging import os +import re import sys from .base import BaseProject @@ -21,21 +22,19 @@ log = logging.getLogger('WakaTime') class Git(BaseProject): + _submodule = None + _project_name = None + _head_file = None def process(self): - self.configFile = self._find_git_config_file(self.path) - return self.configFile is not None + return self._find_git_config_file(self.path) def name(self): - base = self._project_base() - if base: - return u(os.path.basename(base)) - return None # pragma: nocover + return u(self._project_name) if self._project_name else None def branch(self): - base = self._project_base() - if base: - head = os.path.join(self._project_base(), '.git', 'HEAD') + 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()) @@ -49,23 +48,81 @@ class Git(BaseProject): log.traceback(logging.WARNING) return u('master') - def _project_base(self): - if self.configFile: - return os.path.dirname(os.path.dirname(self.configFile)) - return None # pragma: nocover - def _find_git_config_file(self, path): path = os.path.realpath(path) if os.path.isfile(path): path = os.path.split(path)[0] if os.path.isfile(os.path.join(path, '.git', 'config')): - return os.path.join(path, '.git', 'config') + 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') + return True split_path = os.path.split(path) if split_path[1] == '': - return None + return False return self._find_git_config_file(split_path[0]) def _get_branch_from_head_file(self, line): if u(line.strip()).startswith('ref: '): return u(line.strip().rsplit('/', 1)[-1]) return None + + def _submodules_supported_for_path(self, path): + if not self._configs: + return True + + disabled = self._configs.get('submodules_disabled') + if not disabled: + return True + + if disabled.strip().lower() == 'true': + return False + if disabled.strip().lower() == 'false': + return True + + for pattern in disabled.split("\n"): + if pattern.strip(): + try: + compiled = re.compile(pattern, re.IGNORECASE) + if compiled.search(path): + return False + except re.error as ex: + log.warning(u('Regex error ({msg}) for disable git submodules pattern: {pattern}').format( + msg=u(ex), + pattern=u(pattern), + )) + + return True + + def _find_path_from_submodule(self, path): + link = os.path.join(path, '.git') + if not os.path.isfile(link): + return None + + try: + with open(link, 'r', encoding='utf-8') as fh: + return self._get_path_from_submodule_link(path, fh.readline()) + except UnicodeDecodeError: + try: + with open(link, 'r', encoding=sys.getfilesystemencoding()) as fh: + return self._get_path_from_submodule_link(path, fh.readline()) + 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.join(path, subpath) + + return None