Compare commits

..

25 Commits
1.4.7 ... 1.6.1

Author SHA1 Message Date
4c7adf0943 v1.6.1 2013-12-13 16:38:52 +01:00
216a8eaa0a upgrade common wakatime package to v0.5.1 2013-12-13 16:37:54 +01:00
81f838489d v1.6.0 2013-12-13 16:11:46 +01:00
d6228b8dce use [WakaTime] namespace for all console messages 2013-12-13 16:08:34 +01:00
7a2c2b9750 fix variable definition bug 2013-12-13 15:41:45 +01:00
d9cc911595 upgrade common wakatime package to v0.5.0 2013-12-13 15:35:49 +01:00
805e2fe222 v1.5.2 2013-12-03 02:41:54 +01:00
bbcb39b2cf fix #18 by using a non-localized datetime in log 2013-12-03 02:38:30 +01:00
9f9b97c69f v1.5.1 2013-12-02 09:17:35 +01:00
908ff98613 decode file names with filesystem encoding, then encode as utf-8 before encoding with simplejson 2013-12-02 09:16:21 +01:00
37f74b4b56 v1.5.0 2013-11-28 12:18:44 +01:00
a1c0d7e489 prevent sending timestamp when saving same file multiple times. increase send frequency from 5 minutes to 2 minutes. 2013-11-28 12:16:43 +01:00
3127f264b4 v1.4.12 2013-11-21 01:11:40 -08:00
049bc57019 fix #17 by removing non-utf8 characters 2013-11-21 01:09:37 -08:00
03ec38bb67 v1.4.11 2013-11-13 18:07:41 -08:00
4fc1a55ff7 set project name with .wakatime-project file. upgrade wakatime package to 0.4.10. 2013-11-13 18:06:55 -08:00
f60815b813 upgrade wakatime package to recognize .markdown file extension 2013-11-03 11:36:43 -08:00
ca47c2308d v1.4.10 2013-10-31 17:20:12 -07:00
146a959747 recognize jinja2 file extensions as HTML 2013-10-31 17:19:17 -07:00
906184cd88 v1.4.9 2013-10-28 18:12:12 -07:00
a13e11d24d handle case where ignore patterns not defined 2013-10-28 18:09:51 -07:00
d34432217f v1.4.8 2013-10-27 21:31:34 -07:00
f2e8f85198 fix syntax in default sublime-settings file 2013-10-27 21:31:15 -07:00
05b08b6ab2 new sublime-setting ingore for ignoring files by regular expressions 2013-10-27 21:30:10 -07:00
685d242c60 upgrade wakatime package to v0.4.9. adds new ignore config to .wakatime.conf to ignore files based on regex patterns. 2013-10-27 21:07:42 -07:00
16 changed files with 476 additions and 162 deletions

View File

@ -3,6 +3,68 @@ History
------- -------
1.6.1 (2013-12-13)
++++++++++++++++++
- upgrade common wakatime package to v0.5.1
- second line in .wakatime-project now sets branch name
1.6.0 (2013-12-13)
++++++++++++++++++
- upgrade common wakatime package to v0.5.0
1.5.2 (2013-12-03)
++++++++++++++++++
- use non-localized datetime in log
1.5.1 (2013-12-02)
++++++++++++++++++
- decode file names with filesystem encoding, then encode as utf-8 for logging
1.5.0 (2013-11-28)
++++++++++++++++++
- increase "ping" frequency from every 5 minutes to every 2 minutes
- prevent sending multiple api requests when saving the same file
1.4.12 (2013-11-21)
+++++++++++++++++++
- handle UnicodeDecodeError exceptions when json encoding log messages
1.4.11 (2013-11-13)
+++++++++++++++++++
- placing .wakatime-project file in a folder will read the project's name from that file
1.4.10 (2013-10-31)
++++++++++++++++++
- recognize jinja2 file extensions as HTML
1.4.9 (2013-10-28)
++++++++++++++++++
- handle case where ignore patterns not defined
1.4.8 (2013-10-27)
++++++++++++++++++
- new setting to ignore files that match a regular expression pattern
1.4.7 (2013-10-26) 1.4.7 (2013-10-26)
++++++++++++++++++ ++++++++++++++++++

View File

@ -5,7 +5,7 @@ Maintainer: WakaTi.me <support@wakatime.com>
Website: https://www.wakati.me/ Website: https://www.wakati.me/
===========================================================""" ==========================================================="""
__version__ = '1.4.7' __version__ = '1.6.1'
import sublime import sublime
import sublime_plugin import sublime_plugin
@ -21,14 +21,17 @@ from os.path import expanduser, dirname, realpath, isfile, join, exists
# globals # globals
ACTION_FREQUENCY = 5 ACTION_FREQUENCY = 2
ST_VERSION = int(sublime.version()) ST_VERSION = int(sublime.version())
PLUGIN_DIR = dirname(realpath(__file__)) PLUGIN_DIR = dirname(realpath(__file__))
API_CLIENT = '%s/packages/wakatime/wakatime-cli.py' % PLUGIN_DIR API_CLIENT = '%s/packages/wakatime/wakatime-cli.py' % PLUGIN_DIR
SETTINGS_FILE = 'WakaTime.sublime-settings' SETTINGS_FILE = 'WakaTime.sublime-settings'
SETTINGS = {} SETTINGS = {}
LAST_ACTION = 0 LAST_ACTION = {
LAST_FILE = None 'time': 0,
'file': None,
'is_write': False,
}
HAS_SSL = False HAS_SSL = False
LOCK = threading.RLock() LOCK = threading.RLock()
@ -47,31 +50,6 @@ if HAS_SSL:
import wakatime import wakatime
def setup_settings_file():
""" Convert ~/.wakatime.conf to WakaTime.sublime-settings
"""
global SETTINGS
# To be backwards compatible, rename config file
SETTINGS = sublime.load_settings(SETTINGS_FILE)
api_key = SETTINGS.get('api_key', '')
if not api_key:
api_key = ''
try:
with open(join(expanduser('~'), '.wakatime.conf')) as old_file:
for line in old_file:
line = line.split('=', 1)
if line[0] == 'api_key':
api_key = str(line[1].strip())
try:
os.remove(join(expanduser('~'), '.wakatime.conf'))
except:
pass
except IOError:
pass
SETTINGS.set('api_key', api_key)
sublime.save_settings(SETTINGS_FILE)
def prompt_api_key(): def prompt_api_key():
global SETTINGS global SETTINGS
if not SETTINGS.get('api_key'): if not SETTINGS.get('api_key'):
@ -81,10 +59,10 @@ def prompt_api_key():
sublime.save_settings(SETTINGS_FILE) sublime.save_settings(SETTINGS_FILE)
window = sublime.active_window() window = sublime.active_window()
if window: if window:
window.show_input_panel('Enter your WakaTime api key:', '', got_key, None, None) window.show_input_panel('[WakaTime] Enter your wakatime.com api key:', '', got_key, None, None)
return True return True
else: else:
print('Error: Could not prompt for api key because no window found.') print('[WakaTime] Error: Could not prompt for api key because no window found.')
return False return False
@ -101,63 +79,60 @@ def python_binary():
return 'python' return 'python'
def enough_time_passed(now): def enough_time_passed(now, last_time):
if now - LAST_ACTION > ACTION_FREQUENCY * 60: if now - last_time > ACTION_FREQUENCY * 60:
return True return True
return False return False
def handle_write_action(view): def handle_action(view, is_write=False):
global LOCK, LAST_FILE, LAST_ACTION global LOCK, LAST_ACTION
with LOCK: with LOCK:
targetFile = view.file_name() target_file = view.file_name()
thread = SendActionThread(targetFile, isWrite=True) if target_file:
thread.start() thread = SendActionThread(target_file, is_write=is_write)
LAST_FILE = targetFile thread.start()
LAST_ACTION = time.time() LAST_ACTION = {
'file': target_file,
'time': time.time(),
def handle_normal_action(view): 'is_write': is_write,
global LOCK, LAST_FILE, LAST_ACTION }
with LOCK:
targetFile = view.file_name()
thread = SendActionThread(targetFile)
thread.start()
LAST_FILE = targetFile
LAST_ACTION = time.time()
class SendActionThread(threading.Thread): class SendActionThread(threading.Thread):
def __init__(self, targetFile, isWrite=False, force=False): def __init__(self, target_file, is_write=False, force=False):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.targetFile = targetFile self.target_file = target_file
self.isWrite = isWrite self.is_write = is_write
self.force = force self.force = force
self.debug = SETTINGS.get('debug') self.debug = SETTINGS.get('debug')
self.api_key = SETTINGS.get('api_key', '') self.api_key = SETTINGS.get('api_key', '')
self.last_file = LAST_FILE self.ignore = SETTINGS.get('ignore', [])
self.last_action = LAST_ACTION
def run(self): def run(self):
if self.targetFile: if self.target_file:
self.timestamp = time.time() self.timestamp = time.time()
if self.force or self.isWrite or self.targetFile != self.last_file or enough_time_passed(self.timestamp): if self.force or (self.is_write and not self.last_action['is_write']) or self.target_file != self.last_action['file'] or enough_time_passed(self.timestamp, self.last_action['time']):
self.send() self.send()
def send(self): def send(self):
if not self.api_key: if not self.api_key:
print('missing api key') print('[WakaTime] Error: missing api key.')
return return
ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__) ua = 'sublime/%d sublime-wakatime/%s' % (ST_VERSION, __version__)
cmd = [ cmd = [
API_CLIENT, API_CLIENT,
'--file', self.targetFile, '--file', self.target_file,
'--time', str('%f' % self.timestamp), '--time', str('%f' % self.timestamp),
'--plugin', ua, '--plugin', ua,
'--key', str(bytes.decode(self.api_key.encode('utf8'))), '--key', str(bytes.decode(self.api_key.encode('utf8'))),
] ]
if self.isWrite: if self.is_write:
cmd.append('--write') cmd.append('--write')
for pattern in self.ignore:
cmd.extend(['--ignore', pattern])
if self.debug: if self.debug:
cmd.append('--verbose') cmd.append('--verbose')
if HAS_SSL: if HAS_SSL:
@ -165,7 +140,7 @@ class SendActionThread(threading.Thread):
print(cmd) print(cmd)
code = wakatime.main(cmd) code = wakatime.main(cmd)
if code != 0: if code != 0:
print('Error: Response code %d from wakatime package' % code) print('[WakaTime] Error: Response code %d from wakatime package.' % code)
else: else:
python = python_binary() python = python_binary()
if python: if python:
@ -178,11 +153,12 @@ class SendActionThread(threading.Thread):
with open(join(expanduser('~'), '.wakatime.log'), 'a') as stderr: with open(join(expanduser('~'), '.wakatime.log'), 'a') as stderr:
Popen(cmd, stderr=stderr) Popen(cmd, stderr=stderr)
else: else:
print('Error: Unable to find python binary.') print('[WakaTime] Error: Unable to find python binary.')
def plugin_loaded(): def plugin_loaded():
setup_settings_file() global SETTINGS
SETTINGS = sublime.load_settings(SETTINGS_FILE)
after_loaded() after_loaded()
@ -199,10 +175,10 @@ if ST_VERSION < 3000:
class WakatimeListener(sublime_plugin.EventListener): class WakatimeListener(sublime_plugin.EventListener):
def on_post_save(self, view): def on_post_save(self, view):
handle_write_action(view) handle_action(view, is_write=True)
def on_activated(self, view): def on_activated(self, view):
handle_normal_action(view) handle_action(view)
def on_modified(self, view): def on_modified(self, view):
handle_normal_action(view) handle_action(view)

View File

@ -7,6 +7,10 @@
// Set this in your User specific WakaTime.sublime-settings file. // Set this in your User specific WakaTime.sublime-settings file.
"api_key": "", "api_key": "",
// Ignore files; Files (including absolute paths) that match one of these
// POSIX regular expressions will not be logged.
"ignore": ["^/tmp/", "^/etc/", "^/var/"],
// Debug mode. Set to true for verbose logging. Defaults to false. // Debug mode. Set to true for verbose logging. Defaults to false.
"debug": false "debug": false
} }

14
packages/wakatime/AUTHORS Normal file
View File

@ -0,0 +1,14 @@
WakaTime is written and maintained by Alan Hamlett and
various contributors:
Development Lead
----------------
- Alan Hamlett <alan.hamlett@gmail.com>
Patches and Suggestions
-----------------------
- 3onyc <3onyc@x3tech.com>

View File

@ -3,6 +3,32 @@ History
------- -------
0.5.1 (2013-12-13)
++++++++++++++++++
- second line in .wakatime-project file now sets branch name
0.5.0 (2013-12-13)
++++++++++++++++++
- Convert ~/.wakatime.conf to ~/.wakatime.cfg and use configparser format
- new [projectmap] section in cfg file for naming projects based on folders
0.4.10 (2013-11-13)
+++++++++++++++++++
- Placing .wakatime-project file in a folder will read the project's name from that file
0.4.9 (2013-10-27)
++++++++++++++++++
- New config for ignoring files from regular expressions
- Parse more options from config file (verbose, logfile, ignore)
0.4.8 (2013-10-13) 0.4.8 (2013-10-13)
++++++++++++++++++ ++++++++++++++++++

View File

@ -1,9 +1,11 @@
WakaTime WakaTime
======== ========
Automatic time tracking for your text editor. This is the command line Automatic time tracking for your text editor.
event appender for the WakaTime api. You shouldn't need to directly
use this outside of a text editor plugin. This is the common interface for the WakaTime api. You shouldn't need to directly use this package.
Go to http://wakatime.com to install the plugin for your text editor.
Installation Installation

View File

@ -1,10 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
wakatime-cli wakatime-cli
~~~~~~~~~~~~ ~~~~~~~~~~~~
Action event appender for Wakati.Me, auto time tracking for text editors. Command-line entry point.
:copyright: (c) 2013 Alan Hamlett. :copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.

View File

@ -3,7 +3,9 @@
wakatime wakatime
~~~~~~~~ ~~~~~~~~
Action event appender for Wakati.Me, auto time tracking for text editors. Common interface to WakaTime.com for most text editor plugins.
WakaTime.com is fully automatic time tracking for text editors.
More info at http://wakatime.com
:copyright: (c) 2013 Alan Hamlett. :copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
@ -12,7 +14,7 @@
from __future__ import print_function from __future__ import print_function
__title__ = 'wakatime' __title__ = 'wakatime'
__version__ = '0.4.8' __version__ = '0.5.1'
__author__ = 'Alan Hamlett' __author__ = 'Alan Hamlett'
__license__ = 'BSD' __license__ = 'BSD'
__copyright__ = 'Copyright 2013 Alan Hamlett' __copyright__ = 'Copyright 2013 Alan Hamlett'
@ -26,6 +28,15 @@ import re
import sys import sys
import time import time
import traceback import traceback
try:
import ConfigParser as configparser
except ImportError:
import configparser
try:
from urllib2 import HTTPError, Request, urlopen
except ImportError:
from urllib.error import HTTPError
from urllib.request import Request, urlopen
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages')) sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages'))
@ -35,11 +46,6 @@ from .stats import get_file_stats
from .packages import argparse from .packages import argparse
from .packages import simplejson as json from .packages import simplejson as json
from .packages import tzlocal from .packages import tzlocal
try:
from urllib2 import HTTPError, Request, urlopen
except ImportError:
from urllib.error import HTTPError
from urllib.request import Request, urlopen
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -52,11 +58,82 @@ class FileAction(argparse.Action):
setattr(namespace, self.dest, values) setattr(namespace, self.dest, values)
def upgradeConfigFile(configFile):
"""For backwards-compatibility, upgrade the existing config file
to work with configparser and rename from .wakatime.conf to .wakatime.cfg.
"""
if os.path.isfile(configFile):
# if upgraded cfg file already exists, don't overwrite it
return
oldConfig = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
try:
configs = {
'ignore': [],
}
with open(oldConfig) as fh:
for line in fh.readlines():
line = line.split('=', 1)
if len(line) == 2 and line[0].strip() and line[1].strip():
if line[0].strip() == 'ignore':
configs['ignore'].append(line[1].strip())
else:
configs[line[0].strip()] = line[1].strip()
with open(configFile, 'w') as fh:
fh.write("[settings]\n")
for name, value in configs.items():
if isinstance(value, list):
fh.write("%s=\n" % name)
for item in value:
fh.write(" %s\n" % item)
else:
fh.write("%s = %s\n" % (name, value))
os.remove(oldConfig)
except IOError:
pass
def parseConfigFile(configFile):
"""Returns a configparser.SafeConfigParser instance with configs
read from the config file. Default location of the config file is
at ~/.wakatime.cfg.
"""
if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
upgradeConfigFile(configFile)
configs = configparser.SafeConfigParser()
try:
with open(configFile) as fh:
try:
configs.readfp(fh)
except configparser.Error:
print(traceback.format_exc())
return None
except IOError:
if not os.path.isfile(configFile):
print('Error: Could not read from config file ~/.wakatime.conf')
return configs
def parseArguments(argv): def parseArguments(argv):
"""Parse command line arguments and configs from ~/.wakatime.cfg.
Command line arguments take precedence over config file settings.
Returns instances of ArgumentParser and SafeConfigParser.
"""
try: try:
sys.argv sys.argv
except AttributeError: except AttributeError:
sys.argv = argv sys.argv = argv
# define supported command line arguments
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Wakati.Me event api appender') description='Wakati.Me event api appender')
parser.add_argument('--file', dest='targetFile', metavar='file', parser.add_argument('--file', dest='targetFile', metavar='file',
@ -75,6 +152,8 @@ def parseArguments(argv):
parser.add_argument('--key', dest='key', parser.add_argument('--key', dest='key',
help='your wakati.me api key; uses api_key from '+ help='your wakati.me api key; uses api_key from '+
'~/.wakatime.conf by default') '~/.wakatime.conf by default')
parser.add_argument('--ignore', dest='ignore', action='append',
help='filename patterns to ignore; POSIX regex syntax; can be used more than once')
parser.add_argument('--logfile', dest='logfile', parser.add_argument('--logfile', dest='logfile',
help='defaults to ~/.wakatime.log') help='defaults to ~/.wakatime.log')
parser.add_argument('--config', dest='config', parser.add_argument('--config', dest='config',
@ -82,32 +161,59 @@ def parseArguments(argv):
parser.add_argument('--verbose', dest='verbose', action='store_true', parser.add_argument('--verbose', dest='verbose', action='store_true',
help='turns on debug messages in log file') help='turns on debug messages in log file')
parser.add_argument('--version', action='version', version=__version__) parser.add_argument('--version', action='version', version=__version__)
# parse command line arguments
args = parser.parse_args(args=argv[1:]) args = parser.parse_args(args=argv[1:])
# use current unix epoch timestamp by default
if not args.timestamp: if not args.timestamp:
args.timestamp = time.time() args.timestamp = time.time()
# parse ~/.wakatime.cfg file
configs = parseConfigFile(args.config)
if configs is None:
return args, configs
# update args from configs
if not args.key: if not args.key:
default_key = get_api_key(args.config) default_key = None
if configs.has_option('settings', 'api_key'):
default_key = configs.get('settings', 'api_key')
if default_key: if default_key:
args.key = default_key args.key = default_key
else: else:
parser.error('Missing api key') parser.error('Missing api key')
return args if not args.ignore:
args.ignore = []
if configs.has_option('settings', 'ignore'):
try:
for pattern in configs.get('settings', 'ignore').split("\n"):
if pattern.strip() != '':
args.ignore.append(pattern)
except TypeError:
pass
if not args.verbose and configs.has_option('settings', 'verbose'):
args.verbose = configs.getboolean('settings', 'verbose')
if not args.verbose and configs.has_option('settings', 'debug'):
args.verbose = configs.getboolean('settings', 'debug')
if not args.logfile and configs.has_option('settings', 'logfile'):
args.logfile = configs.get('settings', 'logfile')
return args, configs
def get_api_key(configFile): def should_ignore(fileName, patterns):
if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
api_key = None
try: try:
cf = open(configFile) for pattern in patterns:
for line in cf: try:
line = line.split('=', 1) compiled = re.compile(pattern, re.IGNORECASE)
if line[0] == 'api_key': if compiled.search(fileName):
api_key = line[1].strip() return pattern
cf.close() except re.error as ex:
except IOError: log.warning('Regex error (%s) for ignore pattern: %s' % (str(ex), pattern))
print('Error: Could not read from config file.') except TypeError:
return api_key pass
return False
def get_user_agent(plugin): def get_user_agent(plugin):
@ -187,25 +293,39 @@ def send_action(project=None, branch=None, stats={}, key=None, targetFile=None,
def main(argv=None): def main(argv=None):
if not argv: if not argv:
argv = sys.argv argv = sys.argv
args = parseArguments(argv)
args, configs = parseArguments(argv)
if configs is None:
return 103 # config file parsing error
setup_logging(args, __version__) setup_logging(args, __version__)
ignore = should_ignore(args.targetFile, args.ignore)
if ignore is not False:
log.debug('File ignored because matches pattern: %s' % ignore)
return 0
if os.path.isfile(args.targetFile): if os.path.isfile(args.targetFile):
branch = None
name = None
stats = get_file_stats(args.targetFile) stats = get_file_stats(args.targetFile)
project = find_project(args.targetFile)
project = find_project(args.targetFile, configs=configs)
branch = None
project_name = None
if project: if project:
branch = project.branch() branch = project.branch()
name = project.name() project_name = project.name()
if send_action( if send_action(
project=name, project=project_name,
branch=branch, branch=branch,
stats=stats, stats=stats,
**vars(args) **vars(args)
): ):
return 0 return 0 # success
return 102
return 102 # api error
else: else:
log.debug('File does not exist; ignoring this action.') log.debug('File does not exist; ignoring this action.')
return 101 return 0

View File

@ -11,6 +11,7 @@
import logging import logging
import os import os
import sys
from .packages import simplejson as json from .packages import simplejson as json
try: try:
@ -25,7 +26,13 @@ class CustomEncoder(json.JSONEncoder):
if isinstance(obj, bytes): if isinstance(obj, bytes):
obj = bytes.decode(obj) obj = bytes.decode(obj)
return json.dumps(obj) return json.dumps(obj)
return super(CustomEncoder, self).default(obj) try:
encoded = super(CustomEncoder, self).default(obj)
except UnicodeDecodeError:
encoding = sys.getfilesystemencoding()
obj = obj.decode(encoding, 'ignore').encode('utf-8')
encoded = super(CustomEncoder, self).default(obj)
return encoded
class JsonFormatter(logging.Formatter): class JsonFormatter(logging.Formatter):
@ -69,7 +76,7 @@ def setup_logging(args, version):
logger = logging.getLogger() logger = logging.getLogger()
set_log_level(logger, args) set_log_level(logger, args)
if len(logger.handlers) > 0: if len(logger.handlers) > 0:
formatter = JsonFormatter(datefmt='%a %b %d %H:%M:%S %Z %Y') formatter = JsonFormatter(datefmt='%Y/%m/%d %H:%M:%S %z')
formatter.setup( formatter.setup(
timestamp=args.timestamp, timestamp=args.timestamp,
isWrite=args.isWrite, isWrite=args.isWrite,
@ -83,7 +90,7 @@ def setup_logging(args, version):
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))
formatter = JsonFormatter(datefmt='%a %b %d %H:%M:%S %Z %Y') formatter = JsonFormatter(datefmt='%Y/%m/%d %H:%M:%S %z')
formatter.setup( formatter.setup(
timestamp=args.timestamp, timestamp=args.timestamp,
isWrite=args.isWrite, isWrite=args.isWrite,

View File

@ -14,21 +14,32 @@ import os
from .projects.git import Git from .projects.git import Git
from .projects.mercurial import Mercurial from .projects.mercurial import Mercurial
from .projects.projectmap import ProjectMap
from .projects.subversion import Subversion from .projects.subversion import Subversion
from .projects.wakatime import WakaTime
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# List of plugin classes to find a project for the current file path.
# Project plugins will be processed with priority in the order below.
PLUGINS = [ PLUGINS = [
WakaTime,
ProjectMap,
Git, Git,
Mercurial, Mercurial,
Subversion, Subversion,
] ]
def find_project(path): def find_project(path, configs=None):
for plugin in PLUGINS: for plugin in PLUGINS:
project = plugin(path) plugin_name = plugin.__name__.lower()
plugin_configs = None
if configs and configs.has_section(plugin_name):
plugin_configs = dict(configs.items(plugin_name))
project = plugin(path, configs=plugin_configs)
if project.process(): if project.process():
return project return project
return None return None

View File

@ -22,18 +22,19 @@ class BaseProject(object):
be found for the current path. be found for the current path.
""" """
def __init__(self, path): def __init__(self, path, configs=None):
self.path = path self.path = path
self._configs = configs
def type(self): def project_type(self):
""" Returns None if this is the base class. """ Returns None if this is the base class.
Returns the type of project if this is a Returns the type of project if this is a
valid project. valid project.
""" """
type = self.__class__.__name__.lower() project_type = self.__class__.__name__.lower()
if type == 'baseproject': if project_type == 'baseproject':
type = None project_type = None
return type return project_type
def process(self): def process(self):
""" Processes self.path into a project and """ Processes self.path into a project and

View File

@ -25,35 +25,32 @@ log = logging.getLogger(__name__)
class Git(BaseProject): class Git(BaseProject):
def process(self): def process(self):
self.config = self._find_config(self.path) self.configFile = self._find_git_config_file(self.path)
if self.config: return self.configFile is not None
return True
return False
def name(self): def name(self):
base = self._project_base() base = self._project_base()
if base: if base:
return os.path.basename(base) return unicode(os.path.basename(base))
return None return None
def branch(self): def branch(self):
branch = None
base = self._project_base() base = self._project_base()
if base: if base:
head = os.path.join(self._project_base(), '.git', 'HEAD') head = os.path.join(self._project_base(), '.git', 'HEAD')
try: try:
with open(head) as f: with open(head) as fh:
branch = f.readline().strip().rsplit('/', 1)[-1] return unicode(fh.readline().strip().rsplit('/', 1)[-1])
except IOError: except IOError:
pass pass
return branch
def _project_base(self):
if self.config:
return os.path.dirname(os.path.dirname(self.config))
return None return None
def _find_config(self, path): def _project_base(self):
if self.configFile:
return os.path.dirname(os.path.dirname(self.configFile))
return None
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):
path = os.path.split(path)[0] path = os.path.split(path)[0]
@ -62,34 +59,4 @@ class Git(BaseProject):
split_path = os.path.split(path) split_path = os.path.split(path)
if split_path[1] == '': if split_path[1] == '':
return None return None
return self._find_config(split_path[0]) return self._find_git_config_file(split_path[0])
def _parse_config(self):
sections = {}
try:
f = open(self.config, 'r')
except IOError as e:
log.exception("Exception:")
else:
with f:
section = None
for line in f.readlines():
line = line.lstrip()
if len(line) > 0 and line[0] == '[':
section = line[1:].split(']', 1)[0]
temp = section.split(' ', 1)
section = temp[0].lower()
if len(temp) > 1:
section = ' '.join([section, temp[1]])
sections[section] = {}
else:
try:
(setting, value) = line.split('=', 1)
except ValueError:
setting = line.split('#', 1)[0].split(';', 1)[0]
value = 'true'
setting = setting.strip().lower()
value = value.split('#', 1)[0].split(';', 1)[0].strip()
sections[section][setting] = value
f.close()
return sections

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.projectmap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the ~/.wakatime.cfg file to set custom project names by
recursively matching folder paths.
Project maps go under the [projectmap] config section.
For example:
[projectmap]
/home/user/projects/foo = new project name
/home/user/projects/bar = project2
Will result in file `/home/user/projects/foo/src/main.c` to have
project name `new project name`.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .base import BaseProject
log = logging.getLogger(__name__)
class ProjectMap(BaseProject):
def process(self):
if not self._configs:
return False
self.project = self._find_project(self.path)
return self.project is not None
def _find_project(self, path):
path = os.path.realpath(path)
if os.path.isfile(path):
path = os.path.split(path)[0]
if self._configs.get(path.lower()):
return self._configs.get(path.lower())
if self._configs.get('%s/' % path.lower()):
return self._configs.get('%s/' % path.lower())
if self._configs.get('%s\\' % path.lower()):
return self._configs.get('%s\\' % path.lower())
split_path = os.path.split(path)
if split_path[1] == '':
return None
return self._find_project(split_path[0])
def branch(self):
return None
def name(self):
if self.project:
return unicode(self.project)
return None

View File

@ -30,13 +30,12 @@ class Subversion(BaseProject):
return self._find_project_base(self.path) return self._find_project_base(self.path)
def name(self): def name(self):
return self.info['Repository Root'].split('/')[-1] return unicode(self.info['Repository Root'].split('/')[-1])
def branch(self): def branch(self):
branch = None
if self.base: if self.base:
branch = os.path.basename(self.base) unicode(os.path.basename(self.base))
return branch return None
def _get_info(self, path): def _get_info(self, path):
info = OrderedDict() info = OrderedDict()

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
"""
wakatime.projects.wakatime
~~~~~~~~~~~~~~~~~~~~~~~~~~
Information from a .wakatime-project file about the project for
a given file. First line of .wakatime-project sets the project
name. Second line sets the current branch name.
:copyright: (c) 2013 Alan Hamlett.
:license: BSD, see LICENSE for more details.
"""
import logging
import os
from .base import BaseProject
log = logging.getLogger(__name__)
class WakaTime(BaseProject):
def process(self):
self.config = self._find_config(self.path)
self._project_name = None
self._project_branch = None
if self.config:
try:
with open(self.config) as fh:
self._project_name = unicode(fh.readline().strip())
self._project_branch = unicode(fh.readline().strip())
except IOError as e:
log.exception("Exception:")
return True
return False
def name(self):
return self._project_name
def branch(self):
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

@ -25,7 +25,10 @@ log = logging.getLogger(__name__)
# force file name extensions to be recognized as a certain language # force file name extensions to be recognized as a certain language
EXTENSIONS = { EXTENSIONS = {
'j2': 'HTML',
'markdown': 'Markdown',
'md': 'Markdown', 'md': 'Markdown',
'twig': 'Twig',
} }
TRANSLATIONS = { TRANSLATIONS = {
'CSS+Genshi Text': 'CSS', 'CSS+Genshi Text': 'CSS',
@ -35,6 +38,7 @@ TRANSLATIONS = {
'JavaScript+Genshi Text': 'JavaScript', 'JavaScript+Genshi Text': 'JavaScript',
'JavaScript+Lasso': 'JavaScript', 'JavaScript+Lasso': 'JavaScript',
'Perl6': 'Perl', 'Perl6': 'Perl',
'RHTML': 'HTML',
} }