diff -uNr trac.old/htdocs/css/ticket.css trac/htdocs/css/ticket.css
--- trac.old/htdocs/css/ticket.css	2008-08-07 03:00:20.000000000 +0200
+++ trac/htdocs/css/ticket.css	2010-04-23 10:35:09.000000000 +0200
@@ -74,6 +74,9 @@
 }
 
 #changelog { border: 1px outset #996; padding: 1em }
+#changelog .changelogbuttons {
+ display: inline;
+}
 #preview { border: 1px solid #d7d7d7; padding: 1em }
 #preview h3, #changelog h3 {
  border-bottom: 1px solid #d7d7d7;
diff -uNr trac.old/ticket/api.py trac/ticket/api.py
--- trac.old/ticket/api.py	2008-08-07 03:00:20.000000000 +0200
+++ trac/ticket/api.py	2010-04-23 10:20:14.000000000 +0200
@@ -285,10 +285,12 @@
     def get_permission_actions(self):
         return ['TICKET_APPEND', 'TICKET_CREATE', 'TICKET_CHGPROP',
                 'TICKET_VIEW', 'TICKET_EDIT_CC', 'TICKET_EDIT_DESCRIPTION',
+                'TICKET_EDIT_COMMENT', 
                 ('TICKET_MODIFY', ['TICKET_APPEND', 'TICKET_CHGPROP']),
                 ('TICKET_ADMIN', ['TICKET_CREATE', 'TICKET_MODIFY',
                                   'TICKET_VIEW', 'TICKET_EDIT_CC',
-                                  'TICKET_EDIT_DESCRIPTION'])]
+                                  'TICKET_EDIT_DESCRIPTION',
+                                  'TICKET_DELETE_COMMENT'])]
 
     # IWikiSyntaxProvider methods
 
diff -uNr trac.old/ticket/model.py trac/ticket/model.py
--- trac.old/ticket/model.py	2008-08-07 03:00:20.000000000 +0200
+++ trac/ticket/model.py	2010-04-23 10:29:39.000000000 +0200
@@ -336,6 +336,15 @@
         for listener in TicketSystem(self.env).change_listeners:
             listener.ticket_deleted(self)
 
+    def edit_comment(self, cmt, ts, db=None):
+        db, handle_ta = self._get_db_for_write(db)
+        cursor = db.cursor()
+
+        cursor.execute("UPDATE ticket_change SET newvalue=%s "
+                       "WHERE ticket=%s AND time=%s AND field='comment'",
+                       (cmt, self.id, ts))
+        if handle_ta:
+            db.commit()
 
 def simplify_whitespace(name):
     """Strip spaces and remove duplicate spaces within names"""
diff -uNr trac.old/ticket/templates/ticket.html trac/ticket/templates/ticket.html
--- trac.old/ticket/templates/ticket.html	2008-08-07 03:00:20.000000000 +0200
+++ trac/ticket/templates/ticket.html	2010-04-23 10:34:01.000000000 +0200
@@ -49,7 +49,7 @@
       <a href="#comment:$cnum"><small>$prefix$cnum</small></a>
     </py:def>
 
-    <py:def function="display_change(change)">
+    <py:def function="display_change(change, edit_cnum=0)">
       <ul py:if="change.fields" class="changes">
         <li py:for="field_name, field in change.fields.items()">
           <strong>${field_name}</strong>
@@ -70,8 +70,14 @@
           </py:choose>
         </li>
       </ul>
-      <div py:if="'comment' in change" class="comment searchable" xml:space="preserve">
-        ${wiki_to_html(context, change.comment, escape_newlines=preserve_newlines)}
+      <div py:if="'comment' in change and (str(change.cnum) != cnum_edit or comment_preview)" class="comment searchable ${(comment_preview and cnum_edit == str(change.cnum)) and 'ticketdraft'}" xml:space="preserve">
+        <py:choose test="comment_preview and cnum_edit == str(change.cnum)">
+          <py:when>${wiki_to_html(context, comment_preview, escape_newlines=preserve_newlines)}</py:when>
+          <py:otherwise>${wiki_to_html(context, change.comment, escape_newlines=preserve_newlines)}</py:otherwise>
+        </py:choose>
+        <py:if test="change.comment_edited and not (comment_preview and cnum_edit == str(change.cnum))">
+          ${wiki_to_html(context, change.comment_edited, escape_newlines=preserve_newlines)}
+        </py:if>
       </div>
     </py:def>
 
@@ -202,7 +208,7 @@
         <py:if test="ticket.exists and changes">
           <h2>Change History</h2>
           <div id="changelog">
-            <form py:for="change in changes" method="get" action="#comment" class="printableform">
+            <py:for each="change in changes" class="printableform">
               <div class="change">
                 <h3 class="change" id="${'cnum' in change and 'comment:%d' % change.cnum or None}">
                   <span class="threading" py:if="replies and 'cnum' in change"
@@ -221,13 +227,36 @@
                   </span>
                   Changed ${dateinfo(change.date)} ago by ${authorinfo(change.author)}
                 </h3>
-                <div py:if="'cnum' in change and 'TICKET_APPEND' in perm(ticket.resource)" class="inlinebuttons">
-                  <input type="hidden" name="replyto" value="${change.cnum}" />
-                  <input type="submit" value="Reply" title="Reply to comment ${change.cnum}" />
+                <div class="inlinebuttons">
+                  <form py:if="'cnum' in change and 'TICKET_APPEND' in perm(ticket.resource)"
+                        method="get" action="#comment" class="changelogbuttons">
+                    <input type="hidden" name="replyto" value="${change.cnum}" />
+                    <input type="submit" value="${_('Reply')}" title="Reply to comment ${change.cnum}" />
+                  </form>
+                  <form py:if="'TICKET_EDIT_COMMENT' in perm(ticket.resource) and
+                               (authname == change.author or 'TRAC_ADMIN' in perm(ticket.resource))"
+                        method="get" action="#comment:${change.cnum}" class="changelogbuttons">
+                    <input type="hidden" name="cnum_edit" value="${change.cnum}" />
+                    <input type="submit" value="${_('Edit')}" title="Edit comment ${change.cnum}" />
+                  </form>
                 </div>
-                ${display_change(change)}
+                ${display_change(change, ('TICKET_EDIT_COMMENT' in perm(ticket.resource) and cnum_edit or 0))}
+                <form method="post" py:if="'TICKET_EDIT_COMMENT' in perm(ticket.resource) and str(change.cnum) == cnum_edit">
+                  <p>
+                    <py:choose test="comment_preview">
+                      <py:when><textarea name="edited_comment" class="wikitext" rows="10" cols="78">${comment_preview}</textarea></py:when>
+                      <py:otherwise><textarea name="edited_comment" class="wikitext" rows="10" cols="78">${change.comment}</textarea></py:otherwise>
+                    </py:choose>
+                  </p>
+                  <input type="hidden" name="edit_comment_when" value="${change.date.isoformat()}" />
+                  <input type="hidden" name="comment_author" value="${change.author}" />
+                  <input type="hidden" name="comment_num" value="${change.cnum}" />
+                  <input type="submit" name="preview_comment" value="${_('Preview')}" title='Preview changes to comment ${change.cnum}' />
+                  <input type="submit" name="edit_comment" value="${_('Submit changes')}" title="Submit changes to comment ${change.cnum}" />
+                  <input type="submit" name="cancel_comment" value="${_('Cancel')}" title="Cancel comment edit" />
+                </form>
               </div>
-            </form>
+            </py:for>
           </div>
         </py:if>
       </py:if>
diff -uNr trac.old/ticket/web_ui.py trac/ticket/web_ui.py
--- trac.old/ticket/web_ui.py	2008-08-07 03:00:20.000000000 +0200
+++ trac/ticket/web_ui.py	2010-04-23 11:09:31.000000000 +0200
@@ -39,7 +39,8 @@
 from trac.timeline.api import ITimelineEventProvider
 from trac.util import get_reporter_id, partition
 from trac.util.compat import any
-from trac.util.datefmt import to_timestamp, utc
+from trac.util.datefmt import parse_date, pretty_timedelta, to_datetime, \
+                              format_datetime, to_timestamp, utc
 from trac.util.text import CRLF, shorten_line, obfuscate_email_address
 from trac.util.presentation import separated
 from trac.util.translation import _
@@ -451,7 +452,30 @@
                 return self._render_history(req, ticket, data, text_fields)
             elif action == 'diff':
                 return self._render_diff(req, ticket, data, text_fields)
-        elif req.method == 'POST': # 'Preview' or 'Submit'
+        # Comment edit preview is a special case, similar to 'View' action
+        elif (req.method == 'POST' and 'preview_comment' in req.args and
+              'TICKET_EDIT_COMMENT' in req.perm(ticket.resource)):
+              data['comment_preview'] = req.args.get('edited_comment')
+              data['cnum_edit'] = req.args.get('comment_num')
+              field_changes = {}
+        elif req.method == 'POST': # 'Preview' or 'Submit' or Comment Edit
+            # Cancel comment edit
+            if 'cancel_comment' in req.args:
+                req.redirect(req.href.ticket(ticket.id))
+            # Edit a ticket comment
+            elif ('edit_comment' in req.args and
+                  'TICKET_EDIT_COMMENT' in req.perm(ticket.resource)):
+                author = req.args.get('comment_author')
+                if (author == req.authname or
+                   'TRAC_ADMIN' in req.perm(ticket.resource)):
+                    comment = req.args.get('edited_comment')
+                    comment += "^^%s&%s^^" % (to_timestamp(datetime.now(utc)),
+                                              get_reporter_id(req, 'author'))
+                    when = req.args.get('edit_comment_when')
+                    when_ts = to_timestamp(parse_date(when, req.tz))
+                    ticket.edit_comment(comment, when_ts)
+                    req.redirect(req.href.ticket(ticket.id))
+
             # Do any action on the ticket?
             actions = TicketSystem(self.env).get_available_actions(
                 req, ticket)
@@ -1133,6 +1157,8 @@
         """Insert ticket data into the template `data`"""
         replyto = req.args.get('replyto')
         data['replyto'] = replyto
+        if req.args.get('cnum_edit'):
+            data['cnum_edit'] = req.args.get('cnum_edit')
         data['version'] = ticket.resource.version
         data['description_change'] = None
 
@@ -1353,6 +1379,18 @@
                     current['cnum'] = autonum
             # some common processing for fields
             if field == 'comment':
+                # find comments that have been edited and create pretty
+                # "edited by" messages for them
+                m = re.search('\^\^(.+?)\&(.+?)\^\^$', new)
+                if m:
+                    ts = int(m.group(1))
+                    user = m.group(2)
+                    verb = 'Edited'
+                    if re.search('^\^\^(.+?)\&(.+?)\^\^$', new):
+                        verb = 'Comment removed' # Comment is empty!
+                    current['comment_edited'] = ("\n\n''%s %s ago by %s.''"
+                        % (verb, pretty_timedelta(to_datetime(ts)), user))
+                    new = new[:m.start(0)]
                 current['comment'] = new
                 if old:
                     if '.' in old: # retrieve parent.child relationship
diff -uNr trac.old/util/datefmt.py trac/util/datefmt.py
--- trac.old/util/datefmt.py	2008-08-07 03:00:20.000000000 +0200
+++ trac/util/datefmt.py	2010-04-23 11:08:51.000000000 +0200
@@ -167,7 +167,7 @@
 
 _ISO_8601_RE = re.compile(r'(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d))?)?'   # date
                           r'(?:T(\d\d)(?::?(\d\d)(?::?(\d\d))?)?)?' # time
-                          r'(Z(?:([-+])?(\d\d):?(\d\d)?)?)?$'       # timezone
+                          r'(Z*?(?:([-+])?(\d\d):?(\d\d)?)?)?$'      # timezone
                           )
 
 def parse_date(text, tzinfo=None):
