Index: htdocs/css/roadmap.css
===================================================================
--- htdocs/css/roadmap.css	(revision 2523)
+++ htdocs/css/roadmap.css	(working copy)
@@ -1,15 +1,15 @@
 /* General styles for the progress bars */
 div.progress { border: 1px solid #d7d7d7; float: left }
-div.progress :link, div.progress :visited {
+div.progress :link, div.progress :visited, div.progress div {
  background: #fff;
  border: none;
  display: block;
  float: left;
  height: 1.2em;
 }
-div.progress :link:hover, div.progress :visited:hover { background: #fff }
-div.progress .closed:link, div.progress .closed:visited { background: #bae0ba }
-p.percent { font-size: 10px; line-height: 2.4em; margin: 0.9em 0 0 }
+div.progress :link:hover, div.progress :visited:hover, div.progress div { background: #fff }
+div.progress .closed:link, div.progress .closed:visited, div.progress div.closed { background: #bae0ba }
+p.percent { font-size: 10px; line-height: 2.4em; margin: 0.9em 0 0; }
 
 /* Styles for the roadmap view */
 ul.milestones { margin: 2em 0 0; padding: 0 }
Index: htdocs/css/ticket.css
===================================================================
--- htdocs/css/ticket.css	(revision 2523)
+++ htdocs/css/ticket.css	(working copy)
@@ -66,12 +66,22 @@
  width: 45%;
 }
 #properties .col2 { margin-left: 40% }
-#properties .main label, #properties .col1 label, #properties .col2 label {
- float:left;
- width: 7em;
+#properties .main label, #properties .col1 label, #properties .col2 label,
+#properties .custom .field label { 
+ float: left;
+ width: 9em;
  text-align: right;
  margin-right: .5em;
 }
+
+#properties .custom .field fieldset.radio label { 
+ width: auto;
+}
+
+#properties .custom .field fieldset.custom_radio { 
+ border: none;
+}
+
 #properties .custom {
  clear: left;
  border-top: 1px dotted #d7d7d7;
Index: trac/ticket/roadmap.py
===================================================================
--- trac/ticket/roadmap.py	(revision 2523)
+++ trac/ticket/roadmap.py	(working copy)
@@ -29,19 +29,47 @@
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider
 
-def get_tickets_for_milestone(env, db, milestone, field='component'):
+def parse_time(pattern):
+    match = re.search(r'(-?[0-9]*(\.[0-9]*)?).?(m|min|h)', str(pattern))
+    if match:
+        value = match.group(1)
+        unit = match.group(3)
+
+        if (unit == 'm' or unit == 'min'):
+            value = float(value) / 60.0
+        return float(value)
+    return 0.0
+
+def get_tickets_for_milestone(env, db, milestone, fields=[ 'component' ]):
     cursor = db.cursor()
-    fields = TicketSystem(env).get_ticket_fields()
-    if field in [f['name'] for f in fields if not f.get('custom')]:
-        cursor.execute("SELECT id,status,%s FROM ticket WHERE milestone=%%s "
-                       "ORDER BY %s" % (field, field), (milestone,))
-    else:
-        cursor.execute("SELECT id,status,value FROM ticket LEFT OUTER "
-                       "JOIN ticket_custom ON (id=ticket AND name=%s) "
-                       "WHERE milestone=%s ORDER BY value", (field, milestone))
+    stdfields = TicketSystem(env).get_ticket_fields()
+    sql = "SELECT DISTINCT "
+    for field in fields:
+        if field not in [f['name'] for f in stdfields if not f.get('custom')]:
+            sql += "%s.value AS %s, " % (field, field)
+        else:
+            sql += "ticket.%s AS %s, " % (field, field)
+    sql += "ticket.id AS id, ticket.status AS status FROM ticket "
+    for field in fields:
+        if field not in [f['name'] for f in stdfields if not f.get('custom')]:
+            sql += "LEFT OUTER JOIN ticket_custom %s ON (ticket.id=%s.ticket AND %s.name='%s') " % (field, field, field, field)
+    sql += "WHERE milestone='%s' ORDER BY %s" % (milestone, field)
+
+    env.log.warn("Executing '%s'" % (sql))
+    cursor.execute(sql)
+
     tickets = []
-    for tkt_id, status, fieldval in cursor:
-        tickets.append({'id': tkt_id, 'status': status, field: fieldval})
+    while 1:
+        row = cursor.fetchone()
+        if not row:
+            break
+        ticket = {
+            'id': int(row['id']),
+            'status': row['status'],
+        }
+        for field in fields:
+            ticket[field] = row[field]
+        tickets.append(ticket)
     return tickets
 
 def get_query_links(env, milestone, grouped_by='component', group=None):
@@ -75,12 +103,36 @@
         if percent_active + percent_closed > 100:
             percent_closed -= 1
 
+    estimated_work = 0.0
+    spent_work = 0.0
+    remaining_work = 0.0
+    for ticket in tickets:
+        if ticket['status'] != 'closed':
+            if (ticket.has_key('tt_remaining')):
+                remaining_work += parse_time(ticket['tt_remaining'])
+            elif (ticket.has_key('tt_estimated')):
+                remaining_work += parse_time(ticket['tt_estimated'])
+        if (ticket.has_key('tt_spent')):
+            spent_work += parse_time(ticket['tt_spent'])
+        if (ticket.has_key('tt_estimated')):
+            estimated_work += parse_time(ticket['tt_estimated'])
+
+    work_percent_complete = 0
+    if spent_work > 0:
+        work_percent_complete = float(spent_work) / float(spent_work + remaining_work) * 100
+    work_percent_remaining = 100 - work_percent_complete
+
     return {
         'total_tickets': total_cnt,
         'active_tickets': active_cnt,
         'percent_active': percent_active,
         'closed_tickets': closed_cnt,
-        'percent_closed': percent_closed
+        'percent_closed': percent_closed,
+        'estimated_work' : estimated_work,
+        'spent_work' : spent_work,
+        'remaining_work' : remaining_work,
+        'work_percent_complete': work_percent_complete,
+        'work_percent_remaining': work_percent_remaining
     }
 
 def milestone_to_hdf(env, db, req, milestone):
@@ -158,7 +210,7 @@
         for idx,milestone in enum(milestones):
             prefix = 'roadmap.milestones.%d.' % idx
             tickets = get_tickets_for_milestone(self.env, db, milestone['name'],
-                                                'owner')
+                                                [ 'owner', 'tt_estimated', 'tt_remaining', 'tt_spent' ])
             req.hdf[prefix + 'stats'] = calc_ticket_stats(tickets)
             for k, v in get_query_links(self.env, milestone['name']).items():
                 req.hdf[prefix + 'queries.' + k] = escape(v)
@@ -465,7 +517,7 @@
             by = req.args.get('by', available_groups[0]['name'])
         req.hdf['milestone.stats.grouped_by'] = by
 
-        tickets = get_tickets_for_milestone(self.env, db, milestone.name, by)
+        tickets = get_tickets_for_milestone(self.env, db, milestone.name, [ by, 'tt_estimated', 'tt_remaining', 'tt_spent' ])
         stats = calc_ticket_stats(tickets)
         req.hdf['milestone.stats'] = stats
         for key, value in get_query_links(self.env, milestone.name).items():
Index: templates/roadmap.cs
===================================================================
--- templates/roadmap.cs	(revision 2523)
+++ templates/roadmap.cs	(working copy)
@@ -61,6 +61,24 @@
          var:stats.active_tickets ?></a></dd>
       </dl><?cs
      /if ?><?cs
+     if:#stats.spent_work > #0 ?>
+	  <div style="margin: 0; height: 1px;"></div>
+      <div class="progress">
+	    <div class="closed" style="width: <?cs 
+		  var:#stats.work_percent_complete ?>%"></div>
+	    <div class="open" style="width: <?cs
+		  var:#stats.work_percent_remaining ?>%"></div>
+      </div>
+      <p class="percent"><?cs var:#stats.work_percent_complete ?>%</p>     
+      <dl>
+       <dt>Estimated work:</dt>
+       <dd><?cs var:stats.estimated_work ?> h</dd>
+       <dt>Spent work:</dt>
+       <dd><?cs var:stats.spent_work ?> h</dd>
+       <dt>Remaining work:</dt>
+       <dd><?cs var:stats.remaining_work ?> h</dd>
+      </dl><?cs
+     /if ?><?cs
     /with ?>
    </div>
    <div class="description"><?cs var:milestone.description ?></div>
Index: templates/macros.cs
===================================================================
--- templates/macros.cs	(revision 2523)
+++ templates/macros.cs	(working copy)
@@ -131,39 +131,47 @@
  each c=ticket.custom ?>
   <div class="field custom_<?cs var c.name ?>"><?cs
    if c.type == 'text' ?>
-    <label>
-     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:
-     <input type="text" name="custom_<?cs var c.name ?>" value="<?cs var c.value ?>" />
-    </label><?cs
+    <label for="custom_<?cs var c.name ?>">
+     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:</label>
+     <input type="text" name="custom_<?cs var c.name ?>"
+      id="custom_<?cs var c.name ?>" value="<?cs var c.value ?>" />
+    <?cs
    elif c.type == 'textarea' ?>
-    <label>
-     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:<br />
-     <textarea cols="<?cs alt c.width ?>60<?cs /alt ?>" rows="<?cs
+    <label for="custom_<?cs var c.name ?>">
+     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:<br /></label>
+     <textarea id="custom_<?cs var c.name ?>"
+       cols="<?cs alt c.width ?>60<?cs /alt ?>" rows="<?cs
        alt c.height ?>12<?cs /alt ?>" name="custom_<?cs var c.name ?>"><?cs
        var c.value ?></textarea>
-    </label><?cs
+    <?cs
    elif c.type == 'checkbox' ?>
     <input type="hidden" name="checkbox_<?cs var c.name ?>" />
-    <label>
-     <input type="checkbox" name="custom_<?cs var c.name ?>" value="1"<?cs
+    <label for="custom_<?cs var c.name ?>">&nbsp;</label>
+     <input type="checkbox" name="custom_<?cs var c.name ?>" 
+       id="custom_<?cs var c.name ?>" value="1"<?cs
        if c.selected ?> checked="checked"<?cs /if ?> />
      <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>
-    </label><?cs
+    <?cs
    elif c.type == 'select' ?>
-    <label>
-     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:
-     <select name="custom_<?cs var c.name ?>"><?cs each v = c.option ?>
+    <label for="custom_<?cs var c.name ?>">
+     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:</label>
+     <select name="custom_<?cs var c.name ?>" 
+      id="custom_<?cs var c.name ?>"><?cs each v = c.option ?>
       <option<?cs if v.selected ?> selected="selected"<?cs /if ?>><?cs
         var v ?></option><?cs /each ?>
      </select>
-    </label><?cs
+    <?cs
    elif c.type == 'radio' ?>
-    <fieldset class="radio">
-     <legend><?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:</legend><?cs
+    <label for="custom_<?cs var c.name ?>"><?cs alt c.label ?><?cs var
+     c.name ?><?cs /alt ?>:</label>
+    <fieldset class="radio" id="custom_<?cs var c.name ?>"><?cs
      each v = c.option ?>
-      <label><input type="radio" name="custom_<?cs var c.name ?>" value="<?cs
+      <label for="custom_<?cs var c.name ?>_<?cs var v ?>">
+       <input type="radio" id="custom_<?cs var c.name ?>_<?cs var v ?>"
+         name="custom_<?cs var c.name ?>" value="<?cs
          var v ?>"<?cs if v.selected ?> checked="checked"<?cs /if ?> /> <?cs
-         var v ?></label><?cs
+         var v ?>
+      </label><?cs
      /each ?>
     </fieldset><?cs
    /if ?>
Index: templates/milestone.cs
===================================================================
--- templates/milestone.cs	(revision 2523)
+++ templates/milestone.cs	(working copy)
@@ -141,6 +141,24 @@
         var:stats.active_tickets ?></a></dd>
      </dl><?cs
     /if ?><?cs
+   if:#stats.spent_work > #0 ?>
+	<div style="margin: 0; height: 1px;"></div>
+	<div class="progress">
+	 <div class="closed" style="width: <?cs 
+		var:#stats.work_percent_complete ?>%"></div>
+	 <div class="open" style="width: <?cs
+		var:#stats.work_percent_remaining ?>%"></div>
+	</div>
+	<p class="percent"><?cs var:#stats.work_percent_complete ?>%</p>     
+	<dl>
+	 <dt>Estimated work:</dt>
+	 <dd><?cs var:stats.estimated_work ?> h</dd>
+	 <dt>Spent work:</dt>
+	 <dd><?cs var:stats.spent_work ?> h</dd>
+	 <dt>Remaining work:</dt>
+	 <dd><?cs var:stats.remaining_work ?> h</dd>
+	</dl><?cs
+	/if ?><?cs
    /with ?>
   </div>
   <form id="stats" action="" method="get">
Index: contrib/trac-post-commit-hook
===================================================================
--- contrib/trac-post-commit-hook	(revision 2523)
+++ contrib/trac-post-commit-hook	(working copy)
@@ -76,6 +76,7 @@
 from trac.env import open_environment
 from trac.Notify import TicketNotifyEmail
 from trac.ticket import Ticket
+from trac.ticket.roadmap import parse_time
 from trac.web.href import Href
 
 try:
@@ -115,6 +116,8 @@
 
 commandPattern = re.compile(leftEnv + r'(?P<action>[A-Za-z]*).?(?P<ticket>#[0-9]+(?:(?:[, &]*|[ ]?and[ ]?)#[0-9]+)*)' + rghtEnv)
 ticketPattern = re.compile(r'#([0-9]*)')
+spentPattern = re.compile(r'(spent|sp) ([0-9]*)(m|min|h)')
+remainingPattern = re.compile(r'(remaining|rem) ([0-9]*)(m|min|h)')
 
 class CommitHook:
     _supported_cmds = {'close':      '_cmdClose',
@@ -141,16 +144,43 @@
         self.env.href = Href(url)
         self.env.abs_href = Href(url)
 
+        self.time_spent = -1
+        groups = spentPattern.findall(msg) 
+        for cmd, value, unit in groups:
+			self.time_spent = parse_time(value + unit)
+				
+        self.time_remaining = -1
+        groups = remainingPattern.findall(msg) 
+        for cmd, value, unit in groups:
+			self.time_remaining = parse_time(value + unit)
+
         cmdGroups = commandPattern.findall(msg) 
         for cmd, tkts in cmdGroups:
             if CommitHook._supported_cmds.has_key(cmd.lower()):
                 func = getattr(self, CommitHook._supported_cmds[cmd.lower()])
                 func(ticketPattern.findall(tkts))
 
+    def _setCustomFields(self, ticket):
+		if (self.time_spent != -1):
+			if (ticket.values.has_key('tt_spent')):
+				ticket['tt_spent'] = str(parse_time(ticket['tt_spent']) + self.time_spent) + 'h'
+			else:
+				ticket['tt_spent'] = str(self.time_spent) + 'h'
+
+			if (ticket.values.has_key('tt_remaining')):
+				ticket['tt_remaining'] = str(parse_time(ticket['tt_remaining']) - self.time_spent) + 'h'
+			else:
+				if (ticket.values.has_key('tt_planned')):
+					ticket['tt_remaining'] = str(parse_time(ticket['tt_planned']) - self.time_spent) + 'h'
+
+		if (self.time_remaining != -1):
+			ticket['tt_remaining'] = str(self.time_remaining) + 'h'
+
     def _cmdClose(self, tickets):
         for tkt_id in tickets:
             try:
                 ticket = Ticket(self.env, tkt_id)
+                self._setCustomFields(ticket)
                 ticket['status'] = 'closed'
                 ticket['resolution'] = 'fixed'
                 ticket.save_changes(self.author, self.msg, self.now)
@@ -164,14 +194,15 @@
         for tkt_id in tickets: 
             try:
                 ticket = Ticket(self.env, tkt_id)
+                self._setCustomFields(ticket)
                 ticket.save_changes(self.author, self.msg, self.now)
                 tn = TicketNotifyEmail(self.env)
                 tn.notify(ticket, newticket=0, modtime=self.now)
             except Exception, e:
                 print>>sys.stderr, 'Unexpected error while processing ticket ' \
                                    'ID %s: %s' % (tkt_id, e)
+		
 
-
 if __name__ == "__main__":
     if len(sys.argv) < 5:
         print "For usage: %s --help" % (sys.argv[0])

