Source

tweakmsg / tweakmsg.py

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

Features:
 - 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 <phlogistonjohn@asynchrono.us>
"""
# TODO:
#   verify that commiting comments wont mess with pending merges
#   override/edit existing commit messages?


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

_changelog_read = changelog.changelog.read



def new_changelog_read(mangler):
    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):
    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
        self.readconfig()

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

    def sections(self, node):
        prefix = '%s:' % node.encode('hex')
        chg = [ ss for ss in self.config.sections() if ss.startswith(prefix) ]
        chg.sort()
        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():
                continue
            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'
            comments.append(text)
        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):
                break
            index += 1
        self.config.add_section(section)
        self.config.set(section, 'type', 'comment')
        self.config.set(section, 'user', user)
        self.config.set(section, 'text', text)
        try:
            fp = open(self.source, 'wt')
        except IOError, e:
            raise util.Abort(e)
        self.config.write(fp)
        fp.close()
        
    def commit(self, repo, msg, user):
        files = [ self._source ]
        if self.source not in repo.dirstate:
            repo.add(files)
        repo.commit(files, msg, user)
        

def reposetup(ui, repo):
    mangler = messsagemangler(ui, repo)
    changelog.changelog.read = 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 = repo.changelog.read(node)[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
        return
    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):
    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):
    if rev is not None:
        return rev
    branch = repo.workingctx().branch()
    try:
        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]"),
}