Source

hgext-branchident / branchident.py

# branchident extension for Mercurial
#
# Copyright 2009 Adrian Buehlmann <adrian@cadifra.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

'''separate branch identity from branch name

The branchident extension adds an extra field 'branchid' in changesets if
a branch name other than 'default' is committed (use 'hg log --debug' to
print the extra fields).

The branch id is calculated as the sha-1 hash of the concatenation of
date, username, branch name and the node id's of the parents. It is set on
first commit of a new branch name.

Changesets having the same branch id are considered to be of the same
branch.

The name of the branch is defined by the extra field 'branch' of the
changeset with the highest revision number in the repo having that branch
id. So the branch name of later changesets with the same branch id
override earlier names of a branch.

A branch is continued on commit if the first parent already has a branch
id. The new changeset gets the branch id of its first parent.

The branchident extension adds an additional option to the commit command:

    --rename-branch  rename the current branch

This option is to be used on commit after changing the branch name with
'hg branch <newname>' to rename the current branch. If --rename-branch is
not specified on commit of a changed branch name, then a new branch with
a new branch id is created.

To remove a branch name, just rename it to 'default' by doing:

    hg branch --force default
    hg ci --rename-branch

The branchident extension adds an additional option to the identify command:

    --branchid  show branch id
    
To show the branch id of a specific changeset use:

     hg identify --branchid --rev REV
'''

from mercurial.i18n import _
from mercurial.node import nullid, nullrev, hex, bin, short
from mercurial import util, context, error, extensions, commands, cmdutil
from mercurial import encoding, hg


def branchids(ui, repo, **opts):
    '''print branch ids with branch names

    Print all branch ids of the repository, together with their branch names.

    Note that branchids with branch names that have been renamed to 'default'
    are not shown (i.e. removed branch names are not shown).
    '''
    hexfunc = ui.debugflag and hex or short
    for id, bn in repo.branchids().iteritems():
        ui.write("%s %s\n" % (hexfunc(id), bn))


cmdtable = {
    '^branchids|brids': (branchids, [], ''),
}


def uisetup(ui):
    if not commands.table.get('^summary|sum'):
        # summary command was introduced in 1.4
        raise util.Abort(
            _('branchident extension needs Mercurial 1.4 or later'))

    entry = extensions.wrapcommand(commands.table, 'commit', bridcommit)
    entry[1].append(
        ('', 'rename-branch', None, _('rename the current branch')))

    entry = extensions.wrapcommand(commands.table, 'identify', brididentify)
    entry[1].append(
        ('', 'branchid', None, _('show branch id')))


def reposetup(ui, repo):

    if not repo.local():
        return

    repo._branchids = None
    repo._bridseen = nullid
    repo._bridseenrev = nullrev

    class branchidrepo(repo.__class__):

        def branchmap(self):
            origmap = super(branchidrepo, self).branchmap()

            # filter out overridden branch names
            newmap = {}
            for bn, heads in origmap.iteritems():
                okheads = []
                for head in heads:
                    curbn = self[head].branch()
                    if curbn == bn:
                        okheads.append(head)
                if okheads:
                    newmap[bn] = okheads

            return newmap

        def commitctx(self, ctx, error=False):
            if not ctx.extra().get('branchid'):
                pbrid = ctx.p1().extra().get('branchid')
                if pbrid:
                    ctx._extra['branchid'] = pbrid

            return super(branchidrepo, self).commitctx(ctx, error)

        def branchids(self):
            '''return dict that maps branch ids to branch names'''

            ids, seen, seenrev = (
                self._branchids, self._bridseen, self._bridseenrev)

            tip = self.changelog.tip()
            if (ids and seen == tip and self[seenrev].node() == seen):
                return ids

            ids, seen, seenrev = self._readbranchidscache()

            if seen == nullid:
                # scan changelog for branch ids, later ids override earlier ones
                for rev in self:
                    ctx = self[rev]
                    e = ctx.extra()
                    id = e.get('branchid')
                    if id:
                        bn = e['branch']
                        if bn == 'default':
                            # ignore/drop ids with branchname 'default'
                            if id in ids:
                                del ids[id]
                        else:
                            ids[id] = bn
                    seen = ctx.node()
                    seenrev = ctx.rev()

                self._writebranchidscache(ids, seen, seenrev)

            self._branchids, self._bridseen, self._bridseenrev = (
                ids, seen, seenrev)

            return ids

        def _writebranchidscache(self, ids, seen, seenrev):
            try:
                f = self.opener("branchids.cache", "w", atomictemp=True)
                f.write("%s %s\n" % (hex(seen), seenrev))
                for id, bn in ids.iteritems():
                    f.write("%s %s\n" % (hex(id), bn))
                f.rename()
            except (IOError, OSError):
                pass

        def _readbranchidscache(self):
            ids = {}
            try:
                f = self.opener("branchids.cache")
                lines = f.read().split('\n')
                f.close()
            except (IOError, OSError):
                return {}, nullid, nullrev

            try:
                seen, seenrev = lines.pop(0).split(" ", 1)
                seen, seenrev = bin(seen), int(seenrev)
                if seenrev != len(self)-1 or self[seenrev].node() != seen:
                    # invalidate the cache
                    raise ValueError(
                        'invalidating branchid cache (tip differs)')
                for l in lines:
                    if not l: continue
                    id, label = l.split(" ", 1)
                    id = bin(id)
                    ids[id] = label.strip()
            except KeyboardInterrupt:
                raise
            except Exception, inst:
                ids, seen, seenrev = {}, nullid, nullrev
            return ids, seen, seenrev

        def getbranchid(self, branch):
            branchid = None
            ids = self.branchids()
            for id in ids:
                if ids[id] == branch:
                    branchid = id
            return branchid

    repo.__class__ = branchidrepo

    def ctx_branch(orig, self):
        id = self.extra().get('branchid')
        if id:
            branch = self._repo.branchids().get(id, 'default')
        else:
            branch = orig(self)
        return branch

    extensions.wrapfunction(context.changectx, 'branch', ctx_branch)


def brididentify(orig, ui, repo, source=None,
        rev=None, num=None, id=None, branch=None, tags=None, branchid=None):

    if not repo and not source:
        raise util.Abort(_("There is no Mercurial repository here "
                           "(.hg not found)"))

    hexfunc = ui.debugflag and hex or short
    default = not (num or id or branch or branchid or tags)
    output = []

    revs = []
    if source:
        source, revs, checkout = hg.parseurl(ui.expandpath(source), [])
        repo = hg.repository(ui, source)

    if not repo.local():
        if not rev and revs:
            rev = revs[0]
        if not rev:
            rev = "tip"
        if num or branch or branchid or tags:
            raise util.Abort(
                "can't query remote revision number, branch, branchid, or tags")
        output = [hexfunc(repo.lookup(rev))]
    elif not rev:
        ctx = repo[None]
        parents = ctx.parents()
        changed = False
        if default or id or num:
            changed = ctx.files() + ctx.deleted()
        if default or id:
            output = ["%s%s" % ('+'.join([hexfunc(p.node()) for p in parents]),
                                (changed) and "+" or "")]
        if num:
            output.append("%s%s" % ('+'.join([str(p.rev()) for p in parents]),
                                    (changed) and "+" or ""))
    else:
        ctx = repo[rev]
        if default or id:
            output = [hexfunc(ctx.node())]
        if num:
            output.append(str(ctx.rev()))

    if repo.local() and default and not ui.quiet:
        b = encoding.tolocal(ctx.branch())
        if b != 'default':
            output.append("(%s)" % b)

        # multiple tags for a single parent separated by '/'
        t = "/".join(ctx.tags())
        if t:
            output.append(t)

    if branch:
        output.append(encoding.tolocal(ctx.branch()))

    if tags:
        output.extend(ctx.tags())

    if branchid:
        brid = ctx.extra().get('branchid', nullid)
        output.append(hexfunc(brid))

    ui.write("%s\n" % ' '.join(output))


def bridcommit(orig, ui, repo, *pats, **opts):
    extra = {}
    if opts.get('close_branch'):
        extra['close'] = 1

    ds = repo.dirstate
    branchname = ds.branch()
    parents = ds.parents()

    pctx = repo[parents[0]]
    pbrid = pctx.extra().get('branchid')
    pbn = pctx.branch()

    if opts.get('rename_branch') and pbrid == None:
        raise util.Abort(
            _('branch %s cannot be renamed (it has no branch id)') % pbn)

    newbranchid = None

    if (branchname != pbn) and not opts.get('rename_branch'):
        branchid = repo.getbranchid(branchname)
        if branchid:
            extra['branchid'] = branchid
        else:
            date = opts.get('date')
            if date:
                date = util.parsedate(date)
            else:
                date = util.makedate()

            user = opts.get('user')
            if not user:
                user = repo.ui.username()

            v = [str(date), str(user), branchname]
            for p in parents:
                v.append(p)
            v = '+'.join(v)
            newbranchid = util.sha1(v).digest()
            extra['branchid'] = newbranchid

    e = cmdutil.commiteditor
    if opts.get('force_editor'):
        e = cmdutil.commitforceeditor

    def commitfunc(ui, repo, message, match, opts):
        return repo.commit(message, opts.get('user'), opts.get('date'), match,
                           editor=e, extra=extra)

    node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
    if not node:
        ui.status(_("nothing changed\n"))
        return
    cl = repo.changelog
    rev = cl.rev(node)
    parents = cl.parentrevs(rev)
    if rev - 1 in parents:
        # one of the parents was the old tip
        pass
    elif (parents == (nullrev, nullrev) or
          len(cl.heads(cl.node(parents[0]))) > 1 and
          (parents[1] == nullrev or len(cl.heads(cl.node(parents[1]))) > 1)):
        ui.status(_('created new head\n'))

    hexfunc = ui.debugflag and hex or short

    if ui.debugflag or ui.verbose:
        if newbranchid:
            ui.status(_('created new branch %s (branch id %s)\n')
                % (branchname, hexfunc(newbranchid)))
        elif pbrid and opts.get('rename_branch'):
            ui.status(_('renamed branch to %s (branch id %s)\n')
                % (branchname, hexfunc(pbrid)))
        ui.write(_('committed changeset %d:%s\n') % (rev, hexfunc(node)))