New config hide_project_names

This commit is contained in:
Alan Hamlett 2018-07-18 01:01:31 -07:00
parent 3373ef39e4
commit 71d5eef12a
15 changed files with 165 additions and 17 deletions

View file

@ -3,6 +3,13 @@ History
------- -------
10.2.2 (Unreleased)
+++++++++++++++++++
- New config hide_project_name and argument --hide-project-names for
obfuscating project names when sending coding activity to api.
10.2.1 (2018-04-26) 10.2.1 (2018-04-26)
+++++++++++++++++++ +++++++++++++++++++

View file

@ -61,6 +61,7 @@ format. An example config file with all available options::
debug = false debug = false
api_key = your-api-key api_key = your-api-key
hide_filenames = false hide_filenames = false
hide_project_names = false
exclude = exclude =
^COMMIT_EDITMSG$ ^COMMIT_EDITMSG$
^TAG_EDITMSG$ ^TAG_EDITMSG$

View file

@ -0,0 +1,5 @@
[settings]
debug = false
api_key = c21f8ebd-6a6a-48a0-900b-0870db3d7afe
api_url = https://api.wakatime.com/api/v1/heartbeats
hide_project_names = true

View file

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

View file

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

View file

@ -4,8 +4,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN]
[--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT]
[--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE]
[--hostname HOSTNAME] [--disable-offline] [--hide-filenames] [--hostname HOSTNAME] [--disable-offline] [--hide-filenames]
[--exclude EXCLUDE] [--exclude-unknown-project] [--hide-project-names] [--exclude EXCLUDE]
[--include INCLUDE] [--include-only-with-project-file] [--exclude-unknown-project] [--include INCLUDE]
[--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--include-only-with-project-file] [--extra-heartbeats]
[--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT]
[--config CONFIG] [--verbose] [--version]
wakatime: error: argument --timeout: invalid int value: 'abc' wakatime: error: argument --timeout: invalid int value: 'abc'

View file

@ -4,10 +4,11 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN]
[--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT]
[--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE]
[--hostname HOSTNAME] [--disable-offline] [--hide-filenames] [--hostname HOSTNAME] [--disable-offline] [--hide-filenames]
[--exclude EXCLUDE] [--exclude-unknown-project] [--hide-project-names] [--exclude EXCLUDE]
[--include INCLUDE] [--include-only-with-project-file] [--exclude-unknown-project] [--include INCLUDE]
[--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] [--include-only-with-project-file] [--extra-heartbeats]
[--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT]
[--config CONFIG] [--verbose] [--version]
Common interface for the WakaTime api. Common interface for the WakaTime api.
@ -49,6 +50,10 @@ optional arguments:
--disable-offline Disables offline time logging instead of queuing --disable-offline Disables offline time logging instead of queuing
logged time. logged time.
--hide-filenames Obfuscate filenames. Will not send file names to api. --hide-filenames Obfuscate filenames. Will not send file names to api.
--hide-project-names 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.
--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.
--exclude-unknown-project --exclude-unknown-project

View file

@ -459,6 +459,46 @@ class ConfigsTestCase(TestCase):
self.assertOfflineHeartbeatsSynced() self.assertOfflineHeartbeatsSynced()
self.assertSessionCacheSaved() self.assertSessionCacheSaved()
def test_obfuscte_project_names(self):
self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse()
with TemporaryDirectory() as tempdir:
shutil.copytree('tests/samples/projects/git', os.path.join(tempdir, 'git'))
shutil.move(os.path.join(tempdir, 'git', 'dot_git'), os.path.join(tempdir, 'git', '.git'))
entity = os.path.join(tempdir, 'git', 'emptyfile.txt')
now = u(int(time.time()))
config = 'tests/samples/configs/paranoid_projects.cfg'
key = u(uuid.uuid4())
dependencies = []
generated_proj = 'Icy Bridge 42'
args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log']
with mock.patch('wakatime.project.generate_project_name') as mock_proj:
mock_proj.return_value = generated_proj
retval = execute(args)
self.assertEquals(retval, SUCCESS)
self.assertNothingPrinted()
heartbeat = {
'language': 'Text only',
'lines': 0,
'entity': os.path.realpath(entity),
'project': generated_proj,
'time': float(now),
'is_write': False,
'type': 'file',
'dependencies': dependencies,
'user_agent': ANY,
}
self.assertHeartbeatSent(heartbeat)
self.assertHeartbeatNotSavedOffline()
self.assertOfflineHeartbeatsSynced()
self.assertSessionCacheSaved()
@log_capture() @log_capture()
def test_exclude_file(self, logs): def test_exclude_file(self, logs):
logging.disable(logging.NOTSET) logging.disable(logging.NOTSET)

View file

@ -15,6 +15,7 @@ from testfixtures import log_capture
from wakatime.compat import u, open from wakatime.compat import u, open
from wakatime.constants import API_ERROR, SUCCESS from wakatime.constants import API_ERROR, SUCCESS
from wakatime.exceptions import NotYetImplemented from wakatime.exceptions import NotYetImplemented
from wakatime.project import generate_project_name
from wakatime.projects.base import BaseProject from wakatime.projects.base import BaseProject
from wakatime.projects.git import Git from wakatime.projects.git import Git
from .utils import ANY, DynamicIterable, TestCase, TemporaryDirectory, CustomResponse, mock, json from .utils import ANY, DynamicIterable, TestCase, TemporaryDirectory, CustomResponse, mock, json
@ -723,3 +724,7 @@ class ProjectTestCase(TestCase):
self.assertNothingPrinted() self.assertNothingPrinted()
self.assertNothingLogged(logs) self.assertNothingLogged(logs)
self.assertEquals('proj-arg', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) self.assertEquals('proj-arg', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project'])
def test_generate_project_name(self):
self.assertGreater(len(generate_project_name()), 1)
self.assertNotEqual(generate_project_name(), generate_project_name())

View file

@ -133,6 +133,13 @@ def parse_arguments():
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.')
@ -267,6 +274,8 @@ def parse_arguments():
for pattern in option.split("\n"): for pattern in option.split("\n"):
if pattern.strip() != '': if pattern.strip() != '':
args.hide_filenames.append(pattern) args.hide_filenames.append(pattern)
if not args.hide_project_names and configs.has_option('settings', 'hide_project_names'):
args.hide_project_names = configs.getboolean('settings', 'hide_project_names')
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'):

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
@ -62,7 +65,7 @@ def get_project_info(configs, heartbeat, data):
branch_name = project.branch() branch_name = project.branch()
break break
if project_name is None: if project_name is None and not data.get('hide_project_names'):
project_name = data.get('project') or heartbeat.args.project project_name = data.get('project') or heartbeat.args.project
if project_name is None or branch_name is None: if project_name is None or branch_name is None:
@ -76,6 +79,15 @@ 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 data.get('hide_project_names'):
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:
@ -88,3 +100,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)

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

@ -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