timetracker /

#!/usr/bin/env python

# Copyright 2010 Eduardo Robles Elvira <>
# This software may be used and distributed according to the terms of the
# GNU General Public License version 3 or any later version, incorporated
# herein by reference.


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
            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:
        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()"timetracker"))
    if not config.has_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')

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 =
        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"] =
        saveconfig(config, extra, repo)

    elif opts['stop'] and extra.get("time-spent-start", False):
        start = extra.pop("time-spent-start")
        delta = - start
        if extra.get("time-spent", False):
            extra["time-spent"] += delta
            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:

            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
                    summary_data[commit.user()] = time_delta

            for parent in commit.parents():
        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 = - start
        if extra.get("time-spent", False):
            extra["time-spent"] += delta
            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
    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/ 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')],