Ability to only track folders containing a .wakatime-project file

This commit is contained in:
Alan Hamlett 2018-01-04 23:24:19 -08:00
parent 15408459af
commit 27e2d13788
22 changed files with 351 additions and 86 deletions

View file

@ -60,7 +60,7 @@ format. An example config file with all available options::
[settings] [settings]
debug = false debug = false
api_key = your-api-key api_key = your-api-key
hidefilenames = false hide_filenames = false
exclude = exclude =
^COMMIT_EDITMSG$ ^COMMIT_EDITMSG$
^TAG_EDITMSG$ ^TAG_EDITMSG$
@ -68,6 +68,7 @@ format. An example config file with all available options::
^/etc/ ^/etc/
include = include =
.* .*
only_include_with_project_file = false
offline = true offline = true
proxy = https://user:pass@localhost:8080 proxy = https://user:pass@localhost:8080
no_ssl_verify = false no_ssl_verify = false

View file

@ -1,8 +1,8 @@
[settings] [settings]
verbose = true verbose = true
api_key = d491a956-c8f2-44a9-98a7-987814bd71ba api_key = d491a956-c8f2-44a9-98a7-987814bd71ba
logfile = /tmp/waka log_file = /tmp/waka
hidefilenames = true hide_filenames = true
exclude = exclude =
^COMMIT_EDITMSG$ ^COMMIT_EDITMSG$
^TAG_EDITMSG$ ^TAG_EDITMSG$

View file

@ -1,6 +1,6 @@
[settings] [settings]
debug = false debug = false
api_key = 033c47c9-0441-4eb5-8b3f-b51f27b31049 api_key = 033c47c9-0441-4eb5-8b3f-b51f27b31049
hidefilenames = hide_filenames =
missingfile missingfile
python\.py$ python\.py$

View file

@ -0,0 +1,3 @@
[settings]
api_key = 1090a6ae-855f-4be7-b8fb-3edbaf1aa3ec
include_only_with_project_file = true

View file

@ -1,4 +1,4 @@
[settings] [settings]
debug = false debug = false
api_key = 033c47c9-0441-4eb5-8b3f-b51f27b31049 api_key = 033c47c9-0441-4eb5-8b3f-b51f27b31049
hidefilenames = invalid(regex hide_filenames = invalid(regex

View file

@ -12,4 +12,4 @@ include =
\(invalid regex) \(invalid regex)
includeme includeme
offline = true offline = true
hidefilenames = true hide_filenames = true

View file

@ -0,0 +1,15 @@
[settings]
debug = false
api_key = c21f8ebd-6a6a-48a0-900b-0870db3d7afe
api_url = https://api.wakatime.com/api/v1/heartbeats
ignore =
COMMIT_EDITMSG$
TAG_EDITMSG$
exclude =
excludeme
\(invalid regex)
include =
\(invalid regex)
includeme
offline = true
hidefilenames = true

View file

@ -2,8 +2,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN]
[--time time] [--lineno LINENO] [--cursorpos CURSORPOS] [--time time] [--lineno LINENO] [--cursorpos CURSORPOS]
[--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify] [--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify]
[--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT]
[--language LANGUAGE] [--hostname HOSTNAME] [--disableoffline] [--language LANGUAGE] [--hostname HOSTNAME]
[--hidefilenames] [--exclude EXCLUDE] [--include INCLUDE] [--disable-offline] [--hide-filenames] [--exclude EXCLUDE]
[--extra-heartbeats] [--logfile LOGFILE] [--apiurl API_URL] [--include INCLUDE] [--include-only-with-project-file]
[--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL]
[--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version]
wakatime: error: Missing api key. Find your api key from wakatime.com/settings. wakatime: error: Missing api key. Find your api key from wakatime.com/settings.

View file

@ -2,8 +2,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN]
[--time time] [--lineno LINENO] [--cursorpos CURSORPOS] [--time time] [--lineno LINENO] [--cursorpos CURSORPOS]
[--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify] [--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify]
[--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT]
[--language LANGUAGE] [--hostname HOSTNAME] [--disableoffline] [--language LANGUAGE] [--hostname HOSTNAME]
[--hidefilenames] [--exclude EXCLUDE] [--include INCLUDE] [--disable-offline] [--hide-filenames] [--exclude EXCLUDE]
[--extra-heartbeats] [--logfile LOGFILE] [--apiurl API_URL] [--include INCLUDE] [--include-only-with-project-file]
[--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL]
[--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version]
wakatime: error: Missing api key. Find your api key from wakatime.com/settings. wakatime: error: Missing api key. Find your api key from wakatime.com/settings.

View file

@ -2,8 +2,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN]
[--time time] [--lineno LINENO] [--cursorpos CURSORPOS] [--time time] [--lineno LINENO] [--cursorpos CURSORPOS]
[--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify] [--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify]
[--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT]
[--language LANGUAGE] [--hostname HOSTNAME] [--disableoffline] [--language LANGUAGE] [--hostname HOSTNAME]
[--hidefilenames] [--exclude EXCLUDE] [--include INCLUDE] [--disable-offline] [--hide-filenames] [--exclude EXCLUDE]
[--extra-heartbeats] [--logfile LOGFILE] [--apiurl API_URL] [--include INCLUDE] [--include-only-with-project-file]
[--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL]
[--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version]
wakatime: error: argument --timeout: invalid int value: 'abc' wakatime: error: argument --timeout: invalid int value: 'abc'

View file

@ -2,9 +2,10 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN]
[--time time] [--lineno LINENO] [--cursorpos CURSORPOS] [--time time] [--lineno LINENO] [--cursorpos CURSORPOS]
[--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify] [--entity-type ENTITY_TYPE] [--proxy PROXY] [--no-ssl-verify]
[--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT]
[--language LANGUAGE] [--hostname HOSTNAME] [--disableoffline] [--language LANGUAGE] [--hostname HOSTNAME]
[--hidefilenames] [--exclude EXCLUDE] [--include INCLUDE] [--disable-offline] [--hide-filenames] [--exclude EXCLUDE]
[--extra-heartbeats] [--logfile LOGFILE] [--apiurl API_URL] [--include INCLUDE] [--include-only-with-project-file]
[--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL]
[--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version]
Common interface for the WakaTime api. Common interface for the WakaTime api.
@ -39,18 +40,21 @@ optional arguments:
--language LANGUAGE optional language name; if valid, takes priority over --language LANGUAGE optional language name; if valid, takes priority over
auto-detected language auto-detected language
--hostname HOSTNAME hostname of current machine. --hostname HOSTNAME hostname of current machine.
--disableoffline disables offline time logging instead of queuing --disable-offline disables offline time logging instead of queuing
logged time logged time
--hidefilenames obfuscate file names; will not send file names to api --hide-filenames obfuscate filenames; will not send file names to api
--exclude EXCLUDE filename patterns to exclude from logging; POSIX regex --exclude EXCLUDE filename patterns to exclude from logging; POSIX regex
syntax; can be used more than once syntax; can be used more than once
--include INCLUDE filename patterns to log; when used in combination --include INCLUDE filename patterns to log; when used in combination
with --exclude, files matching include will still be with --exclude, files matching include will still be
logged; POSIX regex syntax; can be used more than once logged; POSIX regex syntax; can be used more than once
--include-only-with-project-file
disables tracking folders unless they contain a
.wakatime-project file; defaults to false
--extra-heartbeats reads extra heartbeats from STDIN as a JSON array --extra-heartbeats reads extra heartbeats from STDIN as a JSON array
until EOF until EOF
--logfile LOGFILE defaults to ~/.wakatime.log --log-file LOG_FILE defaults to ~/.wakatime.log
--apiurl API_URL heartbeats api url; for debugging with a local server --api-url API_URL heartbeats api url; for debugging with a local server
--timeout TIMEOUT number of seconds to wait when sending heartbeats to --timeout TIMEOUT number of seconds to wait when sending heartbeats to
api; defaults to 60 seconds api; defaults to 60 seconds
--config CONFIG defaults to ~/.wakatime.cfg --config CONFIG defaults to ~/.wakatime.cfg

View file

@ -12,13 +12,16 @@ import sys
import uuid import uuid
from testfixtures import log_capture from testfixtures import log_capture
from wakatime.arguments import parse_arguments from wakatime.arguments import parse_arguments
from wakatime.compat import u from wakatime.compat import u, is_py3
from wakatime.constants import ( from wakatime.constants import (
API_ERROR,
AUTH_ERROR, AUTH_ERROR,
SUCCESS, SUCCESS,
) )
from wakatime.packages.requests.exceptions import RequestException
from wakatime.packages.requests.models import Response
from wakatime.utils import get_user_agent from wakatime.utils import get_user_agent
from .utils import mock, json, ANY, CustomResponse, TemporaryDirectory, TestCase from .utils import mock, json, ANY, CustomResponse, TemporaryDirectory, TestCase, NamedTemporaryFile
class ArgumentsTestCase(TestCase): class ArgumentsTestCase(TestCase):
@ -944,12 +947,103 @@ class ArgumentsTestCase(TestCase):
with mock.patch.object(sys, 'argv', ['wakatime'] + args): with mock.patch.object(sys, 'argv', ['wakatime'] + args):
args, configs = parse_arguments() args, configs = parse_arguments()
self.assertEquals(args.logfile, None) self.assertEquals(args.log_file, None)
with mock.patch('os.environ.get') as mock_env: with mock.patch('os.environ.get') as mock_env:
mock_env.return_value = os.path.realpath(tempdir) mock_env.return_value = os.path.realpath(tempdir)
args, configs = parse_arguments() args, configs = parse_arguments()
self.assertEquals(args.logfile, logfile) self.assertEquals(args.log_file, logfile)
self.assertNothingPrinted() self.assertNothingPrinted()
self.assertNothingLogged(logs) self.assertNothingLogged(logs)
@log_capture()
def test_legacy_disableoffline_arg_supported(self, logs):
logging.disable(logging.NOTSET)
self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].side_effect = RequestException('requests exception')
with TemporaryDirectory() as tempdir:
entity = 'tests/samples/codefiles/twolinefile.txt'
shutil.copy(entity, os.path.join(tempdir, 'twolinefile.txt'))
entity = os.path.realpath(os.path.join(tempdir, 'twolinefile.txt'))
now = u(int(time.time()))
key = str(uuid.uuid4())
args = ['--file', entity, '--key', key, '--disableoffline',
'--config', 'tests/samples/configs/good_config.cfg', '--time', now]
retval = execute(args)
self.assertEquals(retval, API_ERROR)
self.assertNothingPrinted()
log_output = u("\n").join([u(' ').join(x) for x in logs.actual()])
expected = "WakaTime ERROR {'RequestException': u'requests exception'}"
if is_py3:
expected = "WakaTime ERROR {'RequestException': 'requests exception'}"
self.assertEquals(expected, log_output)
self.assertHeartbeatSent()
self.assertHeartbeatNotSavedOffline()
self.assertOfflineHeartbeatsNotSynced()
self.assertSessionCacheDeleted()
def test_legacy_hidefilenames_arg_supported(self):
self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse()
with TemporaryDirectory() as tempdir:
entity = 'tests/samples/codefiles/python.py'
shutil.copy(entity, os.path.join(tempdir, 'python.py'))
entity = os.path.realpath(os.path.join(tempdir, 'python.py'))
now = u(int(time.time()))
config = 'tests/samples/configs/good_config.cfg'
key = str(uuid.uuid4())
project = 'abcxyz'
args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--hidefilenames', '--logfile', '~/.wakatime.log', '--alternate-project', project]
retval = execute(args)
self.assertEquals(retval, SUCCESS)
self.assertNothingPrinted()
heartbeat = {
'language': 'Python',
'lines': None,
'entity': 'HIDDEN.py',
'project': project,
'time': float(now),
'is_write': False,
'type': 'file',
'dependencies': None,
'user_agent': ANY,
}
self.assertHeartbeatSent(heartbeat)
self.assertHeartbeatNotSavedOffline()
self.assertOfflineHeartbeatsSynced()
self.assertSessionCacheSaved()
@log_capture()
def test_deprecated_logfile_arg_supported(self, logs):
logging.disable(logging.NOTSET)
response = Response()
response.status_code = 0
self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response
with NamedTemporaryFile() as fh:
now = u(int(time.time()))
entity = 'tests/samples/codefiles/python.py'
config = 'tests/samples/configs/good_config.cfg'
logfile = os.path.realpath(fh.name)
args = ['--file', entity, '--config', config, '--time', now, '--logfile', logfile]
execute(args)
retval = execute(args)
self.assertEquals(retval, 102)
self.assertNothingPrinted()
self.assertEquals(logging.WARNING, logging.getLogger('WakaTime').level)
self.assertEquals(logfile, logging.getLogger('WakaTime').handlers[0].baseFilename)
logs.check()

View file

@ -42,7 +42,7 @@ class ConfigsTestCase(TestCase):
entity = 'tests/samples/codefiles/emptyfile.txt' entity = 'tests/samples/codefiles/emptyfile.txt'
shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt')) shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt'))
entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt'))
args = ['--file', entity, '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--log-file', '~/.wakatime.log']
with mock.patch('wakatime.configs.os.environ.get') as mock_env: with mock.patch('wakatime.configs.os.environ.get') as mock_env:
mock_env.return_value = None mock_env.return_value = None
@ -77,7 +77,7 @@ class ConfigsTestCase(TestCase):
with mock.patch('wakatime.configs.os.environ.get') as mock_env: with mock.patch('wakatime.configs.os.environ.get') as mock_env:
mock_env.return_value = tempdir mock_env.return_value = tempdir
args = ['--file', entity, '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--log-file', '~/.wakatime.log']
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
expected_stdout = open('tests/samples/output/configs_test_good_config_file').read() expected_stdout = open('tests/samples/output/configs_test_good_config_file').read()
@ -100,7 +100,7 @@ class ConfigsTestCase(TestCase):
shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt')) shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt'))
entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt'))
args = ['--file', entity, '--config', config, '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--config', config, '--log-file', '~/.wakatime.log']
with self.assertRaises(SystemExit) as e: with self.assertRaises(SystemExit) as e:
execute(args) execute(args)
@ -122,7 +122,7 @@ class ConfigsTestCase(TestCase):
entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt'))
config = 'tests/samples/configs/has_everything.cfg' config = 'tests/samples/configs/has_everything.cfg'
args = ['--file', entity, '--config', config, '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--config', config, '--log-file', '~/.wakatime.log']
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
expected_stdout = open('tests/samples/output/configs_test_good_config_file').read() expected_stdout = open('tests/samples/output/configs_test_good_config_file').read()
@ -151,7 +151,7 @@ class ConfigsTestCase(TestCase):
entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt'))
config = 'tests/samples/configs/sample_alternate_apikey.cfg' config = 'tests/samples/configs/sample_alternate_apikey.cfg'
args = ['--file', entity, '--config', config, '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--config', config, '--log-file', '~/.wakatime.log']
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
self.assertEquals(sys.stdout.getvalue(), '') self.assertEquals(sys.stdout.getvalue(), '')
@ -174,7 +174,7 @@ class ConfigsTestCase(TestCase):
entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt'))
config = 'tests/samples/configs/bad_config.cfg' config = 'tests/samples/configs/bad_config.cfg'
args = ['--file', entity, '--config', config, '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--config', config, '--log-file', '~/.wakatime.log']
with self.assertRaises(SystemExit) as e: with self.assertRaises(SystemExit) as e:
execute(args) execute(args)
@ -207,7 +207,7 @@ class ConfigsTestCase(TestCase):
config = 'tests/samples/configs/good_config.cfg' config = 'tests/samples/configs/good_config.cfg'
key = str(uuid.uuid4()) key = str(uuid.uuid4())
args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log']
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
@ -246,7 +246,42 @@ class ConfigsTestCase(TestCase):
key = u(uuid.uuid4()) key = u(uuid.uuid4())
project = 'abcxyz' project = 'abcxyz'
args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log', '--alternate-project', project] args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log', '--alternate-project', project]
retval = execute(args)
self.assertEquals(retval, SUCCESS)
self.assertNothingPrinted()
heartbeat = {
'language': 'Python',
'lines': None,
'entity': 'HIDDEN.py',
'project': project,
'time': float(now),
'is_write': False,
'type': 'file',
'dependencies': None,
'user_agent': ANY,
}
self.assertHeartbeatSent(heartbeat)
self.assertHeartbeatNotSavedOffline()
self.assertOfflineHeartbeatsSynced()
self.assertSessionCacheSaved()
def test_legacy_hidefilenames_config_supported(self):
self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse()
with TemporaryDirectory() as tempdir:
entity = 'tests/samples/codefiles/python.py'
shutil.copy(entity, os.path.join(tempdir, 'python.py'))
entity = os.path.realpath(os.path.join(tempdir, 'python.py'))
now = u(int(time.time()))
config = 'tests/samples/configs/paranoid_legacy.cfg'
key = u(uuid.uuid4())
project = 'abcxyz'
args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log', '--alternate-project', project]
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
@ -281,7 +316,7 @@ class ConfigsTestCase(TestCase):
key = str(uuid.uuid4()) key = str(uuid.uuid4())
project = 'abcxyz' project = 'abcxyz'
args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--hidefilenames', '--logfile', '~/.wakatime.log', '--alternate-project', project] args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--hide-filenames', '--log-file', '~/.wakatime.log', '--alternate-project', project]
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
@ -316,7 +351,7 @@ class ConfigsTestCase(TestCase):
key = '033c47c9-0441-4eb5-8b3f-b51f27b31049' key = '033c47c9-0441-4eb5-8b3f-b51f27b31049'
project = 'abcxyz' project = 'abcxyz'
args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log', '--alternate-project', project] args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log', '--alternate-project', project]
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
@ -355,7 +390,7 @@ class ConfigsTestCase(TestCase):
dependencies = ['sqlalchemy', 'jinja', 'simplejson', 'flask', 'app', 'django', 'pygments', 'unittest', 'mock'] dependencies = ['sqlalchemy', 'jinja', 'simplejson', 'flask', 'app', 'django', 'pygments', 'unittest', 'mock']
project = 'abcxyz' project = 'abcxyz'
args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log', '--alternate-project', project] args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log', '--alternate-project', project]
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
@ -392,7 +427,7 @@ class ConfigsTestCase(TestCase):
config = 'tests/samples/configs/invalid_hide_file_names.cfg' config = 'tests/samples/configs/invalid_hide_file_names.cfg'
key = str(uuid.uuid4()) key = str(uuid.uuid4())
args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log']
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
@ -438,7 +473,7 @@ class ConfigsTestCase(TestCase):
entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt'))
config = 'tests/samples/configs/good_config.cfg' config = 'tests/samples/configs/good_config.cfg'
args = ['--file', entity, '--config', config, '--exclude', 'empty', '--verbose', '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--config', config, '--exclude', 'empty', '--verbose', '--log-file', '~/.wakatime.log']
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
self.assertNothingPrinted() self.assertNothingPrinted()
@ -452,6 +487,77 @@ class ConfigsTestCase(TestCase):
self.assertOfflineHeartbeatsSynced() self.assertOfflineHeartbeatsSynced()
self.assertSessionCacheUntouched() self.assertSessionCacheUntouched()
@log_capture()
def test_exclude_file_without_project_file(self, logs):
logging.disable(logging.NOTSET)
response = Response()
response.status_code = 0
self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = response
with TemporaryDirectory() as tempdir:
entity = 'tests/samples/codefiles/emptyfile.txt'
shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt'))
entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt'))
config = 'tests/samples/configs/include_only_with_project_file.cfg'
args = ['--file', entity, '--config', config, '--verbose', '--log-file', '~/.wakatime.log']
retval = execute(args)
self.assertEquals(retval, SUCCESS)
self.assertNothingPrinted()
actual = self.getLogOutput(logs)
expected = 'WakaTime DEBUG Skipping because missing .wakatime-project file in parent path.'
self.assertEquals(actual, expected)
self.assertHeartbeatNotSent()
self.assertHeartbeatNotSavedOffline()
self.assertOfflineHeartbeatsSynced()
self.assertSessionCacheUntouched()
@log_capture()
def test_include_file_with_project_file(self, logs):
logging.disable(logging.NOTSET)
self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse()
with TemporaryDirectory() as tempdir:
entity = 'tests/samples/codefiles/emptyfile.txt'
shutil.copy(entity, os.path.join(tempdir, 'emptyfile.txt'))
entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt'))
config = 'tests/samples/configs/include_only_with_project_file.cfg'
project = 'abcxyz'
now = u(int(time.time()))
with open(os.path.join(tempdir, '.wakatime-project'), 'w'):
pass
args = ['--file', entity, '--config', config, '--time', now, '--verbose', '--log-file', '~/.wakatime.log', '--project', project]
retval = execute(args)
self.assertEquals(retval, SUCCESS)
self.assertNothingPrinted()
heartbeat = {
'language': 'Text only',
'lines': 0,
'entity': os.path.realpath(entity),
'project': project,
'branch': ANY,
'cursorpos': None,
'lineno': None,
'time': float(now),
'is_write': False,
'type': 'file',
'dependencies': [],
'user_agent': ANY,
}
self.assertHeartbeatSent(heartbeat)
self.assertHeartbeatNotSavedOffline()
self.assertOfflineHeartbeatsSynced()
self.assertSessionCacheSaved()
def test_hostname_set_from_config_file(self): def test_hostname_set_from_config_file(self):
self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse() self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse()
@ -462,7 +568,7 @@ class ConfigsTestCase(TestCase):
hostname = 'fromcfgfile' hostname = 'fromcfgfile'
config = 'tests/samples/configs/has_everything.cfg' config = 'tests/samples/configs/has_everything.cfg'
args = ['--file', entity, '--config', config, '--timeout', '15', '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--config', config, '--timeout', '15', '--log-file', '~/.wakatime.log']
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
self.assertNothingPrinted() self.assertNothingPrinted()
@ -485,7 +591,7 @@ class ConfigsTestCase(TestCase):
entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt')) entity = os.path.realpath(os.path.join(tempdir, 'emptyfile.txt'))
config = 'tests/samples/configs/has_ssl_verify_disabled.cfg' config = 'tests/samples/configs/has_ssl_verify_disabled.cfg'
args = ['--file', entity, '--config', config, '--timeout', '15', '--logfile', '~/.wakatime.log'] args = ['--file', entity, '--config', config, '--timeout', '15', '--log-file', '~/.wakatime.log']
retval = execute(args) retval = execute(args)
self.assertEquals(retval, SUCCESS) self.assertEquals(retval, SUCCESS)
self.assertNothingPrinted() self.assertNothingPrinted()

View file

@ -17,9 +17,10 @@ class HeartbeatTestCase(TestCase):
class Args(object): class Args(object):
exclude = [] exclude = []
hidefilenames = ['.*'] hide_filenames = ['.*']
include = [] include = []
plugin = None plugin = None
include_only_with_project_file = None
data = { data = {
'entity': os.path.realpath('tests/samples/codefiles/python.py'), 'entity': os.path.realpath('tests/samples/codefiles/python.py'),
@ -49,9 +50,10 @@ class HeartbeatTestCase(TestCase):
class Args(object): class Args(object):
exclude = [] exclude = []
hidefilenames = [] hide_filenames = []
include = [] include = []
plugin = None plugin = None
include_only_with_project_file = None
data = { data = {
'entity': os.path.realpath('tests/samples/codefiles/python.py'), 'entity': os.path.realpath('tests/samples/codefiles/python.py'),
@ -72,7 +74,7 @@ class HeartbeatTestCase(TestCase):
logging.disable(logging.NOTSET) logging.disable(logging.NOTSET)
class Args(object): class Args(object):
hidefilenames = ['.*'] hide_filenames = ['.*']
plugin = None plugin = None
branch = 'abc123' branch = 'abc123'
@ -93,7 +95,7 @@ class HeartbeatTestCase(TestCase):
logging.disable(logging.NOTSET) logging.disable(logging.NOTSET)
class Args(object): class Args(object):
hidefilenames = ['.*'] hide_filenames = ['.*']
plugin = None plugin = None
branch = 'abc123' branch = 'abc123'

View file

@ -69,7 +69,7 @@ class LoggingTestCase(utils.TestCase):
entity = 'tests/samples/codefiles/python.py' entity = 'tests/samples/codefiles/python.py'
config = 'tests/samples/configs/good_config.cfg' config = 'tests/samples/configs/good_config.cfg'
logfile = os.path.realpath(fh.name) logfile = os.path.realpath(fh.name)
args = ['--file', entity, '--config', config, '--time', now, '--logfile', logfile] args = ['--file', entity, '--config', config, '--time', now, '--log-file', logfile]
execute(args) execute(args)

View file

@ -163,7 +163,7 @@ class MainTestCase(utils.TestCase):
'dependencies': [], 'dependencies': [],
} }
args = ['--file', entity, '--key', key, '--disableoffline', args = ['--file', entity, '--key', key, '--disable-offline',
'--config', 'tests/samples/configs/good_config.cfg', '--time', now] '--config', 'tests/samples/configs/good_config.cfg', '--time', now]
retval = execute(args) retval = execute(args)
@ -244,7 +244,7 @@ class MainTestCase(utils.TestCase):
now = u(int(time.time())) now = u(int(time.time()))
key = str(uuid.uuid4()) key = str(uuid.uuid4())
args = ['--file', entity, '--key', key, '--disableoffline', args = ['--file', entity, '--key', key, '--disable-offline',
'--config', 'tests/samples/configs/good_config.cfg', '--time', now] '--config', 'tests/samples/configs/good_config.cfg', '--time', now]
retval = execute(args) retval = execute(args)

View file

@ -25,7 +25,7 @@ class SessionCacheTestCase(utils.TestCase):
version = '' version = ''
plugin = '' plugin = ''
verbose = False verbose = False
logfile = '' log_file = ''
self.args = MockArgs() self.args = MockArgs()
setup_logging(self.args, self.args.version) setup_logging(self.args, self.args.version)

View file

@ -103,12 +103,17 @@ def parse_arguments():
'auto-detected language') 'auto-detected language')
parser.add_argument('--hostname', dest='hostname', action=StoreWithoutQuotes, help='hostname of '+ parser.add_argument('--hostname', dest='hostname', action=StoreWithoutQuotes, help='hostname of '+
'current machine.') 'current machine.')
parser.add_argument('--disableoffline', dest='offline', parser.add_argument('--disable-offline', dest='offline',
action='store_false', action='store_false',
help='disables offline time logging instead of queuing logged time') help='disables offline time logging instead of queuing logged time')
parser.add_argument('--hidefilenames', dest='hidefilenames', parser.add_argument('--disableoffline', dest='offline_deprecated',
action='store_true', help=argparse.SUPPRESS)
parser.add_argument('--hide-filenames', dest='hide_filenames',
action='store_true', action='store_true',
help='obfuscate filenames; will not send file names to api') help='obfuscate filenames; will not send file names to api')
parser.add_argument('--hidefilenames', dest='hidefilenames',
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--exclude', dest='exclude', action='append', parser.add_argument('--exclude', dest='exclude', action='append',
help='filename patterns to exclude from logging; POSIX regex '+ help='filename patterns to exclude from logging; POSIX regex '+
'syntax; can be used more than once') 'syntax; can be used more than once')
@ -116,15 +121,24 @@ def parse_arguments():
help='filename patterns to log; when used in combination with '+ help='filename patterns to log; when used in combination with '+
'--exclude, files matching include will still be logged; '+ '--exclude, files matching include will still be logged; '+
'POSIX regex syntax; can be used more than once') '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')
parser.add_argument('--ignore', dest='ignore', action='append', parser.add_argument('--ignore', dest='ignore', action='append',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument('--extra-heartbeats', dest='extra_heartbeats', parser.add_argument('--extra-heartbeats', dest='extra_heartbeats',
action='store_true', action='store_true',
help='reads extra heartbeats from STDIN as a JSON array until EOF') help='reads extra heartbeats from STDIN as a JSON array until EOF')
parser.add_argument('--logfile', dest='logfile', action=StoreWithoutQuotes, parser.add_argument('--log-file', dest='log_file', action=StoreWithoutQuotes,
help='defaults to ~/.wakatime.log') help='defaults to ~/.wakatime.log')
parser.add_argument('--apiurl', dest='api_url', action=StoreWithoutQuotes, parser.add_argument('--logfile', dest='logfile', action=StoreWithoutQuotes,
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, parser.add_argument('--timeout', dest='timeout', type=int, action=StoreWithoutQuotes,
help='number of seconds to wait when sending heartbeats to api; '+ help='number of seconds to wait when sending heartbeats to api; '+
'defaults to 60 seconds') 'defaults to 60 seconds')
@ -194,6 +208,8 @@ def parse_arguments():
args.exclude.append(pattern) args.exclude.append(pattern)
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'):
args.include_only_with_project_file = configs.get('settings', 'include_only_with_project_file')
if not args.include: if not args.include:
args.include = [] args.include = []
if configs.has_option('settings', 'include'): if configs.has_option('settings', 'include'):
@ -203,18 +219,26 @@ def parse_arguments():
args.include.append(pattern) args.include.append(pattern)
except TypeError: # pragma: nocover except TypeError: # pragma: nocover
pass pass
if args.hidefilenames: if not args.hide_filenames and args.hidefilenames:
args.hidefilenames = ['.*'] args.hide_filenames = args.hidefilenames
if args.hide_filenames:
args.hide_filenames = ['.*']
else: else:
args.hidefilenames = [] args.hide_filenames = []
option = None
if configs.has_option('settings', 'hidefilenames'): if configs.has_option('settings', 'hidefilenames'):
option = configs.get('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': if option.strip().lower() == 'true':
args.hidefilenames = ['.*'] args.hide_filenames = ['.*']
elif option.strip().lower() != 'false': elif option.strip().lower() != 'false':
for pattern in option.split("\n"): for pattern in option.split("\n"):
if pattern.strip() != '': if pattern.strip() != '':
args.hidefilenames.append(pattern) args.hide_filenames.append(pattern)
if args.offline_deprecated:
args.offline = False
if args.offline and configs.has_option('settings', 'offline'): if args.offline and configs.has_option('settings', 'offline'):
args.offline = configs.getboolean('settings', 'offline') args.offline = configs.getboolean('settings', 'offline')
if not args.proxy and configs.has_option('settings', 'proxy'): if not args.proxy and configs.has_option('settings', 'proxy'):
@ -235,11 +259,15 @@ def parse_arguments():
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'):
args.verbose = configs.getboolean('settings', 'debug') args.verbose = configs.getboolean('settings', 'debug')
if not args.logfile and configs.has_option('settings', 'logfile'): if not args.log_file and args.logfile:
args.logfile = configs.get('settings', 'logfile') args.log_file = args.logfile
if not args.logfile and os.environ.get('WAKATIME_HOME'): if not args.log_file and configs.has_option('settings', 'log_file'):
args.log_file = configs.get('settings', 'log_file')
if not args.log_file and os.environ.get('WAKATIME_HOME'):
home = os.environ.get('WAKATIME_HOME') home = os.environ.get('WAKATIME_HOME')
args.logfile = os.path.join(os.path.expanduser(home), '.wakatime.log') args.log_file = os.path.join(os.path.expanduser(home), '.wakatime.log')
if not args.api_url and args.apiurl:
args.api_url = args.apiurl
if not args.api_url and configs.has_option('settings', 'api_url'): if not args.api_url and configs.has_option('settings', 'api_url'):
args.api_url = configs.get('settings', 'api_url') args.api_url = configs.get('settings', 'api_url')
if not args.timeout and configs.has_option('settings', 'timeout'): if not args.timeout and configs.has_option('settings', 'timeout'):

View file

@ -14,7 +14,7 @@ import re
from .compat import u, json from .compat import u, json
from .project import get_project_info from .project import get_project_info
from .stats import get_file_stats from .stats import get_file_stats
from .utils import get_user_agent, should_exclude, format_file_path from .utils import get_user_agent, should_exclude, format_file_path, find_project_file
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
@ -66,9 +66,12 @@ 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 self.type == 'file' and (not self.entity or not os.path.isfile(self.entity)): if not self.entity or not os.path.isfile(self.entity):
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():
self.skip = u('Skipping because missing .wakatime-project file in parent path.')
return
project, branch = get_project_info(configs, self, data) project, branch = get_project_info(configs, self, data)
self.project = project self.project = project
@ -103,7 +106,7 @@ class Heartbeat(object):
Returns a Heartbeat. Returns a Heartbeat.
""" """
if not self.args.hidefilenames: if not self.args.hide_filenames:
return self return self
if self.entity is None: if self.entity is None:
@ -112,7 +115,7 @@ class Heartbeat(object):
if self.type != 'file': if self.type != 'file':
return self return self
for pattern in self.args.hidefilenames: for pattern in self.args.hide_filenames:
try: try:
compiled = re.compile(pattern, re.IGNORECASE) compiled = re.compile(pattern, re.IGNORECASE)
if compiled.search(self.entity): if compiled.search(self.entity):
@ -183,6 +186,11 @@ class Heartbeat(object):
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)
def _excluded_by_missing_project_file(self):
if not self.args.include_only_with_project_file:
return False
return find_project_file(self.entity) is None
def __repr__(self): def __repr__(self):
return self.json() return self.json()

View file

@ -75,7 +75,7 @@ def setup_logging(args, version):
for handler in logger.handlers: for handler in logger.handlers:
logger.removeHandler(handler) logger.removeHandler(handler)
set_log_level(logger, args) set_log_level(logger, args)
logfile = args.logfile logfile = args.log_file
if not logfile: if not logfile:
logfile = '~/.wakatime.log' logfile = '~/.wakatime.log'
handler = logging.FileHandler(os.path.expanduser(logfile)) handler = logging.FileHandler(os.path.expanduser(logfile))

View file

@ -12,11 +12,11 @@
""" """
import logging import logging
import os
import sys import sys
from .base import BaseProject from .base import BaseProject
from ..compat import u, open from ..compat import u, open
from ..utils import find_project_file
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
@ -25,7 +25,7 @@ log = logging.getLogger('WakaTime')
class ProjectFile(BaseProject): class ProjectFile(BaseProject):
def process(self): def process(self):
self.config = self._find_config(self.path) self.config = find_project_file(self.path)
self._project_name = None self._project_name = None
self._project_branch = None self._project_branch = None
@ -33,13 +33,13 @@ class ProjectFile(BaseProject):
try: try:
with open(self.config, 'r', encoding='utf-8') as fh: with open(self.config, 'r', encoding='utf-8') as fh:
self._project_name = u(fh.readline().strip()) self._project_name = u(fh.readline().strip()) or None
self._project_branch = u(fh.readline().strip()) self._project_branch = u(fh.readline().strip()) or None
except UnicodeDecodeError: # pragma: nocover except UnicodeDecodeError: # pragma: nocover
try: try:
with open(self.config, 'r', encoding=sys.getfilesystemencoding()) as fh: with open(self.config, 'r', encoding=sys.getfilesystemencoding()) as fh:
self._project_name = u(fh.readline().strip()) self._project_name = u(fh.readline().strip()) or None
self._project_branch = u(fh.readline().strip()) self._project_branch = u(fh.readline().strip()) or None
except: except:
log.traceback(logging.WARNING) log.traceback(logging.WARNING)
except IOError: # pragma: nocover except IOError: # pragma: nocover
@ -53,14 +53,3 @@ class ProjectFile(BaseProject):
def branch(self): def branch(self):
return self._project_branch return self._project_branch
def _find_config(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, '.wakatime-project')):
return os.path.join(path, '.wakatime-project')
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_config(split_path[0])

View file

@ -82,3 +82,15 @@ def format_file_path(filepath):
def get_hostname(args): def get_hostname(args):
return args.hostname or socket.gethostname() return args.hostname or socket.gethostname()
def find_project_file(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, '.wakatime-project')):
return os.path.join(path, '.wakatime-project')
split_path = os.path.split(path)
if split_path[1] == '':
return None
return find_project_file(split_path[0])