tweakmsg / tweakmsg.py

#!/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
#   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 = changelog.changelog.read



def new_changelog_read(mangler):
    """monkeypatches a new changelog.read 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
        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):
    """monkeypatch in the message mangling read function only
    when the repo is setup
    """
    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):
    """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()
    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]"),
}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.