Source

timetracker / timetracker.py

#!/usr/bin/env python

'''timetracker

Tracks the time spent on a project.
'''

from mercurial import hg, extensions, commands, localrepo
from datetime import datetime, timedelta
import time
import ConfigParser

def timedelta_str(diff):
    """
    Returns string representing "time delta" e.g.
    3 days, 5 hours 3 minutes, etc.
    """
    if not diff:
        return "0 seconds"

    def int_sub(a, b):
        if b >= a:
            return 0
        else:
            return a - b

    periods = (
        (diff.seconds // 3600, "hour", "hours", 60),
        (diff.seconds // 60, "minute", "minutes", 60),
        (diff.seconds, "second", "seconds", 60),
    )

    ret_periods = []
    count = 0
    previous_period = None
    for period, singular, plural, factor in periods:
        if previous_period:
            period -= previous_period * previous_factor
        if period:
            ret_periods += ["%d %s" % (period, singular if period == 1 else plural)]
            count += 1
        if count >= 2:
            break
        previous_period = period
        previous_factor = factor

    return ", ".join(ret_periods)


def uisetup(ui):
    entry = extensions.wrapcommand(commands.table, 'commit', wrap_commit)
    extensions.wrapfunction(localrepo.localrepository, "commit", wrap_localrepo_commit)

def readconfig(repo):
    extra = dict()
    config = ConfigParser.SafeConfigParser()
    config.read(repo.join("timetracker"))
    if not config.has_section("main"):
        config.add_section("main")
    config_time_spent = None
    config_time_spent_start = None
    if config.has_option("main", "time-spent"):
        extra["time-spent"] = timedelta(seconds=int(config.get("main",
            "time-spent", "0")))
    if config.has_option("main", "time-spent-start"):
        extra["time-spent-start"] = datetime.fromtimestamp(float(config.get(
            "main", "time-spent-start", "0.0")))
    return extra, config

def saveconfig(config, extra, repo):
    if not extra.has_key("time-spent") and\
        config.has_option("main", "time-spent"):
        config.remove_option("main", "time-spent")
    elif extra.has_key("time-spent"):
        time_spent = str(int(extra["time-spent"].total_seconds()))
        config.set("main", "time-spent", time_spent)

    if not extra.has_key("time-spent-start") and\
        config.has_option("main", "time-spent-start"):
        config.remove_option("main", "time-spent-start")
    elif extra.has_key("time-spent-start"):
        time_spent_start = str(time.mktime(extra["time-spent-start"].timetuple()))
        config.set("main", "time-spent-start", str(time_spent_start))

    filep = repo.opener('timetracker', 'w')
    config.write(filep)
    filep.close()

def timetracker_cmd(ui, repo, *args, **opts):
    """Tracks the time spent on a project"""

    extra, config = readconfig(repo)

    def currently_spent_time():
        time_spent = extra.get("time-spent", timedelta(0))
        now = datetime.now()
        return time_spent + now - extra.get("time-spent-start", now)

    if opts['reset'] and extra.has_key("time-spent"):
        extra = dict()
        saveconfig(config, extra, repo)

    elif opts['start']:
        extra["time-spent-start"] = datetime.now()
        saveconfig(config, extra, repo)

    elif opts['stop'] and extra.get("time-spent-start", False):
        start = extra.pop("time-spent-start")
        delta = datetime.now() - start
        if extra.get("time-spent", False):
            extra["time-spent"] += delta
        else:
            extra["time-spent"] = delta
        saveconfig(config, extra, repo)

    elif opts['set']:
        extra = {"time-spent": timedelta(seconds=int(args[0])*60)}
        saveconfig(config, extra, repo)

    elif opts['current']:
        time_spent_current = currently_spent_time()
        time_str = timedelta_str(time_spent_current)
        print "time spent in next commit: %s" % time_str

    elif opts['summary']:

        summary_data = dict()
        def update_summary_data(commit):
            if commit.rev() == -1:
                return

            ci_extra = commit.extra()
            if ci_extra.has_key("time-spent"):
                time_delta = timedelta(seconds=int(ci_extra["time-spent"]))
                if summary_data.has_key(commit.user()):
                    summary_data[commit.user()] += time_delta
                else:
                    summary_data[commit.user()] = time_delta

            for parent in commit.parents():
                update_summary_data(parent)
        
        update_summary_data(repo[None])
        currently_total_timespent = currently_spent_time()
        total_time_spent = currently_total_timespent

        print "total time spent by authors"
        
        for username, time_spent in summary_data.items():
            total_time_spent = total_time_spent + time_spent
            time_spent_str = timedelta_str(time_spent)
            print "  %-48s %s" % (username, time_spent_str)

        print "\n%-50s %s" % ("total time spent in this project", timedelta_str(total_time_spent))

        next_time_str = timedelta_str(currently_total_timespent)
        print "%-50s %s" % ("time spent in next commit", next_time_str)



def wrap_commit(orig, ui, repo, *pats, **opts):
    '''
    Adds the timespent extra metadata to the changeset of this commit
    '''
    # do a stop
    extra, config = readconfig(repo)
    if extra.get("time-spent-start", False):
        start = extra.pop("time-spent-start")
        delta = datetime.now() - start
        if extra.get("time-spent", False):
            extra["time-spent"] += delta
        else:
            extra["time-spent"] = delta
        saveconfig(config, extra, repo)

    return orig(ui, repo, *pats, **opts)

def wrap_localrepo_commit(origfunc, self, *args, **kwargs):
    extra_config, config = readconfig(self)

    # only adds time-spent extra if timetracker was put to use for this commit
    if extra_config.get("time-spent", False):
        # serialize saving it as str
        extra_update = {"time-spent": str(int(extra_config["time-spent"].total_seconds()))}
        
        if not kwargs["extra"]:
            kwargs["extra"] = extra_update
        else:
            kwargs["extra"].update(extra_update)
    ret = origfunc(self, *args, **kwargs)

    extra_config = {} # starts again from zero, once the commit has been done
    saveconfig(config, extra_config, self)

    return ret

cmdtable = {
    # cmd name        function call
    "timetracker": (timetracker_cmd,
        # see mercurial/fancyopts.py for all of the command
        # flag options.
        [('s', 'start', None, 'start/continue counting time spent'),
        ('p', 'stop', None, 'stop/pause counting time spent'),
        ('r', 'reset', None, 'continue counting time spent'),
        ('e', 'set', None, 'set time spent in minutes'),
        ('c', 'current', None, 'time spent currently in next commit'),
        ('u', 'summary', None, 'show summary of total time spent')],
        "[options]")
}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.