Compare commits

..

25 Commits
8.1.1 ... 8.3.4

Author SHA1 Message Date
f0532f5b8e v8.3.4 2019-03-30 19:17:06 -07:00
8094db9680 changes for v8.3.4 2019-03-30 19:16:49 -07:00
bf20551849 upgrade wakatime-cli to v10.8.2 2019-03-30 18:53:40 -07:00
2b6e32b578 remove redundant line 2019-03-13 09:36:51 +09:00
363c3d38e2 update license file with copyright owner inline 2019-03-13 09:20:50 +09:00
88466d7db2 v8.3.3 2018-12-19 07:43:01 -08:00
122fcbbee5 changes for v8.3.3 2018-12-19 07:42:30 -08:00
c41fcec5d8 upgrade wakatime-cli to v10.6.1 2018-12-19 07:38:18 -08:00
be09b34d44 v8.3.2 2018-10-06 20:40:57 -07:00
e1ee1c1216 changes for v8.3.2 2018-10-06 20:40:37 -07:00
a37061924b Send buffered heartbeats to API every 30 seconds. 2018-10-06 20:40:12 -07:00
da01fa268b v8.3.1 2018-10-05 00:07:11 -07:00
c279418651 changes for v8.3.1 2018-10-05 00:06:46 -07:00
5cf2c8f7ac upgrade wakatime-cli to v10.4.1 2018-10-05 00:06:02 -07:00
d1455e77a8 v8.3.0 2018-10-03 00:55:02 -07:00
8499e7bafe changes for v8.3.0 2018-10-03 00:54:40 -07:00
abc26a0864 upgrade wakatime-cli to v10.4.0 2018-10-03 00:47:20 -07:00
71ad97ffe9 v8.2.0 2018-09-30 22:03:01 -07:00
3ec5995c99 changes for v8.2.0 2018-09-30 22:02:53 -07:00
195cf4de36 upgrade wakatime-cli to v10.3.0 2018-09-30 21:48:20 -07:00
b39eefb4f5 cross-platform Popen with hidden window 2018-09-30 21:29:04 -07:00
bbf5761e26 v8.1.2 2018-09-20 22:31:14 -07:00
c4df1dc633 changes for v8.1.2 2018-09-20 22:30:57 -07:00
360a491cda upgrade wakatime-cli to v10.2.4 2018-09-20 22:29:34 -07:00
f61a34eda7 build subprocess stdin near heartbeats 2018-09-13 19:39:05 -07:00
20 changed files with 614 additions and 204 deletions

View File

@ -3,6 +3,86 @@ History
------- -------
8.3.4 (2019-03-30)
++++++++++++++++++
- Upgrade wakatime-cli to v10.8.2.
- Detect go.mod files as Go language.
`jetbrains-wakatime#119 <https://github.com/wakatime/jetbrains-wakatime/issues/119>`_
- Detect C++ language from all C++ file extensions.
`vscode-wakatime#87 <https://github.com/wakatime/vscode-wakatime/issues/87>`_
- Add ssl_certs_file arg and config for custom ca bundles.
`#164 <https://github.com/wakatime/wakatime/issues/164>`_
- Fix bug causing random project names when hide project names enabled.
`vscode-wakatime#162 <https://github.com/wakatime/vscode-wakatime/issues/61>`_
- Add support for UNC network shares without drive letter mapped on Winows.
`#162 <https://github.com/wakatime/wakatime/issues/162>`_
8.3.3 (2018-12-19)
++++++++++++++++++
- Upgrade wakatime-cli to v10.6.1.
- Correctly parse include_only_with_project_file when set to false.
`wakatime#161 <https://github.com/wakatime/wakatime/issues/161>`_
- Support language argument for non-file entity types.
- Send 25 heartbeats per API request.
- New category "Writing Tests".
`wakatime#156 <https://github.com/wakatime/wakatime/issues/156>`_
- Fix bug caused by git config section without any submodule option defined.
`wakatime#152 <https://github.com/wakatime/wakatime/issues/152>`_
8.3.2 (2018-10-06)
++++++++++++++++++
- Send buffered heartbeats to API every 30 seconds.
8.3.1 (2018-10-05)
++++++++++++++++++
- Upgrade wakatime-cli to v10.4.1.
- Send 50 offline heartbeats to API per request with 1 second delay in between.
8.3.0 (2018-10-03)
++++++++++++++++++
- Upgrade wakatime-cli to v10.4.0.
- Support logging coding activity to remote network drive files on Windows
platform by detecting UNC path from drive letter.
`wakatime#72 <https://github.com/wakatime/wakatime/issues/72>`_
8.2.0 (2018-09-30)
++++++++++++++++++
- Prevent opening cmd window on Windows when running wakatime-cli.
`#91 <https://github.com/wakatime/sublime-wakatime/issues/91>`_
- Upgrade wakatime-cli to v10.3.0.
- Re-enable detecting projects from Subversion folder on Windows platform.
- Prevent opening cmd window on Windows when detecting project from Subversion.
- Run tests on Windows using Appveyor.
8.1.2 (2018-09-20)
++++++++++++++++++
- Upgrade wakatime-cli to v10.2.4.
- Default --sync-offline-activity to 100 instead of 5, so offline coding is
synced to dashboard faster.
- Batch heartbeats in groups of 10 per api request.
- New config hide_project_name and argument --hide-project-names for
obfuscating project names when sending coding activity to api.
- Fix mispelled Gosu language.
`wakatime#137 <https://github.com/wakatime/wakatime/issues/137>`_
- Remove metadata when hiding project or file names.
- New --local-file argument to be used when --entity is a remote file.
- New argument --sync-offline-activity for configuring the maximum offline
heartbeats to sync to the WakaTime API.
8.1.1 (2018-04-26) 8.1.1 (2018-04-26)
++++++++++++++++++ ++++++++++++++++++
@ -33,10 +113,10 @@ History
- Upgrade wakatime-cli to v10.1.2. - Upgrade wakatime-cli to v10.1.2.
- Detect dependencies from Swift, Objective-C, TypeScript and JavaScript files. - Detect dependencies from Swift, Objective-C, TypeScript and JavaScript files.
- Categorize .mjs files as JavaScript. - Categorize .mjs files as JavaScript.
`#wakatime121 <https://github.com/wakatime/wakatime/issues/121>`_ `wakatime#121 <https://github.com/wakatime/wakatime/issues/121>`_
- Detect dependencies from Elm, Haskell, Haxe, Kotlin, Rust, and Scala files. - Detect dependencies from Elm, Haskell, Haxe, Kotlin, Rust, and Scala files.
- Improved Matlab vs Objective-C language detection. - Improved Matlab vs Objective-C language detection.
`#wakatime129 <https://github.com/wakatime/wakatime/issues/129>`_ `wakatime#129 <https://github.com/wakatime/wakatime/issues/129>`_
8.0.6 (2018-01-04) 8.0.6 (2018-01-04)

View File

@ -1,7 +1,6 @@
BSD 3-Clause License BSD 3-Clause License
Copyright (c) 2014 by the respective authors (see AUTHORS file). Copyright (c) 2014 Alan Hamlett.
All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

View File

@ -7,7 +7,7 @@ Website: https://wakatime.com/
===========================================================""" ==========================================================="""
__version__ = '8.1.1' __version__ = '8.3.4'
import sublime import sublime
@ -18,6 +18,7 @@ import json
import os import os
import platform import platform
import re import re
import subprocess
import sys import sys
import time import time
import threading import threading
@ -25,7 +26,7 @@ import traceback
import urllib import urllib
import webbrowser import webbrowser
from datetime import datetime from datetime import datetime
from subprocess import Popen, STDOUT, PIPE from subprocess import STDOUT, PIPE
from zipfile import ZipFile from zipfile import ZipFile
try: try:
import _winreg as winreg # py2 import _winreg as winreg # py2
@ -42,6 +43,8 @@ except ImportError:
is_py2 = (sys.version_info[0] == 2) is_py2 = (sys.version_info[0] == 2)
is_py3 = (sys.version_info[0] == 3) is_py3 = (sys.version_info[0] == 3)
is_win = platform.system() == 'Windows'
if is_py2: if is_py2:
def u(text): def u(text):
@ -91,8 +94,22 @@ else:
)) ))
class Popen(subprocess.Popen):
"""Patched Popen to prevent opening cmd window on Windows platform."""
def __init__(self, *args, **kwargs):
startupinfo = kwargs.get('startupinfo')
if is_win or True:
try:
startupinfo = startupinfo or subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
except AttributeError:
pass
kwargs['startupinfo'] = startupinfo
super(Popen, self).__init__(*args, **kwargs)
# globals # globals
HEARTBEAT_FREQUENCY = 2
ST_VERSION = int(sublime.version()) ST_VERSION = int(sublime.version())
PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__)) PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py') API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
@ -103,8 +120,11 @@ LAST_HEARTBEAT = {
'file': None, 'file': None,
'is_write': False, 'is_write': False,
} }
LAST_HEARTBEAT_SENT_AT = 0
PYTHON_LOCATION = None PYTHON_LOCATION = None
HEARTBEATS = queue.Queue() HEARTBEATS = queue.Queue()
HEARTBEAT_FREQUENCY = 2 # minutes between logging heartbeat when editing same file
SEND_BUFFER_SECONDS = 30 # seconds between sending buffered heartbeats to API
# Log Levels # Log Levels
@ -433,11 +453,18 @@ def append_heartbeat(entity, timestamp, is_write, view, project, folders):
} }
# process the queue of heartbeats in the future # process the queue of heartbeats in the future
seconds = 4 set_timeout(lambda: process_queue(timestamp), SEND_BUFFER_SECONDS)
set_timeout(process_queue, seconds)
def process_queue(): def process_queue(timestamp):
global LAST_HEARTBEAT_SENT_AT
# Prevent sending heartbeats more often than SEND_BUFFER_SECONDS
now = int(time.time())
if timestamp != LAST_HEARTBEAT['time'] and LAST_HEARTBEAT_SENT_AT > now - SEND_BUFFER_SECONDS:
return
LAST_HEARTBEAT_SENT_AT = now
try: try:
heartbeat = HEARTBEATS.get_nowait() heartbeat = HEARTBEATS.get_nowait()
except queue.Empty: except queue.Empty:
@ -542,19 +569,16 @@ class SendHeartbeatsThread(threading.Thread):
if self.has_extra_heartbeats: if self.has_extra_heartbeats:
cmd.append('--extra-heartbeats') cmd.append('--extra-heartbeats')
stdin = PIPE stdin = PIPE
extra_heartbeats = [self.build_heartbeat(**x) for x in self.extra_heartbeats] extra_heartbeats = json.dumps([self.build_heartbeat(**x) for x in self.extra_heartbeats])
extra_heartbeats = json.dumps(extra_heartbeats) inp = "{0}\n".format(extra_heartbeats).encode('utf-8')
else: else:
extra_heartbeats = None extra_heartbeats = None
stdin = None stdin = None
inp = None
log(DEBUG, ' '.join(obfuscate_apikey(cmd))) log(DEBUG, ' '.join(obfuscate_apikey(cmd)))
try: try:
process = Popen(cmd, stdin=stdin, stdout=PIPE, stderr=STDOUT) process = Popen(cmd, stdin=stdin, stdout=PIPE, stderr=STDOUT)
inp = None
if self.has_extra_heartbeats:
inp = "{0}\n".format(extra_heartbeats)
inp = inp.encode('utf-8')
output, err = process.communicate(input=inp) output, err = process.communicate(input=inp)
output = u(output) output = u(output)
retcode = process.poll() retcode = process.poll()

View File

@ -1,7 +1,7 @@
__title__ = 'wakatime' __title__ = 'wakatime'
__description__ = 'Common interface to the WakaTime api.' __description__ = 'Common interface to the WakaTime api.'
__url__ = 'https://github.com/wakatime/wakatime' __url__ = 'https://github.com/wakatime/wakatime'
__version_info__ = ('10', '2', '1') __version_info__ = ('10', '8', '2')
__version__ = '.'.join(__version_info__) __version__ = '.'.join(__version_info__)
__author__ = 'Alan Hamlett' __author__ = 'Alan Hamlett'
__author_email__ = 'alan@wakatime.com' __author_email__ = 'alan@wakatime.com'

View File

@ -99,12 +99,16 @@ def send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=False):
should_try_ntlm = '\\' in args.proxy should_try_ntlm = '\\' in args.proxy
proxies['https'] = args.proxy proxies['https'] = args.proxy
ssl_verify = not args.nosslverify
if args.ssl_certs_file and ssl_verify:
ssl_verify = args.ssl_certs_file
# send request to api # send request to api
response, code = None, None response, code = None, None
try: try:
response = session.post(api_url, data=request_body, headers=headers, response = session.post(api_url, data=request_body, headers=headers,
proxies=proxies, timeout=timeout, proxies=proxies, timeout=timeout,
verify=not args.nosslverify) verify=ssl_verify)
except RequestException: except RequestException:
if should_try_ntlm: if should_try_ntlm:
return send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=True) return send_heartbeats(heartbeats, args, configs, use_ntlm_proxy=True)

View File

@ -20,7 +20,7 @@ import traceback
from .__about__ import __version__ from .__about__ import __version__
from .compat import basestring from .compat import basestring
from .configs import parseConfigFile from .configs import parseConfigFile
from .constants import AUTH_ERROR from .constants import AUTH_ERROR, DEFAULT_SYNC_OFFLINE_ACTIVITY
from .packages import argparse from .packages import argparse
@ -89,8 +89,8 @@ def parse_arguments():
help='Category of this heartbeat activity. Can be ' + help='Category of this heartbeat activity. Can be ' +
'"coding", "building", "indexing", ' + '"coding", "building", "indexing", ' +
'"debugging", "running tests", ' + '"debugging", "running tests", ' +
'"manual testing", "browsing", ' + '"writing tests", "manual testing", ' +
'"code reviewing" or "designing". ' + '"code reviewing", "browsing", or "designing". ' +
'Defaults to "coding".') 'Defaults to "coding".')
parser.add_argument('--proxy', dest='proxy', action=StoreWithoutQuotes, parser.add_argument('--proxy', dest='proxy', action=StoreWithoutQuotes,
help='Optional proxy configuration. Supports HTTPS '+ help='Optional proxy configuration. Supports HTTPS '+
@ -103,6 +103,10 @@ def parse_arguments():
help='Disables SSL certificate verification for HTTPS '+ help='Disables SSL certificate verification for HTTPS '+
'requests. By default, SSL certificates are ' + 'requests. By default, SSL certificates are ' +
'verified.') 'verified.')
parser.add_argument('--ssl-certs-file', dest='ssl_certs_file',
action=StoreWithoutQuotes,
help='Override the bundled Python Requests CA certs ' +
'file. By default, uses certifi for ca certs.')
parser.add_argument('--project', dest='project', action=StoreWithoutQuotes, parser.add_argument('--project', dest='project', action=StoreWithoutQuotes,
help='Optional project name.') help='Optional project name.')
parser.add_argument('--alternate-project', dest='alternate_project', parser.add_argument('--alternate-project', dest='alternate_project',
@ -116,6 +120,12 @@ def parse_arguments():
action=StoreWithoutQuotes, action=StoreWithoutQuotes,
help='Optional language name. If valid, takes ' + help='Optional language name. If valid, takes ' +
'priority over auto-detected language.') 'priority over auto-detected language.')
parser.add_argument('--local-file', dest='local_file', metavar='FILE',
action=FileAction,
help='Absolute path to local file for the ' +
'heartbeat. When --entity is a remote file, ' +
'this local file will be used for stats and ' +
'just the value of --entity sent with heartbeat.')
parser.add_argument('--hostname', dest='hostname', parser.add_argument('--hostname', dest='hostname',
action=StoreWithoutQuotes, action=StoreWithoutQuotes,
help='Hostname of current machine.') help='Hostname of current machine.')
@ -126,13 +136,23 @@ def parse_arguments():
parser.add_argument('--disableoffline', dest='offline_deprecated', parser.add_argument('--disableoffline', dest='offline_deprecated',
action='store_true', action='store_true',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument('--hide-filenames', dest='hide_filenames', parser.add_argument('--hide-file-names', dest='hide_file_names',
action='store_true', action='store_true',
help='Obfuscate filenames. Will not send file names ' + help='Obfuscate filenames. Will not send file names ' +
'to api.') 'to api.')
parser.add_argument('--hide-filenames', dest='hide_filenames',
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--hidefilenames', dest='hidefilenames', parser.add_argument('--hidefilenames', dest='hidefilenames',
action='store_true', action='store_true',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument('--hide-project-names', dest='hide_project_names',
action='store_true',
help='Obfuscate project names. When a project ' +
'folder is detected instead of using the ' +
'folder name as the project, a ' +
'.wakatime-project file is created with a ' +
'random project name.')
parser.add_argument('--exclude', dest='exclude', action='append', parser.add_argument('--exclude', dest='exclude', action='append',
help='Filename patterns to exclude from logging. ' + help='Filename patterns to exclude from logging. ' +
'POSIX regex syntax. Can be used more than once.') 'POSIX regex syntax. Can be used more than once.')
@ -170,6 +190,17 @@ def parse_arguments():
action=StoreWithoutQuotes, action=StoreWithoutQuotes,
help='Number of seconds to wait when sending ' + help='Number of seconds to wait when sending ' +
'heartbeats to api. Defaults to 60 seconds.') 'heartbeats to api. Defaults to 60 seconds.')
parser.add_argument('--sync-offline-activity',
dest='sync_offline_activity',
action=StoreWithoutQuotes,
help='Amount of offline activity to sync from your ' +
'local ~/.wakatime.db sqlite3 file to your ' +
'WakaTime Dashboard before exiting. Can be ' +
'"none" or a positive integer number. Defaults ' +
'to 5, meaning for every heartbeat sent while ' +
'online 5 offline heartbeats are synced. Can ' +
'be used without --entity to only sync offline ' +
'activity without generating new heartbeats.')
parser.add_argument('--config', dest='config', action=StoreWithoutQuotes, 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', parser.add_argument('--verbose', dest='verbose', action='store_true',
@ -214,9 +245,20 @@ def parse_arguments():
if not args.entity: if not args.entity:
if args.file: if args.file:
args.entity = args.file args.entity = args.file
else: elif not args.sync_offline_activity or args.sync_offline_activity == 'none':
parser.error('argument --entity is required') parser.error('argument --entity is required')
if not args.sync_offline_activity:
args.sync_offline_activity = DEFAULT_SYNC_OFFLINE_ACTIVITY
if args.sync_offline_activity == 'none':
args.sync_offline_activity = 0
try:
args.sync_offline_activity = int(args.sync_offline_activity)
if args.sync_offline_activity < 0:
raise Exception('Error')
except:
parser.error('argument --sync-offline-activity must be "none" or an integer number')
if not args.language and args.alternate_language: if not args.language and args.alternate_language:
args.language = args.alternate_language args.language = args.alternate_language
@ -237,7 +279,7 @@ def parse_arguments():
except TypeError: # pragma: nocover except TypeError: # pragma: nocover
pass pass
if not args.include_only_with_project_file and configs.has_option('settings', 'include_only_with_project_file'): if not args.include_only_with_project_file and configs.has_option('settings', 'include_only_with_project_file'):
args.include_only_with_project_file = configs.get('settings', 'include_only_with_project_file') args.include_only_with_project_file = configs.get('settings', 'include_only_with_project_file') == 'true'
if not args.include: if not args.include:
args.include = [] args.include = []
if configs.has_option('settings', 'include'): if configs.has_option('settings', 'include'):
@ -249,24 +291,8 @@ def parse_arguments():
pass pass
if not args.exclude_unknown_project and configs.has_option('settings', 'exclude_unknown_project'): if not args.exclude_unknown_project and configs.has_option('settings', 'exclude_unknown_project'):
args.exclude_unknown_project = configs.getboolean('settings', 'exclude_unknown_project') args.exclude_unknown_project = configs.getboolean('settings', 'exclude_unknown_project')
if not args.hide_filenames and args.hidefilenames: boolean_or_list('hide_file_names', args, configs, alternative_names=['hide_filenames', 'hidefilenames'])
args.hide_filenames = args.hidefilenames boolean_or_list('hide_project_names', args, configs, alternative_names=['hide_projectnames', 'hideprojectnames'])
if args.hide_filenames:
args.hide_filenames = ['.*']
else:
args.hide_filenames = []
option = None
if configs.has_option('settings', 'hidefilenames'):
option = configs.get('settings', 'hidefilenames')
if configs.has_option('settings', 'hide_filenames'):
option = configs.get('settings', 'hide_filenames')
if option is not None:
if option.strip().lower() == 'true':
args.hide_filenames = ['.*']
elif option.strip().lower() != 'false':
for pattern in option.split("\n"):
if pattern.strip() != '':
args.hide_filenames.append(pattern)
if args.offline_deprecated: if args.offline_deprecated:
args.offline = False args.offline = False
if args.offline and configs.has_option('settings', 'offline'): if args.offline and configs.has_option('settings', 'offline'):
@ -285,6 +311,8 @@ def parse_arguments():
'domain\\user:pass.') 'domain\\user:pass.')
if configs.has_option('settings', 'no_ssl_verify'): if configs.has_option('settings', 'no_ssl_verify'):
args.nosslverify = configs.getboolean('settings', 'no_ssl_verify') args.nosslverify = configs.getboolean('settings', 'no_ssl_verify')
if configs.has_option('settings', 'ssl_certs_file'):
args.ssl_certs_file = configs.get('settings', 'ssl_certs_file')
if not args.verbose and configs.has_option('settings', 'verbose'): if not args.verbose and configs.has_option('settings', 'verbose'):
args.verbose = configs.getboolean('settings', 'verbose') args.verbose = configs.getboolean('settings', 'verbose')
if not args.verbose and configs.has_option('settings', 'debug'): if not args.verbose and configs.has_option('settings', 'debug'):
@ -307,3 +335,30 @@ def parse_arguments():
print(traceback.format_exc()) print(traceback.format_exc())
return args, configs return args, configs
def boolean_or_list(config_name, args, configs, alternative_names=[]):
"""Get a boolean or list of regexes from args and configs."""
# when argument flag present, set to wildcard regex
for key in alternative_names + [config_name]:
if hasattr(args, key) and getattr(args, key):
setattr(args, config_name, ['.*'])
return
setattr(args, config_name, [])
option = None
alternative_names.insert(0, config_name)
for key in alternative_names:
if configs.has_option('settings', key):
option = configs.get('settings', key)
break
if option is not None:
if option.strip().lower() == 'true':
setattr(args, config_name, ['.*'])
elif option.strip().lower() != 'false':
for pattern in option.split("\n"):
if pattern.strip() != '':
getattr(args, config_name).append(pattern)

View File

@ -11,11 +11,15 @@
import codecs import codecs
import os
import platform
import subprocess
import sys import sys
is_py2 = (sys.version_info[0] == 2) is_py2 = (sys.version_info[0] == 2)
is_py3 = (sys.version_info[0] == 3) is_py3 = (sys.version_info[0] == 3)
is_win = platform.system() == 'Windows'
if is_py2: # pragma: nocover if is_py2: # pragma: nocover
@ -98,3 +102,21 @@ try:
from .packages import simplejson as json from .packages import simplejson as json
except (ImportError, SyntaxError): # pragma: nocover except (ImportError, SyntaxError): # pragma: nocover
import json import json
class Popen(subprocess.Popen):
"""Patched Popen to prevent opening cmd window on Windows platform."""
def __init__(self, *args, **kwargs):
startupinfo = kwargs.get('startupinfo')
if is_win or True:
try:
startupinfo = startupinfo or subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
except AttributeError:
pass
kwargs['startupinfo'] = startupinfo
if 'env' not in kwargs:
kwargs['env'] = os.environ.copy()
kwargs['env']['LANG'] = 'en-US' if is_win else 'en_US.UTF-8'
subprocess.Popen.__init__(self, *args, **kwargs)

View File

@ -45,3 +45,12 @@ Files larger than this in bytes will not have a line count stat for performance.
Default is 2MB. Default is 2MB.
""" """
MAX_FILE_SIZE_SUPPORTED = 2000000 MAX_FILE_SIZE_SUPPORTED = 2000000
""" Default limit of number of offline heartbeats to sync before exiting."""
DEFAULT_SYNC_OFFLINE_ACTIVITY = 100
""" Number of heartbeats per api request.
Even when sending more heartbeats, this is the number of heartbeats sent per
individual https request to the WakaTime API.
"""
HEARTBEATS_PER_REQUEST = 25

View File

@ -131,5 +131,5 @@ class DependencyParser(object):
if self.parser: if self.parser:
plugin = self.parser(self.source_file, lexer=self.lexer) plugin = self.parser(self.source_file, lexer=self.lexer)
dependencies = plugin.parse() dependencies = plugin.parse()
return list(set(dependencies)) return list(filter(bool, set(dependencies)))
return [] return []

View File

@ -10,8 +10,9 @@
import os import os
import logging import logging
import re import re
from subprocess import PIPE
from .compat import u, json from .compat import u, json, is_win, Popen
from .exceptions import SkipHeartbeat from .exceptions import SkipHeartbeat
from .project import get_project_info from .project import get_project_info
from .stats import get_file_stats from .stats import get_file_stats
@ -42,6 +43,8 @@ class Heartbeat(object):
cursorpos = None cursorpos = None
user_agent = None user_agent = None
_sensitive = ('dependencies', 'lines', 'lineno', 'cursorpos', 'branch')
def __init__(self, data, args, configs, _clone=None): def __init__(self, data, args, configs, _clone=None):
if not data: if not data:
self.skip = u('Skipping because heartbeat data is missing.') self.skip = u('Skipping because heartbeat data is missing.')
@ -67,6 +70,7 @@ class Heartbeat(object):
'debugging', 'debugging',
'running tests', 'running tests',
'manual testing', 'manual testing',
'writing tests',
'browsing', 'browsing',
'code reviewing', 'code reviewing',
'designing', 'designing',
@ -83,13 +87,17 @@ class Heartbeat(object):
return return
if self.type == 'file': if self.type == 'file':
self.entity = format_file_path(self.entity) self.entity = format_file_path(self.entity)
if not self.entity or not os.path.isfile(self.entity): self._format_local_file()
if not self._file_exists():
self.skip = u('File does not exist; ignoring this heartbeat.') self.skip = u('File does not exist; ignoring this heartbeat.')
return return
if self._excluded_by_missing_project_file(): if self._excluded_by_missing_project_file():
self.skip = u('Skipping because missing .wakatime-project file in parent path.') self.skip = u('Skipping because missing .wakatime-project file in parent path.')
return return
if args.local_file and not os.path.isfile(args.local_file):
args.local_file = None
project, branch = get_project_info(configs, self, data) project, branch = get_project_info(configs, self, data)
self.project = project self.project = project
self.branch = branch self.branch = branch
@ -104,7 +112,8 @@ class Heartbeat(object):
lineno=data.get('lineno'), lineno=data.get('lineno'),
cursorpos=data.get('cursorpos'), cursorpos=data.get('cursorpos'),
plugin=args.plugin, plugin=args.plugin,
language=data.get('language')) language=data.get('language'),
local_file=args.local_file)
except SkipHeartbeat as ex: except SkipHeartbeat as ex:
self.skip = u(ex) or 'Skipping' self.skip = u(ex) or 'Skipping'
return return
@ -132,7 +141,7 @@ class Heartbeat(object):
Returns a Heartbeat. Returns a Heartbeat.
""" """
if not self.args.hide_filenames: if not self.args.hide_file_names:
return self return self
if self.entity is None: if self.entity is None:
@ -141,29 +150,12 @@ class Heartbeat(object):
if self.type != 'file': if self.type != 'file':
return self return self
for pattern in self.args.hide_filenames: if self.should_obfuscate_filename():
try: self._sanitize_metadata()
compiled = re.compile(pattern, re.IGNORECASE) extension = u(os.path.splitext(self.entity)[1])
if compiled.search(self.entity): self.entity = u('HIDDEN{0}').format(extension)
elif self.should_obfuscate_project():
sanitized = {} self._sanitize_metadata()
sensitive = ['dependencies', 'lines', 'lineno', 'cursorpos', 'branch']
for key, val in self.items():
if key in sensitive:
sanitized[key] = None
else:
sanitized[key] = val
extension = u(os.path.splitext(self.entity)[1])
sanitized['entity'] = u('HIDDEN{0}').format(extension)
return self.update(sanitized)
except re.error as ex:
log.warning(u('Regex error ({msg}) for include pattern: {pattern}').format(
msg=u(ex),
pattern=u(pattern),
))
return self return self
@ -201,6 +193,38 @@ class Heartbeat(object):
is_write=self.is_write, is_write=self.is_write,
) )
def should_obfuscate_filename(self):
"""Returns True if hide_file_names is true or the entity file path
matches one in the list of obfuscated file paths."""
for pattern in self.args.hide_file_names:
try:
compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(self.entity):
return True
except re.error as ex:
log.warning(u('Regex error ({msg}) for hide_file_names pattern: {pattern}').format(
msg=u(ex),
pattern=u(pattern),
))
return False
def should_obfuscate_project(self):
"""Returns True if hide_project_names is true or the entity file path
matches one in the list of obfuscated project paths."""
for pattern in self.args.hide_project_names:
try:
compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(self.entity):
return True
except re.error as ex:
log.warning(u('Regex error ({msg}) for hide_project_names pattern: {pattern}').format(
msg=u(ex),
pattern=u(pattern),
))
return False
def _unicode(self, value): def _unicode(self, value):
if value is None: if value is None:
return None return None
@ -211,6 +235,91 @@ class Heartbeat(object):
return None return None
return [self._unicode(value) for value in values] return [self._unicode(value) for value in values]
def _file_exists(self):
return (self.entity and os.path.isfile(self.entity) or
self.args.local_file and os.path.isfile(self.args.local_file))
def _format_local_file(self):
"""When args.local_file empty on Windows, tries to map args.entity to a
unc path.
Updates args.local_file in-place without returning anything.
"""
if self.type != 'file':
return
if not self.entity:
return
if not is_win:
return
if self._file_exists():
return
self.args.local_file = self._to_unc_path(self.entity)
def _to_unc_path(self, filepath):
drive, rest = self._splitdrive(filepath)
if not drive:
return filepath
stdout = None
try:
stdout, stderr = Popen(['net', 'use'], stdout=PIPE, stderr=PIPE).communicate()
except OSError:
pass
else:
if stdout:
cols = None
for line in stdout.strip().splitlines()[1:]:
line = u(line)
if not line.strip():
continue
if not cols:
cols = self._unc_columns(line)
continue
start, end = cols.get('local', (0, 0))
if not start and not end:
break
local = line[start:end].strip().split(':')[0].upper()
if not local.isalpha():
continue
if local == drive:
start, end = cols.get('remote', (0, 0))
if not start and not end:
break
remote = line[start:end].strip()
return remote + rest
return filepath
def _unc_columns(self, line):
cols = {}
current_col = u('')
newcol = False
start, end = 0, 0
for char in line:
if char.isalpha():
if newcol:
cols[current_col.strip().lower()] = (start, end)
current_col = u('')
start = end
newcol = False
current_col += u(char)
else:
newcol = True
end += 1
if start != end and current_col:
cols[current_col.strip().lower()] = (start, -1)
return cols
def _splitdrive(self, filepath):
if filepath[1:2] != ':' or not filepath[0].isalpha():
return None, filepath
return filepath[0].upper(), filepath[2:]
def _excluded_by_pattern(self): def _excluded_by_pattern(self):
return should_exclude(self.entity, self.args.include, self.args.exclude) return should_exclude(self.entity, self.args.include, self.args.exclude)
@ -224,6 +333,10 @@ class Heartbeat(object):
return False return False
return find_project_file(self.entity) is None return find_project_file(self.entity) is None
def _sanitize_metadata(self):
for key in self._sensitive:
setattr(self, key, None)
def __repr__(self): def __repr__(self):
return self.json() return self.json()

View File

@ -1,81 +1,81 @@
{ {
"actionscript": "ActionScript", "actionscript": "ActionScript",
"apacheconf": "ApacheConf", "apacheconf": "ApacheConf",
"applescript": "AppleScript", "applescript": "AppleScript",
"asp": "ASP", "asp": "ASP",
"assembly": "Assembly", "assembly": "Assembly",
"awk": "Awk", "awk": "Awk",
"bash": "Bash", "bash": "Bash",
"basic": "Basic", "basic": "Basic",
"brightscript": "BrightScript", "brightscript": "BrightScript",
"c": "C", "c": "C",
"c#": "C#", "c#": "C#",
"c++": "C++", "c++": "C++",
"clojure": "Clojure", "clojure": "Clojure",
"cocoa": "Cocoa", "cocoa": "Cocoa",
"coffeescript": "CoffeeScript", "coffeescript": "CoffeeScript",
"coldfusion": "ColdFusion", "coldfusion": "ColdFusion",
"common lisp": "Common Lisp", "common lisp": "Common Lisp",
"cshtml": "CSHTML", "cshtml": "CSHTML",
"css": "CSS", "css": "CSS",
"dart": "Dart", "dart": "Dart",
"delphi": "Delphi", "delphi": "Delphi",
"elixir": "Elixir", "elixir": "Elixir",
"elm": "Elm", "elm": "Elm",
"emacs lisp": "Emacs Lisp", "emacs lisp": "Emacs Lisp",
"erlang": "Erlang", "erlang": "Erlang",
"f#": "F#", "f#": "F#",
"fortran": "Fortran", "fortran": "Fortran",
"go": "Go", "go": "Go",
"gous": "Gosu", "gosu": "Gosu",
"groovy": "Groovy", "groovy": "Groovy",
"haml": "Haml", "haml": "Haml",
"haskell": "Haskell", "haskell": "Haskell",
"haxe": "Haxe", "haxe": "Haxe",
"html": "HTML", "html": "HTML",
"ini": "INI", "ini": "INI",
"jade": "Jade", "jade": "Jade",
"java": "Java", "java": "Java",
"javascript": "JavaScript", "javascript": "JavaScript",
"json": "JSON", "json": "JSON",
"jsx": "JSX", "jsx": "JSX",
"kotlin": "Kotlin", "kotlin": "Kotlin",
"less": "LESS", "less": "LESS",
"lua": "Lua", "lua": "Lua",
"markdown": "Markdown", "markdown": "Markdown",
"matlab": "Matlab", "matlab": "Matlab",
"mustache": "Mustache", "mustache": "Mustache",
"objective-c": "Objective-C", "objective-c": "Objective-C",
"objective-c++": "Objective-C++", "objective-c++": "Objective-C++",
"objective-j": "Objective-J", "objective-j": "Objective-J",
"ocaml": "OCaml", "ocaml": "OCaml",
"perl": "Perl", "perl": "Perl",
"php": "PHP", "php": "PHP",
"powershell": "PowerShell", "powershell": "PowerShell",
"prolog": "Prolog", "prolog": "Prolog",
"puppet": "Puppet", "puppet": "Puppet",
"python": "Python", "python": "Python",
"r": "R", "r": "R",
"restructuredtext": "reStructuredText", "restructuredtext": "reStructuredText",
"ruby": "Ruby", "ruby": "Ruby",
"rust": "Rust", "rust": "Rust",
"sass": "Sass", "sass": "Sass",
"scala": "Scala", "scala": "Scala",
"scheme": "Scheme", "scheme": "Scheme",
"scss": "SCSS", "scss": "SCSS",
"shell": "Shell", "shell": "Shell",
"slim": "Slim", "slim": "Slim",
"smalltalk": "Smalltalk", "smalltalk": "Smalltalk",
"sql": "SQL", "sql": "SQL",
"swift": "Swift", "swift": "Swift",
"text": "Text", "text": "Text",
"turing": "Turing", "turing": "Turing",
"twig": "Twig", "twig": "Twig",
"typescript": "TypeScript", "typescript": "TypeScript",
"typoscript": "TypoScript", "typoscript": "TypoScript",
"vb.net": "VB.net", "vb.net": "VB.net",
"viml": "VimL", "viml": "VimL",
"xaml": "XAML", "xaml": "XAML",
"xml": "XML", "xml": "XML",
"yaml": "YAML" "yaml": "YAML"
} }

View File

@ -14,6 +14,7 @@ from __future__ import print_function
import logging import logging
import os import os
import sys import sys
import time
import traceback import traceback
pwd = os.path.dirname(os.path.abspath(__file__)) pwd = os.path.dirname(os.path.abspath(__file__))
@ -24,7 +25,7 @@ from .__about__ import __version__
from .api import send_heartbeats from .api import send_heartbeats
from .arguments import parse_arguments from .arguments import parse_arguments
from .compat import u, json from .compat import u, json
from .constants import SUCCESS, UNKNOWN_ERROR from .constants import SUCCESS, UNKNOWN_ERROR, HEARTBEATS_PER_REQUEST
from .logger import setup_logging from .logger import setup_logging
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
@ -63,12 +64,23 @@ def execute(argv=None):
msg=u(ex), msg=u(ex),
)) ))
retval = send_heartbeats(heartbeats, args, configs) retval = SUCCESS
while heartbeats:
retval = send_heartbeats(heartbeats[:HEARTBEATS_PER_REQUEST], args, configs)
heartbeats = heartbeats[HEARTBEATS_PER_REQUEST:]
if retval != SUCCESS:
break
if heartbeats:
Queue(args, configs).push_many(heartbeats)
if retval == SUCCESS: if retval == SUCCESS:
queue = Queue(args, configs) queue = Queue(args, configs)
offline_heartbeats = queue.pop_many() for offline_heartbeats in queue.pop_many(args.sync_offline_activity):
if len(offline_heartbeats) > 0: time.sleep(1)
retval = send_heartbeats(offline_heartbeats, args, configs) retval = send_heartbeats(offline_heartbeats, args, configs)
if retval != SUCCESS:
break
return retval return retval

View File

@ -15,6 +15,7 @@ import os
from time import sleep from time import sleep
from .compat import json from .compat import json
from .constants import DEFAULT_SYNC_OFFLINE_ACTIVITY, HEARTBEATS_PER_REQUEST
from .heartbeat import Heartbeat from .heartbeat import Heartbeat
@ -104,19 +105,23 @@ class Queue(object):
def pop_many(self, limit=None): def pop_many(self, limit=None):
if limit is None: if limit is None:
limit = 5 limit = DEFAULT_SYNC_OFFLINE_ACTIVITY
heartbeats = [] heartbeats = []
count = 0 count = 0
while limit == 0 or count < limit: while count < limit:
heartbeat = self.pop() heartbeat = self.pop()
if not heartbeat: if not heartbeat:
break break
heartbeats.append(heartbeat) heartbeats.append(heartbeat)
count += 1 count += 1
if count % HEARTBEATS_PER_REQUEST == 0:
yield heartbeats
heartbeats = []
return heartbeats if heartbeats:
yield heartbeats
def _get_db_file(self): def _get_db_file(self):
home = '~' home = '~'

View File

@ -9,8 +9,11 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import os
import logging import logging
import random
from .compat import open
from .projects.git import Git from .projects.git import Git
from .projects.mercurial import Mercurial from .projects.mercurial import Mercurial
from .projects.projectfile import ProjectFile from .projects.projectfile import ProjectFile
@ -65,6 +68,10 @@ def get_project_info(configs, heartbeat, data):
if project_name is None: if project_name is None:
project_name = data.get('project') or heartbeat.args.project project_name = data.get('project') or heartbeat.args.project
hide_project = heartbeat.should_obfuscate_project()
if hide_project and project_name is not None:
return project_name, None
if project_name is None or branch_name is None: if project_name is None or branch_name is None:
for plugin_cls in REV_CONTROL_PLUGINS: for plugin_cls in REV_CONTROL_PLUGINS:
@ -76,9 +83,18 @@ def get_project_info(configs, heartbeat, data):
if project.process(): if project.process():
project_name = project_name or project.name() project_name = project_name or project.name()
branch_name = branch_name or project.branch() branch_name = branch_name or project.branch()
if hide_project:
branch_name = None
project_name = generate_project_name()
project_file = os.path.join(project.folder(), '.wakatime-project')
try:
with open(project_file, 'w') as fh:
fh.write(project_name)
except IOError:
project_name = None
break break
if project_name is None: if project_name is None and not hide_project:
project_name = data.get('alternate_project') or heartbeat.args.alternate_project project_name = data.get('alternate_project') or heartbeat.args.alternate_project
return project_name, branch_name return project_name, branch_name
@ -88,3 +104,42 @@ def get_configs_for_plugin(plugin_name, configs):
if configs and configs.has_section(plugin_name): if configs and configs.has_section(plugin_name):
return dict(configs.items(plugin_name)) return dict(configs.items(plugin_name))
return None return None
def generate_project_name():
"""Generates a random project name."""
adjectives = [
'aged', 'ancient', 'autumn', 'billowing', 'bitter', 'black', 'blue', 'bold',
'broad', 'broken', 'calm', 'cold', 'cool', 'crimson', 'curly', 'damp',
'dark', 'dawn', 'delicate', 'divine', 'dry', 'empty', 'falling', 'fancy',
'flat', 'floral', 'fragrant', 'frosty', 'gentle', 'green', 'hidden', 'holy',
'icy', 'jolly', 'late', 'lingering', 'little', 'lively', 'long', 'lucky',
'misty', 'morning', 'muddy', 'mute', 'nameless', 'noisy', 'odd', 'old',
'orange', 'patient', 'plain', 'polished', 'proud', 'purple', 'quiet', 'rapid',
'raspy', 'red', 'restless', 'rough', 'round', 'royal', 'shiny', 'shrill',
'shy', 'silent', 'small', 'snowy', 'soft', 'solitary', 'sparkling', 'spring',
'square', 'steep', 'still', 'summer', 'super', 'sweet', 'throbbing', 'tight',
'tiny', 'twilight', 'wandering', 'weathered', 'white', 'wild', 'winter', 'wispy',
'withered', 'yellow', 'young'
]
nouns = [
'art', 'band', 'bar', 'base', 'bird', 'block', 'boat', 'bonus',
'bread', 'breeze', 'brook', 'bush', 'butterfly', 'cake', 'cell', 'cherry',
'cloud', 'credit', 'darkness', 'dawn', 'dew', 'disk', 'dream', 'dust',
'feather', 'field', 'fire', 'firefly', 'flower', 'fog', 'forest', 'frog',
'frost', 'glade', 'glitter', 'grass', 'hall', 'hat', 'haze', 'heart',
'hill', 'king', 'lab', 'lake', 'leaf', 'limit', 'math', 'meadow',
'mode', 'moon', 'morning', 'mountain', 'mouse', 'mud', 'night', 'paper',
'pine', 'poetry', 'pond', 'queen', 'rain', 'recipe', 'resonance', 'rice',
'river', 'salad', 'scene', 'sea', 'shadow', 'shape', 'silence', 'sky',
'smoke', 'snow', 'snowflake', 'sound', 'star', 'sun', 'sun', 'sunset',
'surf', 'term', 'thunder', 'tooth', 'tree', 'truth', 'union', 'unit',
'violet', 'voice', 'water', 'waterfall', 'wave', 'wildflower', 'wind', 'wood'
]
numbers = [str(x) for x in range(10)]
return ' '.join([
random.choice(adjectives).capitalize(),
random.choice(nouns).capitalize(),
random.choice(numbers) + random.choice(numbers),
])

View File

@ -43,3 +43,8 @@ class BaseProject(object):
""" Returns the current branch. """ Returns the current branch.
""" """
raise NotYetImplemented() raise NotYetImplemented()
def folder(self):
""" Returns the project's top folder path.
"""
raise NotYetImplemented()

View File

@ -25,6 +25,7 @@ class Git(BaseProject):
_submodule = None _submodule = None
_project_name = None _project_name = None
_head_file = None _head_file = None
_project_folder = None
def process(self): def process(self):
return self._find_git_config_file(self.path) return self._find_git_config_file(self.path)
@ -40,6 +41,9 @@ class Git(BaseProject):
return self._get_branch_from_head_file(line) return self._get_branch_from_head_file(line)
return u('master') return u('master')
def folder(self):
return self._project_folder
def _find_git_config_file(self, path): def _find_git_config_file(self, path):
path = os.path.realpath(path) path = os.path.realpath(path)
if os.path.isfile(path): if os.path.isfile(path):
@ -47,6 +51,7 @@ class Git(BaseProject):
if os.path.isfile(os.path.join(path, '.git', 'config')): if os.path.isfile(os.path.join(path, '.git', 'config')):
self._project_name = os.path.basename(path) self._project_name = os.path.basename(path)
self._head_file = os.path.join(path, '.git', 'HEAD') self._head_file = os.path.join(path, '.git', 'HEAD')
self._project_folder = path
return True return True
link_path = self._path_from_gitdir_link_file(path) link_path = self._path_from_gitdir_link_file(path)
@ -56,12 +61,14 @@ class Git(BaseProject):
if self._is_worktree(link_path): if self._is_worktree(link_path):
self._project_name = self._project_from_worktree(link_path) self._project_name = self._project_from_worktree(link_path)
self._head_file = os.path.join(link_path, 'HEAD') self._head_file = os.path.join(link_path, 'HEAD')
self._project_folder = path
return True return True
# next check if this is a submodule # next check if this is a submodule
if self._submodules_supported_for_path(path): if self._submodules_supported_for_path(path):
self._project_name = os.path.basename(path) self._project_name = os.path.basename(path)
self._head_file = os.path.join(link_path, 'HEAD') self._head_file = os.path.join(link_path, 'HEAD')
self._project_folder = path
return True return True
split_path = os.path.split(path) split_path = os.path.split(path)
@ -80,10 +87,10 @@ class Git(BaseProject):
disabled = self._configs.get('submodules_disabled') disabled = self._configs.get('submodules_disabled')
if not disabled or disabled.strip().lower() == 'false':
return True
if disabled.strip().lower() == 'true': if disabled.strip().lower() == 'true':
return False return False
if disabled.strip().lower() == 'false':
return True
for pattern in disabled.split("\n"): for pattern in disabled.split("\n"):
if pattern.strip(): if pattern.strip():

View File

@ -47,6 +47,11 @@ class Mercurial(BaseProject):
log.traceback(logging.WARNING) log.traceback(logging.WARNING)
return u('default') return u('default')
def folder(self):
if self.configDir:
return os.path.dirname(self.configDir)
return None
def _find_hg_config_dir(self, path): def _find_hg_config_dir(self, path):
path = os.path.realpath(path) path = os.path.realpath(path)
if os.path.isfile(path): if os.path.isfile(path):

View File

@ -12,10 +12,10 @@
import logging import logging
import os import os
import platform import platform
from subprocess import Popen, PIPE from subprocess import PIPE
from .base import BaseProject from .base import BaseProject
from ..compat import u, open from ..compat import u, open, Popen
try: try:
from collections import OrderedDict from collections import OrderedDict
except ImportError: # pragma: nocover except ImportError: # pragma: nocover
@ -41,6 +41,11 @@ class Subversion(BaseProject):
return None # pragma: nocover return None # pragma: nocover
return u(self.info['URL'].split('/')[-1].split('\\')[-1]) return u(self.info['URL'].split('/')[-1].split('\\')[-1])
def folder(self):
if 'Repository Root' not in self.info:
return None
return self.info['Repository Root']
def _find_binary(self): def _find_binary(self):
if self.binary_location: if self.binary_location:
return self.binary_location return self.binary_location
@ -65,24 +70,22 @@ class Subversion(BaseProject):
if not self._is_mac() or self._has_xcode_tools(): if not self._is_mac() or self._has_xcode_tools():
stdout = None stdout = None
try: try:
os.environ['LANG'] = 'en_US' stdout, stderr = Popen(
stdout, stderr = Popen([ [self._find_binary(), 'info', os.path.realpath(path)],
self._find_binary(), 'info', os.path.realpath(path) stdout=PIPE,
], stdout=PIPE, stderr=PIPE).communicate() stderr=PIPE,
).communicate()
except OSError: except OSError:
pass pass
else: else:
if stdout: if stdout:
for line in stdout.splitlines(): for line in stdout.splitlines():
line = u(line) line = u(line).split(': ', 1)
line = line.split(': ', 1)
if len(line) == 2: if len(line) == 2:
info[line[0]] = line[1] info[line[0]] = line[1]
return info return info
def _find_project_base(self, path, found=False): def _find_project_base(self, path, found=False):
if platform.system() == 'Windows':
return False # pragma: nocover
path = os.path.realpath(path) path = os.path.realpath(path)
if os.path.isfile(path): if os.path.isfile(path):
path = os.path.split(path)[0] path = os.path.split(path)[0]

View File

@ -25,6 +25,7 @@ from .packages.pygments.lexers import (
_fn_matches, _fn_matches,
basename, basename,
ClassNotFound, ClassNotFound,
CppLexer,
find_lexer_class, find_lexer_class,
get_lexer_by_name, get_lexer_by_name,
) )
@ -40,36 +41,33 @@ log = logging.getLogger('WakaTime')
def get_file_stats(file_name, entity_type='file', lineno=None, cursorpos=None, def get_file_stats(file_name, entity_type='file', lineno=None, cursorpos=None,
plugin=None, language=None): plugin=None, language=None, local_file=None):
if entity_type != 'file': """Returns a hash of information about the entity."""
stats = {
'language': None, language = standardize_language(language, plugin)
'dependencies': [], stats = {
'lines': None, 'language': language,
'lineno': lineno, 'dependencies': [],
'cursorpos': cursorpos, 'lines': None,
} 'lineno': lineno,
else: 'cursorpos': cursorpos,
language, lexer = standardize_language(language, plugin) }
if entity_type == 'file':
lexer = get_lexer(language)
if not language: if not language:
language, lexer = guess_language(file_name) language, lexer = guess_language(file_name, local_file)
parser = DependencyParser(local_file or file_name, lexer)
stats.update({
'language': use_root_language(language, lexer),
'dependencies': parser.parse(),
'lines': number_lines_in_file(local_file or file_name),
})
language = use_root_language(language, lexer)
parser = DependencyParser(file_name, lexer)
dependencies = parser.parse()
stats = {
'language': language,
'dependencies': dependencies,
'lines': number_lines_in_file(file_name),
'lineno': lineno,
'cursorpos': cursorpos,
}
return stats return stats
def guess_language(file_name): def guess_language(file_name, local_file):
"""Guess lexer and language for a file. """Guess lexer and language for a file.
Returns a tuple of (language_str, lexer_obj). Returns a tuple of (language_str, lexer_obj).
@ -81,14 +79,14 @@ def guess_language(file_name):
if language: if language:
lexer = get_lexer(language) lexer = get_lexer(language)
else: else:
lexer = smart_guess_lexer(file_name) lexer = smart_guess_lexer(file_name, local_file)
if lexer: if lexer:
language = u(lexer.name) language = u(lexer.name)
return language, lexer return language, lexer
def smart_guess_lexer(file_name): def smart_guess_lexer(file_name, local_file):
"""Guess Pygments lexer for a file. """Guess Pygments lexer for a file.
Looks for a vim modeline in file contents, then compares the accuracy Looks for a vim modeline in file contents, then compares the accuracy
@ -99,7 +97,7 @@ def smart_guess_lexer(file_name):
text = get_file_head(file_name) text = get_file_head(file_name)
lexer1, accuracy1 = guess_lexer_using_filename(file_name, text) lexer1, accuracy1 = guess_lexer_using_filename(local_file or file_name, text)
lexer2, accuracy2 = guess_lexer_using_modeline(text) lexer2, accuracy2 = guess_lexer_using_modeline(text)
if lexer1: if lexer1:
@ -171,6 +169,10 @@ def get_language_from_extension(file_name):
""" """
filepart, extension = os.path.splitext(file_name) filepart, extension = os.path.splitext(file_name)
pathpart, filename = os.path.split(file_name)
if filename == 'go.mod':
return 'Go'
if re.match(r'\.h.*$', extension, re.IGNORECASE) or re.match(r'\.c.*$', extension, re.IGNORECASE): if re.match(r'\.h.*$', extension, re.IGNORECASE) or re.match(r'\.c.*$', extension, re.IGNORECASE):
@ -184,8 +186,12 @@ def get_language_from_extension(file_name):
return 'Objective-C++' return 'Objective-C++'
available_extensions = extensions_in_same_folder(file_name) available_extensions = extensions_in_same_folder(file_name)
if '.cpp' in available_extensions:
return 'C++' for ext in CppLexer.filenames:
ext = ext.lstrip('*')
if ext in available_extensions:
return 'C++'
if '.c' in available_extensions: if '.c' in available_extensions:
return 'C' return 'C'
@ -222,22 +228,21 @@ def number_lines_in_file(file_name):
def standardize_language(language, plugin): def standardize_language(language, plugin):
"""Maps a string to the equivalent Pygments language. """Maps a string to the equivalent Pygments language.
Returns a tuple of (language_str, lexer_obj). Returns the standardized language string.
""" """
if not language: if not language:
return None, None return None
# standardize language for this plugin # standardize language for this plugin
if plugin: if plugin:
plugin = plugin.split(' ')[-1].split('/')[0].split('-')[0] plugin = plugin.split(' ')[-1].split('/')[0].split('-')[0]
standardized = get_language_from_json(language, plugin) standardized = get_language_from_json(language, plugin)
if standardized is not None: if standardized is not None:
return standardized, get_lexer(standardized) return standardized
# standardize language against default languages # standardize language against default languages
standardized = get_language_from_json(language, 'default') return get_language_from_json(language, 'default')
return standardized, get_lexer(standardized)
def get_lexer(language): def get_lexer(language):

View File

@ -26,6 +26,7 @@ log = logging.getLogger('WakaTime')
BACKSLASH_REPLACE_PATTERN = re.compile(r'[\\/]+') BACKSLASH_REPLACE_PATTERN = re.compile(r'[\\/]+')
WINDOWS_DRIVE_PATTERN = re.compile(r'^[a-z]:/') WINDOWS_DRIVE_PATTERN = re.compile(r'^[a-z]:/')
WINDOWS_NETWORK_MOUNT_PATTERN = re.compile(r'^\\{2}[a-z]+', re.IGNORECASE)
def should_exclude(entity, include, exclude): def should_exclude(entity, include, exclude):
@ -77,10 +78,16 @@ def format_file_path(filepath):
"""Formats a path as absolute and with the correct platform separator.""" """Formats a path as absolute and with the correct platform separator."""
try: try:
is_windows_network_mount = WINDOWS_NETWORK_MOUNT_PATTERN.match(filepath)
filepath = os.path.realpath(os.path.abspath(filepath)) filepath = os.path.realpath(os.path.abspath(filepath))
filepath = re.sub(BACKSLASH_REPLACE_PATTERN, '/', filepath) filepath = re.sub(BACKSLASH_REPLACE_PATTERN, '/', filepath)
if WINDOWS_DRIVE_PATTERN.match(filepath): is_windows_drive = WINDOWS_DRIVE_PATTERN.match(filepath)
if is_windows_drive:
filepath = filepath.capitalize() filepath = filepath.capitalize()
if is_windows_network_mount:
# Add back a / to the front, since the previous modifications
# will have replaced any double slashes with single
filepath = '/' + filepath
except: except:
pass pass
return filepath return filepath