tweakmsg /

Full commit
#!/usr/bin/env python
"""manipulate commit messages

 - Add comments to commit messages
 - Comments appear as a normal part of commit description

All message manipulations are versioned as part of the
.hgmsg file which works in a way similar to hg tags.

Author: John Mulligan <>
#   verify that commiting comments wont mess with pending merges
#   allow user to enter pipe/filename on cli for comment text
#   override/edit existing commit messages?

from mercurial import changelog, util
from mercurial.i18n import _
from mercurial.node import short
import os

_changelog_read =

def new_changelog_read(mangler):
    """monkeypatches a new function allowing
    access to the a messagemangler object
    def changelog_read(self, node):
        (manifest, user, t, files, desc, extra) = _changelog_read(self, node)
        desc = mangler.update(node, desc)
        return (manifest, user, t, files, desc, extra)
    return changelog_read

class messsagemangler(object):
    """commit message mangler/manipulator

    All commit messages are permanent in mercurial, but we can annotate
    the message and allow the user to see additions or updates.

    This update data will be kept as part of the repository just like
    tags in a file that should be human editable if need be.
    The file tracks changeset ids in the python cfg format.

    def __init__(self, ui, repo):
        self.ui = ui
        self.repo = repo
        self._source = self.ui.config('messagemangler', 'source', '.hgmsg')
        self.source = os.path.join(self.repo.root, self._source)
        self.config = None

    def readconfig(self):
        self.config = util.configparser()
        if os.path.isfile(self.source):

    def sections(self, node):
        prefix = '%s:' % node.encode('hex')
        chg = [ ss for ss in self.config.sections() if ss.startswith(prefix) ]
        return chg

    def comments(self, node, sep=None):
        if sep is None:
            sep = '--\n'
        comments = ['']
        for section in self.sections(node):
            msgtype = self.config.get(section, 'type')
            if msgtype not in 'comment note'.split():
            text = self.config.get(section, 'text')
            text = text.strip()
            if self.config.has_option(section, 'user'):
                text += '\n ~ %s' % self.config.get(section, 'user')
            text += '\n'
        return sep.join(comments)

    def update(self, node, desc):
        desc = '%s\n%s' % (desc, self.comments(node))
        return desc

    def addcomment(self, node, text, user=None):
        user = user or ui.username()
        node = node.encode('hex')
        index = 0
        while True:
            section = '%s:%d' % (node, index)
            if not self.config.has_section(section):
            index += 1
        self.config.set(section, 'type', 'comment')
        self.config.set(section, 'user', user)
        self.config.set(section, 'text', text)
            fp = open(self.source, 'wt')
        except IOError, e:
            raise util.Abort(e)
    def commit(self, repo, msg, user):
        files = [ self._source ]
        if self.source not in repo.dirstate:
        repo.commit(files, msg, user)

def reposetup(ui, repo):
    """monkeypatch in the message mangling read function only
    when the repo is setup
    if not repo.local():
    mangler = messsagemangler(ui, repo) = new_changelog_read(mangler)

def comment(ui, repo, rev=None, user=None, **kwargs):
    """add a comment to the current or given revision

    Comments are similar to tags, and are stored as a file
    named '.hgmsg'. This file is revisioned like other project files
    and can be hand-edited if needed.
    Comments will appear beneath the changeset description.

    If REV is not given the comment will be added for the tip.
    user = user or ui.username()
    node = repo.lookup(realrev(repo, rev))
    desc =[4]
    text = ui.edit(prepdesc(desc), user).strip()
    if not text:
        raise util.Abort(_('empty comment'))
    mangler = messsagemangler(ui, repo)
    mangler.addcomment(node, text, user)
    if kwargs['no_commit']:
        # done, if we're not commiting
    message = kwargs.get('message', '')
    if not message:
        mtxt = _('Added user comment for changeset %s')
        message = mtxt % short(repo.changectx(rev).node())
    mangler.commit(repo, message, user)

def prepdesc(desc):
    """what the user will see in the editor
    text = []
    text.append(_("HG: Enter a comment." 
                  "  Lines beginning with 'HG:' are removed."))
    text.append('HG: --')
    for line in desc.split('\n'):
        text.append('HG: %s' % line)

    return '\n\n%s\n' % '\n'.join(text)

def realrev(repo, rev):
    """try and fint the current revision
    if rev is not None:
        return rev
    branch = repo.workingctx().branch()
        return repo.branchtags()[branch]
    except KeyError:
        if branch == 'default':
            return 'tip'
        raise util.Abort(_('branch %s not found') % branch)

cmdtable = {
    "comment": (comment,
                ('u', 'user', '', _('user adding the comment')),
                ('m', 'message', '', _('commit message')),
                ('', 'no-commit', None, _("don't commit new comment")),
                "hg comment [-u USER] [REV]"),