From f98bb024230b677e48accc91404292247873c071 Mon Sep 17 00:00:00 2001 From: Alan Hamlett Date: Thu, 26 Apr 2018 08:39:09 -0700 Subject: [PATCH] upgrade wakatime-cli to v10.2.1 --- packages/wakatime/__about__.py | 2 +- packages/wakatime/arguments.py | 170 ++++++++++++++++++------------ packages/wakatime/heartbeat.py | 29 ++++- packages/wakatime/projects/git.py | 87 +++++++++------ packages/wakatime/utils.py | 10 +- 5 files changed, 191 insertions(+), 107 deletions(-) diff --git a/packages/wakatime/__about__.py b/packages/wakatime/__about__.py index 1e1804f..cfcdd1e 100644 --- a/packages/wakatime/__about__.py +++ b/packages/wakatime/__about__.py @@ -1,7 +1,7 @@ __title__ = 'wakatime' __description__ = 'Common interface to the WakaTime api.' __url__ = 'https://github.com/wakatime/wakatime' -__version_info__ = ('10', '1', '3') +__version_info__ = ('10', '2', '1') __version__ = '.'.join(__version_info__) __author__ = 'Alan Hamlett' __author_email__ = 'alan@wakatime.com' diff --git a/packages/wakatime/arguments.py b/packages/wakatime/arguments.py index a2ff819..0df2d6a 100644 --- a/packages/wakatime/arguments.py +++ b/packages/wakatime/arguments.py @@ -52,100 +52,128 @@ def parse_arguments(): """ # define supported command line arguments - parser = argparse.ArgumentParser( - description='Common interface for the WakaTime api.') + parser = argparse.ArgumentParser(description='Common interface for the ' + + 'WakaTime api.') parser.add_argument('--entity', dest='entity', metavar='FILE', - action=FileAction, - help='absolute path to file for the heartbeat; can also be a '+ - 'url, domain, or app when --entity-type is not file') + action=FileAction, + help='Absolute path to file for the heartbeat. Can ' + + 'also be a url, domain or app when ' + + '--entity-type is not file.') parser.add_argument('--file', dest='file', action=FileAction, - help=argparse.SUPPRESS) + help=argparse.SUPPRESS) parser.add_argument('--key', dest='key', action=StoreWithoutQuotes, - help='your wakatime api key; uses api_key from '+ - '~/.wakatime.cfg by default') - parser.add_argument('--write', dest='is_write', - action='store_true', - help='when set, tells api this heartbeat was triggered from '+ - 'writing to a file') + help='Your wakatime api key; uses api_key from ' + + '~/.wakatime.cfg by default.') + parser.add_argument('--write', dest='is_write', action='store_true', + help='When set, tells api this heartbeat was ' + + 'triggered from writing to a file.') parser.add_argument('--plugin', dest='plugin', action=StoreWithoutQuotes, - help='optional text editor plugin name and version '+ - 'for User-Agent header') + help='Optional text editor plugin name and version ' + + 'for User-Agent header.') parser.add_argument('--time', dest='timestamp', metavar='time', - type=float, action=StoreWithoutQuotes, - help='optional floating-point unix epoch timestamp; '+ - 'uses current time by default') + type=float, action=StoreWithoutQuotes, + help='Optional floating-point unix epoch timestamp. ' + + 'Uses current time by default.') parser.add_argument('--lineno', dest='lineno', action=StoreWithoutQuotes, - help='optional line number; current line being edited') - parser.add_argument('--cursorpos', dest='cursorpos', action=StoreWithoutQuotes, - help='optional cursor position in the current file') - parser.add_argument('--entity-type', dest='entity_type', action=StoreWithoutQuotes, - help='entity type for this heartbeat. can be one of "file", '+ - '"domain", or "app"; defaults to file.') + help='Optional line number. This is the current ' + + 'line being edited.') + parser.add_argument('--cursorpos', dest='cursorpos', + action=StoreWithoutQuotes, + help='Optional cursor position in the current file.') + parser.add_argument('--entity-type', dest='entity_type', + action=StoreWithoutQuotes, + help='Entity type for this heartbeat. Can be ' + + '"file", "domain" or "app". Defaults to "file".') + parser.add_argument('--category', dest='category', + action=StoreWithoutQuotes, + help='Category of this heartbeat activity. Can be ' + + '"coding", "building", "indexing", ' + + '"debugging", "running tests", ' + + '"manual testing", "browsing", ' + + '"code reviewing" or "designing". ' + + 'Defaults to "coding".') parser.add_argument('--proxy', dest='proxy', action=StoreWithoutQuotes, - help='optional proxy configuration. Supports HTTPS '+ - 'and SOCKS proxies. For example: '+ - 'https://user:pass@host:port or '+ - 'socks5://user:pass@host:port or ' + - 'domain\\user:pass') + help='Optional proxy configuration. Supports HTTPS '+ + 'and SOCKS proxies. For example: '+ + 'https://user:pass@host:port or '+ + 'socks5://user:pass@host:port or ' + + 'domain\\user:pass') parser.add_argument('--no-ssl-verify', dest='nosslverify', action='store_true', - help='disables SSL certificate verification for HTTPS '+ - 'requests. By default, SSL certificates are verified.') + help='Disables SSL certificate verification for HTTPS '+ + 'requests. By default, SSL certificates are ' + + 'verified.') parser.add_argument('--project', dest='project', action=StoreWithoutQuotes, - help='optional project name') - parser.add_argument('--alternate-project', dest='alternate_project', action=StoreWithoutQuotes, - help='optional alternate project name; auto-discovered project '+ - 'takes priority') - parser.add_argument('--alternate-language', dest='alternate_language', action=StoreWithoutQuotes, - help=argparse.SUPPRESS) - parser.add_argument('--language', dest='language', action=StoreWithoutQuotes, - help='optional language name; if valid, takes priority over '+ - 'auto-detected language') - parser.add_argument('--hostname', dest='hostname', action=StoreWithoutQuotes, help='hostname of '+ - 'current machine.') + help='Optional project name.') + parser.add_argument('--alternate-project', dest='alternate_project', + action=StoreWithoutQuotes, + help='Optional alternate project name. ' + + 'Auto-discovered project takes priority.') + parser.add_argument('--alternate-language', dest='alternate_language', + action=StoreWithoutQuotes, + help=argparse.SUPPRESS) + parser.add_argument('--language', dest='language', + action=StoreWithoutQuotes, + help='Optional language name. If valid, takes ' + + 'priority over auto-detected language.') + parser.add_argument('--hostname', dest='hostname', + action=StoreWithoutQuotes, + help='Hostname of current machine.') parser.add_argument('--disable-offline', dest='offline', - action='store_false', - help='disables offline time logging instead of queuing logged time') + action='store_false', + help='Disables offline time logging instead of ' + + 'queuing logged time.') parser.add_argument('--disableoffline', dest='offline_deprecated', - action='store_true', help=argparse.SUPPRESS) + action='store_true', + help=argparse.SUPPRESS) parser.add_argument('--hide-filenames', dest='hide_filenames', - action='store_true', - help='obfuscate filenames; will not send file names to api') + action='store_true', + help='Obfuscate filenames. Will not send file names ' + + 'to api.') parser.add_argument('--hidefilenames', dest='hidefilenames', - action='store_true', - help=argparse.SUPPRESS) + action='store_true', + help=argparse.SUPPRESS) parser.add_argument('--exclude', dest='exclude', action='append', - help='filename patterns to exclude from logging; POSIX regex '+ - 'syntax; can be used more than once') + help='Filename patterns to exclude from logging. ' + + 'POSIX regex syntax. Can be used more than once.') + parser.add_argument('--exclude-unknown-project', + dest='exclude_unknown_project', action='store_true', + help='When set, any activity where the project ' + + 'cannot be detected will be ignored.') parser.add_argument('--include', dest='include', action='append', - help='filename patterns to log; when used in combination with '+ - '--exclude, files matching include will still be logged; '+ - 'POSIX regex syntax; can be used more than once') + help='Filename patterns to log. When used in ' + + 'combination with --exclude, files matching ' + + 'include will still be logged. POSIX regex ' + + 'syntax. Can be used more than once.') parser.add_argument('--include-only-with-project-file', dest='include_only_with_project_file', action='store_true', - help='disables tracking folders unless they contain '+ - 'a .wakatime-project file; defaults to false') + help='Disables tracking folders unless they contain ' + + 'a .wakatime-project file. Defaults to false.') parser.add_argument('--ignore', dest='ignore', action='append', - help=argparse.SUPPRESS) + help=argparse.SUPPRESS) parser.add_argument('--extra-heartbeats', dest='extra_heartbeats', - action='store_true', - help='reads extra heartbeats from STDIN as a JSON array until EOF') - parser.add_argument('--log-file', dest='log_file', action=StoreWithoutQuotes, - help='defaults to ~/.wakatime.log') + action='store_true', + help='Reads extra heartbeats from STDIN as a JSON ' + + 'array until EOF.') + parser.add_argument('--log-file', dest='log_file', + action=StoreWithoutQuotes, + help='Defaults to ~/.wakatime.log.') parser.add_argument('--logfile', dest='logfile', action=StoreWithoutQuotes, - help=argparse.SUPPRESS) + help=argparse.SUPPRESS) parser.add_argument('--api-url', dest='api_url', action=StoreWithoutQuotes, - help='heartbeats api url; for debugging with a local server') + help='Heartbeats api url. For debugging with a ' + + 'local server.') parser.add_argument('--apiurl', dest='apiurl', action=StoreWithoutQuotes, - help=argparse.SUPPRESS) - parser.add_argument('--timeout', dest='timeout', type=int, action=StoreWithoutQuotes, - help='number of seconds to wait when sending heartbeats to api; '+ - 'defaults to 60 seconds') + help=argparse.SUPPRESS) + parser.add_argument('--timeout', dest='timeout', type=int, + action=StoreWithoutQuotes, + help='Number of seconds to wait when sending ' + + 'heartbeats to api. Defaults to 60 seconds.') parser.add_argument('--config', dest='config', action=StoreWithoutQuotes, - help='defaults to ~/.wakatime.cfg') + help='Defaults to ~/.wakatime.cfg.') parser.add_argument('--verbose', dest='verbose', action='store_true', - help='turns on debug messages in log file') + help='Turns on debug messages in log file.') parser.add_argument('--version', action='version', version=__version__) # parse command line arguments @@ -172,14 +200,14 @@ def parse_arguments(): args.key = default_key else: try: - parser.error('Missing api key. Find your api key from wakatime.com/settings.') + parser.error('Missing api key. Find your api key from wakatime.com/settings/api-key.') except SystemExit: raise SystemExit(AUTH_ERROR) is_valid = not not re.match(r'^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$', args.key, re.I) if not is_valid: try: - parser.error('Invalid api key. Find your api key from wakatime.com/settings.') + parser.error('Invalid api key. Find your api key from wakatime.com/settings/api-key.') except SystemExit: raise SystemExit(AUTH_ERROR) @@ -219,6 +247,8 @@ def parse_arguments(): args.include.append(pattern) except TypeError: # pragma: nocover pass + if not args.exclude_unknown_project and configs.has_option('settings', 'exclude_unknown_project'): + args.exclude_unknown_project = configs.getboolean('settings', 'exclude_unknown_project') if not args.hide_filenames and args.hidefilenames: args.hide_filenames = args.hidefilenames if args.hide_filenames: diff --git a/packages/wakatime/heartbeat.py b/packages/wakatime/heartbeat.py index 757838e..252ff66 100644 --- a/packages/wakatime/heartbeat.py +++ b/packages/wakatime/heartbeat.py @@ -31,6 +31,7 @@ class Heartbeat(object): time = None entity = None type = None + category = None is_write = None project = None branch = None @@ -58,6 +59,21 @@ class Heartbeat(object): if self.type not in ['file', 'domain', 'app']: self.type = 'file' + self.category = data.get('category') + allowed_categories = [ + 'coding', + 'building', + 'indexing', + 'debugging', + 'running tests', + 'manual testing', + 'browsing', + 'code reviewing', + 'designing', + ] + if self.category not in allowed_categories: + self.category = None + if not _clone: exclude = self._excluded_by_pattern() if exclude: @@ -78,6 +94,10 @@ class Heartbeat(object): self.project = project self.branch = branch + if self._excluded_by_unknown_project(): + self.skip = u('Skipping because project unknown.') + return + try: stats = get_file_stats(self.entity, entity_type=self.type, @@ -155,6 +175,7 @@ class Heartbeat(object): 'time': self.time, 'entity': self._unicode(self.entity), 'type': self.type, + 'category': self.category, 'is_write': self.is_write, 'project': self._unicode(self.project), 'branch': self._unicode(self.branch), @@ -170,9 +191,10 @@ class Heartbeat(object): return self.dict().items() def get_id(self): - return u('{time}-{type}-{project}-{branch}-{entity}-{is_write}').format( + return u('{time}-{type}-{category}-{project}-{branch}-{entity}-{is_write}').format( time=self.time, type=self.type, + category=self.category, project=self._unicode(self.project), branch=self._unicode(self.branch), entity=self._unicode(self.entity), @@ -192,6 +214,11 @@ class Heartbeat(object): def _excluded_by_pattern(self): return should_exclude(self.entity, self.args.include, self.args.exclude) + def _excluded_by_unknown_project(self): + if self.project: + return False + return self.args.exclude_unknown_project + def _excluded_by_missing_project_file(self): if not self.args.include_only_with_project_file: return False diff --git a/packages/wakatime/projects/git.py b/packages/wakatime/projects/git.py index 5435d80..9acdc78 100644 --- a/packages/wakatime/projects/git.py +++ b/packages/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 @@ -77,8 +79,6 @@ class Git(BaseProject): return True disabled = self._configs.get('submodules_disabled') - if not disabled: - return True if disabled.strip().lower() == 'true': return False @@ -99,30 +99,51 @@ 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 - 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) + line = self._first_line_of_file(link) + if line is not None: + return self._path_from_gitdir_string(path, line) return None - def _get_path_from_submodule_link(self, path, line): + 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, 'config')) and \ - os.path.isfile(os.path.join(path, subpath, 'HEAD')): + 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(filepath, 'r', encoding='utf-8') as fh: + return fh.readline().strip() + except UnicodeDecodeError: + pass + except IOError: + pass + try: + with open(filepath, 'r', encoding=sys.getfilesystemencoding()) as fh: + return fh.readline().strip() + except: + log.traceback(logging.WARNING) + + return None diff --git a/packages/wakatime/utils.py b/packages/wakatime/utils.py index d7a3bc6..9e04ecf 100644 --- a/packages/wakatime/utils.py +++ b/packages/wakatime/utils.py @@ -24,6 +24,10 @@ from .compat import u log = logging.getLogger('WakaTime') +BACKSLASH_REPLACE_PATTERN = re.compile(r'[\\/]+') +WINDOWS_DRIVE_PATTERN = re.compile(r'^[a-z]:/') + + def should_exclude(entity, include, exclude): if entity is not None and entity.strip() != '': for pattern in include: @@ -74,8 +78,10 @@ def format_file_path(filepath): try: filepath = os.path.realpath(os.path.abspath(filepath)) - filepath = re.sub(r'[/\\]', os.path.sep, filepath) - except: # pragma: nocover + filepath = re.sub(BACKSLASH_REPLACE_PATTERN, '/', filepath) + if WINDOWS_DRIVE_PATTERN.match(filepath): + filepath = filepath.capitalize() + except: pass return filepath