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)
+++++++++++++++++++

View file

@ -61,6 +61,7 @@ format. An example config file with all available options::
debug = false
api_key = your-api-key
hide_filenames = false
hide_project_names = false
exclude =
^COMMIT_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]
[--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE]
[--hostname HOSTNAME] [--disable-offline] [--hide-filenames]
[--exclude EXCLUDE] [--exclude-unknown-project]
[--include INCLUDE] [--include-only-with-project-file]
[--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL]
[--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version]
[--hide-project-names] [--exclude EXCLUDE]
[--exclude-unknown-project] [--include INCLUDE]
[--include-only-with-project-file] [--extra-heartbeats]
[--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.

View file

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

View file

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

View file

@ -459,6 +459,46 @@ class ConfigsTestCase(TestCase):
self.assertOfflineHeartbeatsSynced()
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()
def test_exclude_file(self, logs):
logging.disable(logging.NOTSET)

View file

@ -15,6 +15,7 @@ from testfixtures import log_capture
from wakatime.compat import u, open
from wakatime.constants import API_ERROR, SUCCESS
from wakatime.exceptions import NotYetImplemented
from wakatime.project import generate_project_name
from wakatime.projects.base import BaseProject
from wakatime.projects.git import Git
from .utils import ANY, DynamicIterable, TestCase, TemporaryDirectory, CustomResponse, mock, json
@ -723,3 +724,7 @@ class ProjectTestCase(TestCase):
self.assertNothingPrinted()
self.assertNothingLogged(logs)
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',
action='store_true',
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',
help='Filename patterns to exclude from logging. ' +
'POSIX regex syntax. Can be used more than once.')
@ -267,6 +274,8 @@ def parse_arguments():
for pattern in option.split("\n"):
if pattern.strip() != '':
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:
args.offline = False
if args.offline and configs.has_option('settings', 'offline'):

View file

@ -9,8 +9,11 @@
:license: BSD, see LICENSE for more details.
"""
import os
import logging
import random
from .compat import open
from .projects.git import Git
from .projects.mercurial import Mercurial
from .projects.projectfile import ProjectFile
@ -62,7 +65,7 @@ def get_project_info(configs, heartbeat, data):
branch_name = project.branch()
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
if project_name is None or branch_name is None:
@ -76,6 +79,15 @@ def get_project_info(configs, heartbeat, data):
if project.process():
project_name = project_name or project.name()
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
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):
return dict(configs.items(plugin_name))
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.
"""
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
_project_name = None
_head_file = None
_project_folder = None
def process(self):
return self._find_git_config_file(self.path)
@ -40,6 +41,9 @@ class Git(BaseProject):
return self._get_branch_from_head_file(line)
return u('master')
def folder(self):
return self._project_folder
def _find_git_config_file(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
@ -47,6 +51,7 @@ class Git(BaseProject):
if os.path.isfile(os.path.join(path, '.git', 'config')):
self._project_name = os.path.basename(path)
self._head_file = os.path.join(path, '.git', 'HEAD')
self._project_folder = path
return True
link_path = self._path_from_gitdir_link_file(path)
@ -56,12 +61,14 @@ class Git(BaseProject):
if self._is_worktree(link_path):
self._project_name = self._project_from_worktree(link_path)
self._head_file = os.path.join(link_path, 'HEAD')
self._project_folder = path
return True
# next check if this is a submodule
if self._submodules_supported_for_path(path):
self._project_name = os.path.basename(path)
self._head_file = os.path.join(link_path, 'HEAD')
self._project_folder = path
return True
split_path = os.path.split(path)

View file

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

View file

@ -41,6 +41,11 @@ class Subversion(BaseProject):
return None # pragma: nocover
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):
if self.binary_location:
return self.binary_location