[PATCH] Proposed implementaiton of .hgbranches

Andrew Beekhof beekhof at gmail.com
Mon Jul 31 09:31:12 UTC 2006


Under Chris Mason's direction I have implemented .hgbranches support.

Possible points of interest:
 * I chose to add a -b option to tag and tags rather than add branch
and branches
 * I added a -b option to identify
 * Performance - the use of reachable() might be suboptimal

It's a rather long patch so I will also attach it.
It can also be obtained by pulling from http://hg.beekhof.net/hg/mercurial

For people using Chris' hg-cvs-import, you should pull from
http://hg.beekhof.net/hg/cvs-import to obtain a version that works
with this patch.

regards,
andrew

Exporting patch:
# HG changeset patch
# User Andrew Beekhof <beekhof at gmail.com>
# Date 1154337087 -7200
# Node ID 20dabd5b7d2ce1c5183c3dc39a1fcfed6b0d7a1c
# Parent  ad4155e757da149262b6596928404b9aaf5a7e1e
Initial .hgbranches support
 * Adds -b options to tags, tag, and id
 * Changed the implementation of -b for heads() and doupdate()
 * Includes list of branches for cmd=tags in web interface

diff -r ad4155e757da149262b6596928404b9aaf5a7e1e -r
20dabd5b7d2ce1c5183c3dc39a1fcfed6b0d7a1c mercurial/commands.py
--- a/mercurial/commands.py     Mon Jul 31 00:47:43 2006 -0500
+++ b/mercurial/commands.py     Mon Jul 31 11:11:27 2006 +0200
@@ -461,7 +461,7 @@ class changeset_printer(object):
         self.ui = ui
         self.repo = repo

-    def show(self, rev=0, changenode=None, brinfo=None):
+    def show(self, rev=0, changenode=None, branch=False):
         '''show a single changeset or file revision'''
         log = self.repo.changelog
         if changenode is None:
@@ -493,8 +493,13 @@ class changeset_printer(object):
         for parent in parents:
             self.ui.write(_("parent:      %d:%s\n") % parent)

-        if brinfo and changenode in brinfo:
-            br = brinfo[changenode]
+        if branch:
+            br = []
+            brinfo = self.repo.nodebranches(changenode)
+            brinfo.reverse()
+            for b_name, cs in brinfo:
+                br.append(b_name)
+
             self.ui.write(_("branch:      %s\n") % " ".join(br))

         self.ui.debug(_("manifest:    %d:%s\n") %
@@ -1657,14 +1662,11 @@ def heads(ui, repo, **opts):
         heads = repo.heads(repo.lookup(opts['rev']))
     else:
         heads = repo.heads()
-    br = None
-    if opts['branches']:
-        br = repo.branchlookup(heads)
     displayer = show_changeset(ui, repo, opts)
     for n in heads:
-        displayer.show(changenode=n, brinfo=br)
-
-def identify(ui, repo):
+        displayer.show(changenode=n, branch=opts['branches'])
+
+def identify(ui, repo, **opts):
     """print information about the working copy

     Print a short summary of the current state of the repo.
@@ -1680,14 +1682,34 @@ def identify(ui, repo):

     hexfunc = ui.verbose and hex or short
     modified, added, removed, deleted, unknown = repo.changes()
-    output = ["%s%s" %
-              ('+'.join([hexfunc(parent) for parent in parents]),
-              (modified or added or removed or deleted) and "+" or "")]
+    for parent in parents:
+        p_entries = []
+        if opts['cs']:
+            p_entries.append("%d:%s" % (repo.changelog.rev(parent),
+                                        hexfunc(parent)))
+        else:
+            p_entries.append(hexfunc(parent))
+
+        output = ["%s%s" %
+                  ('+'.join(p_entries),
+                   (modified or added or removed or deleted) and "+" or "")]

     if not ui.quiet:
         # multiple tags for a single parent separated by '/'
         parenttags = ['/'.join(tags)
                       for tags in map(repo.nodetags, parents) if tags]
+
+        if opts['branches']:
+            for parent in parents:
+                brinfo = repo.nodebranches(parent)
+                brinfo.reverse()
+
+                # All branches, or just the last one?
+                if brinfo:
+                    parenttags.append(brinfo[0][0])
+                #for br in brinfo:
+                #    parenttags.append(br[0])
+
         # tags for multiple parents separated by ' + '
         if parenttags:
             output.append(' + '.join(parenttags))
@@ -2002,11 +2024,7 @@ def log(ui, repo, *pats, **opts):
                 if miss:
                     continue

-            br = None
-            if opts['branches']:
-                br = repo.branchlookup([repo.changelog.node(rev)])
-
-            displayer.show(rev, brinfo=br)
+            displayer.show(rev, opts['branches'])
             if opts['patch']:
                 prev = (parents and parents[0]) or nullid
                 dodiff(du, du, repo, prev, changenode, match=matchfn)
@@ -2116,13 +2134,10 @@ def parents(ui, repo, file_=None, rev=No
     else:
         p = repo.dirstate.parents()

-    br = None
-    if branches is not None:
-        br = repo.branchlookup(p)
     displayer = show_changeset(ui, repo, opts)
     for n in p:
         if n != nullid:
-            displayer.show(changenode=n, brinfo=br)
+            displayer.show(changenode=n, branch=branches)

 def paths(ui, repo, search=None):
     """show definition of symbolic path names
@@ -2703,10 +2718,13 @@ def tag(ui, repo, name, rev_=None, **opt
             raise util.Abort(_('outstanding uncommitted merges'))
         r = hex(p1)

-    repo.tag(name, r, opts['local'], opts['message'], opts['user'],
-             opts['date'])
-
-def tags(ui, repo):
+    if opts['branch']:
+        repo.branch(name, r, opts['message'], opts['user'], opts['date'])
+    else:
+        repo.tag(name, r, opts['local'], opts['message'], opts['user'],
+                 opts['date'])
+
+def tags(ui, repo, **opts):
     """list repository tags

     List the repository tags.
@@ -2714,7 +2732,11 @@ def tags(ui, repo):
     This lists both regular and local tags.
     """

-    l = repo.tagslist()
+    if opts['branches']:
+        l = repo.branchlist()
+    else:
+        l = repo.tagslist()
+
     l.reverse()
     for t, n in l:
         try:
@@ -2732,10 +2754,7 @@ def tip(ui, repo, **opts):
     Show the tip revision.
     """
     n = repo.changelog.tip()
-    br = None
-    if opts['branches']:
-        br = repo.branchlookup([n])
-    show_changeset(ui, repo, opts).show(changenode=n, brinfo=br)
+    show_changeset(ui, repo, opts).show(changenode=n, branch=opts['branches'])
     if opts['patch']:
         dodiff(ui, ui, repo, repo.changelog.parents(n)[0], n)

@@ -2805,24 +2824,40 @@ def doupdate(ui, repo, node=None, merge=
 def doupdate(ui, repo, node=None, merge=False, clean=False, force=None,
              branch=None, **opts):
     if branch:
-        br = repo.branchlookup(branch=branch)
-        found = []
-        for x in br:
-            if branch in br[x]:
-                found.append(x)
-        if len(found) > 1:
-            ui.warn(_("Found multiple heads for %s\n") % branch)
-            for x in found:
-                show_changeset(ui, repo, opts).show(changenode=x, brinfo=br)
-            return 1
-        if len(found) == 1:
-            node = found[0]
-            ui.warn(_("Using head %s for branch %s\n") % (short(node), branch))
-        else:
+        cs_match = None
+        brinfo = repo.branchlist()
+        for b_name, cs in brinfo:
+            if b_name == branch:
+                cs_match = cs
+
+        # Fall back to regular tags?
+        #if not cs_match:
+        #    brinfo = repo.tagslist()
+        #    for b_name, cs in brinfo:
+        #        if b_name == branch:
+        #            cs_match = cs
+
+        if not cs_match:
             ui.warn(_("branch %s not found\n") % (branch))
             return 1
+
+        heads = repo.heads(cs_match)
+        if not heads:
+            ui.warn(_("No heads found for branch %s\n") % (branch))
+            return 1
+
+        elif len(heads) > 1:
+            ui.warn(_("Found multiple heads for %s\n") % branch)
+            for x in heads:
+                show_changeset(ui, repo, opts).show(changenode=x)
+            return 1
+
+        node = heads[0]
+        ui.warn(_("Using head %s for branch %s\n") % (short(node), branch))
+
     else:
         node = node and repo.lookup(node) or repo.changelog.tip()
+
     return repo.update(node, allow=merge, force=clean, forcemerge=force)

 def verify(ui, repo):
@@ -2997,7 +3032,11 @@ table = {
           ('', 'template', '', _('display with template'))],
          _('hg heads [-b] [-r <rev>]')),
     "help": (help_, [], _('hg help [COMMAND]')),
-    "identify|id": (identify, [], _('hg identify')),
+    "identify|id":
+        (identify,
+          [('b', 'branches', None, _('show branches')),
+           ('c', 'cs', None, _('show changeset'))],
+         _('hg identify [-b] [-c]')),
     "import|patch":
         (import_,
          [('p', 'strip', 1,
@@ -3176,9 +3215,13 @@ table = {
           ('m', 'message', '', _('message for tag commit log entry')),
           ('d', 'date', '', _('record datecode as commit date')),
           ('u', 'user', '', _('record user as commiter')),
-          ('r', 'rev', '', _('revision to tag'))],
-         _('hg tag [-l] [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME')),
-    "tags": (tags, [], _('hg tags')),
+          ('r', 'rev', '', _('revision to tag')),
+          ('b', 'branch', None, _('create tag as a branch'))],
+         _('hg tag [-l] [-m TEXT] [-d DATE] [-u USER] [-r REV] [-b] NAME')),
+    "tags":
+        (tags,
+         [('b', 'branches', None, _('show branches'))],
+         _('hg tags [-b]')),
     "tip":
         (tip,
          [('b', 'branches', None, _('show branches')),
diff -r ad4155e757da149262b6596928404b9aaf5a7e1e -r
20dabd5b7d2ce1c5183c3dc39a1fcfed6b0d7a1c mercurial/hgweb/hgweb_mod.py
--- a/mercurial/hgweb/hgweb_mod.py      Mon Jul 31 00:47:43 2006 -0500
+++ b/mercurial/hgweb/hgweb_mod.py      Mon Jul 31 11:11:27 2006 +0200
@@ -528,8 +528,17 @@ class hgweb(object):
         cl = self.repo.changelog
         mf = cl.read(cl.tip())[0]

-        i = self.repo.tagslist()
-        i.reverse()
+       # Include any branches (newer ones first) before regular tags
+        branches = self.repo.branchlist()
+        tags = self.repo.tagslist()
+        tags.reverse()
+
+        if branches:
+            branches.reverse()
+            branches.extend(tags)
+            i = branches
+        else:
+            i = tags

         def entries(notip=False, **map):
             parity = 0
diff -r ad4155e757da149262b6596928404b9aaf5a7e1e -r
20dabd5b7d2ce1c5183c3dc39a1fcfed6b0d7a1c mercurial/localrepo.py
--- a/mercurial/localrepo.py    Mon Jul 31 00:47:43 2006 -0500
+++ b/mercurial/localrepo.py    Mon Jul 31 11:11:27 2006 +0200
@@ -74,6 +74,9 @@ class localrepository(repo.repository):
         self.encodepats = None
         self.decodepats = None
         self.transhandle = None
+
+        self.branchcache = None
+        self.branchcachelist = None

         if create:
             if not os.path.exists(path):
@@ -213,31 +216,31 @@ class localrepository(repo.repository):
         self.commit(['.hgtags'], message, user, date)
         self.hook('tag', node=node, tag=name, local=local)

+    def parsetag(self, line, context, cache):
+        if not line:
+            return
+        s = line.split(" ", 1)
+        if len(s) != 2:
+            self.ui.warn(_("%s: cannot parse entry\n") % context)
+            return
+        node, key = s
+        key = key.strip()
+        try:
+            bin_n = bin(node)
+        except TypeError:
+            self.ui.warn(_("%s: node '%s' is not well formed\n") %
+                         (context, node))
+            return
+        if bin_n not in self.changelog.nodemap:
+            self.ui.warn(_("%s: branch/tag '%s' refers to unknown node\n") %
+                         (context, key))
+            return
+        cache[key] = bin_n
+
     def tags(self):
         '''return a mapping of tag to node'''
         if not self.tagscache:
             self.tagscache = {}
-
-            def parsetag(line, context):
-                if not line:
-                    return
-                s = l.split(" ", 1)
-                if len(s) != 2:
-                    self.ui.warn(_("%s: cannot parse entry\n") % context)
-                    return
-                node, key = s
-                key = key.strip()
-                try:
-                    bin_n = bin(node)
-                except TypeError:
-                    self.ui.warn(_("%s: node '%s' is not well formed\n") %
-                                 (context, node))
-                    return
-                if bin_n not in self.changelog.nodemap:
-                    self.ui.warn(_("%s: tag '%s' refers to unknown node\n") %
-                                 (context, key))
-                    return
-                self.tagscache[key] = bin_n

             # read the tags file from each head, ending with the tip,
             # and add each tag found to the map, with "newer" ones
@@ -253,14 +256,15 @@ class localrepository(repo.repository):
                 count = 0
                 for l in fl.read(fn).splitlines():
                     count += 1
-                    parsetag(l, _(".hgtags (rev %d:%s), line %d") %
-                             (rev, short(node), count))
+                    self.parsetag(l, _(".hgtags (rev %d:%s), line %d") %
+                             (rev, short(node), count), self.tagscache)
             try:
                 f = self.opener("localtags")
                 count = 0
                 for l in f:
                     count += 1
-                    parsetag(l, _("localtags, line %d") % count)
+                    self.parsetag(l, _("localtags, line %d") % count,
+                                  self.tagscache)
             except IOError:
                 pass

@@ -287,6 +291,98 @@ class localrepository(repo.repository):
             for t, n in self.tags().items():
                 self.nodetagscache.setdefault(n, []).append(t)
         return self.nodetagscache.get(node, [])
+
+    def branch(self, name, node, message=None, user=None, date=None):
+        '''create a branch based on a revision.
+
+        the branch is stored in the .hgbranches file, and a new
+        changeset is committed with the change.
+
+        keyword arguments:
+
+        message: commit message to use if committing
+
+        user: name of user to use if committing
+
+        date: date tuple to use if committing'''
+
+        for c in self.tag_disallowed:
+            if c in name:
+                raise util.Abort(_('%r cannot be used in a branch name') % c)
+
+        for x in self.changes():
+            if '.hgbranches' in x:
+                raise util.Abort(_('working copy of .hgbranches is changed '
+                                   '(please commit .hgbranches manually)'))
+
+        self.hook('prebranch', throw=True, node=node, tag=name)
+
+        self.wfile('.hgbranches', 'ab').write('%s %s\n' % (node, name))
+        if self.dirstate.state('.hgbranches') == '?':
+            self.add(['.hgbranches'])
+
+        if not message:
+            message = _('Added branch %s for changeset %s') % (name, node)
+
+        self.commit(['.hgbranches'], message, user, date)
+        self.hook('branch', node=node, tag=name)
+
+    def all_branches(self):
+        '''return a mapping of tag to node'''
+
+        # read the branch file from each head, ending with the tip,
+        # and add each branch found to the map, with "newer" ones
+        # taking precedence
+
+        if self.branchcache != None:
+           return self.branchcache
+
+        self.branchcache = {}
+        heads = self.heads()
+        heads.reverse()
+
+        fl = self.file(".hgbranches")
+        for node in heads:
+            change = self.changelog.read(node)
+            rev = self.changelog.rev(node)
+            fn, ff = self.manifest.find(change[0], '.hgbranches')
+            if fn is None: continue
+            count = 0
+            for l in fl.read(fn).splitlines():
+                count += 1
+                self.parsetag(l, _(".hgbranches (rev %d:%s), line %d") %
+                              (rev, short(node), count), self.branchcache)
+
+        return self.branchcache
+
+    def branchlist(self):
+        '''return a list of tags ordered by revision'''
+        if self.branchcachelist != None:
+            return self.branchcachelist
+
+        l = []
+        for t, n in self.all_branches().items():
+            try:
+                r = self.changelog.rev(n)
+            except:
+                r = -2 # sort to the beginning of the list if unknown
+            l.append((r, t, n))
+
+        l.sort()
+        self.branchcachelist = [(t, n) for r, t, n in l]
+        return self.branchcachelist
+
+    def nodebranches(self, node):
+        if not node:
+            return []
+
+        l = []
+        brinfo = self.branchlist()
+        reachable = self.changelog.reachable(node)
+        for t, n in brinfo:
+            if n in reachable:
+                l.append((r, t, n))
+        return l

     def lookup(self, key):
         try:
@@ -845,115 +941,10 @@ class localrepository(repo.repository):
         heads.sort()
         return [n for (r, n) in heads]

-    # branchlookup returns a dict giving a list of branches for
-    # each head.  A branch is defined as the tag of a node or
-    # the branch of the node's parents.  If a node has multiple
-    # branch tags, tags are eliminated if they are visible from other
-    # branch tags.
-    #
-    # So, for this graph:  a->b->c->d->e
-    #                       \         /
-    #                        aa -----/
-    # a has tag 2.6.12
-    # d has tag 2.6.13
-    # e would have branch tags for 2.6.12 and 2.6.13.  Because the node
-    # for 2.6.12 can be reached from the node 2.6.13, that is eliminated
-    # from the list.
-    #
-    # It is possible that more than one head will have the same branch tag.
-    # callers need to check the result for multiple heads under the same
-    # branch tag if that is a problem for them (ie checkout of a specific
-    # branch).
-    #
-    # passing in a specific branch will limit the depth of the search
-    # through the parents.  It won't limit the branches returned in the
-    # result though.
-    def branchlookup(self, heads=None, branch=None):
-        if not heads:
-            heads = self.heads()
-        headt = [ h for h in heads ]
-        chlog = self.changelog
-        branches = {}
-        merges = []
-        seenmerge = {}
-
-        # traverse the tree once for each head, recording in the branches
-        # dict which tags are visible from this head.  The branches
-        # dict also records which tags are visible from each tag
-        # while we traverse.
-        while headt or merges:
-            if merges:
-                n, found = merges.pop()
-                visit = [n]
-            else:
-                h = headt.pop()
-                visit = [h]
-                found = [h]
-                seen = {}
-            while visit:
-                n = visit.pop()
-                if n in seen:
-                    continue
-                pp = chlog.parents(n)
-                tags = self.nodetags(n)
-                if tags:
-                    for x in tags:
-                        if x == 'tip':
-                            continue
-                        for f in found:
-                            branches.setdefault(f, {})[n] = 1
-                        branches.setdefault(n, {})[n] = 1
-                        break
-                    if n not in found:
-                        found.append(n)
-                    if branch in tags:
-                        continue
-                seen[n] = 1
-                if pp[1] != nullid and n not in seenmerge:
-                    merges.append((pp[1], [x for x in found]))
-                    seenmerge[n] = 1
-                if pp[0] != nullid:
-                    visit.append(pp[0])
-        # traverse the branches dict, eliminating branch tags from each
-        # head that are visible from another branch tag for that head.
-        out = {}
-        viscache = {}
-        for h in heads:
-            def visible(node):
-                if node in viscache:
-                    return viscache[node]
-                ret = {}
-                visit = [node]
-                while visit:
-                    x = visit.pop()
-                    if x in viscache:
-                        ret.update(viscache[x])
-                    elif x not in ret:
-                        ret[x] = 1
-                        if x in branches:
-                            visit[len(visit):] = branches[x].keys()
-                viscache[node] = ret
-                return ret
-            if h not in branches:
-                continue
-            # O(n^2), but somewhat limited.  This only searches the
-            # tags visible from a specific head, not all the tags in the
-            # whole repo.
-            for b in branches[h]:
-                vis = False
-                for bb in branches[h].keys():
-                    if b != bb:
-                        if b in visible(bb):
-                            vis = True
-                            break
-                if not vis:
-                    l = out.setdefault(h, [])
-                    l[len(l):] = self.nodetags(b)
-        return out
-
     def branches(self, nodes):
         if not nodes:
             nodes = [self.changelog.tip()]
+
         b = []
         for n in nodes:
             t = n
-------------- next part --------------
A non-text attachment was scrubbed...
Name: hg-branches.export
Type: application/octet-stream
Size: 21025 bytes
Desc: not available
URL: <http://lists.mercurial-scm.org/pipermail/mercurial/attachments/20060731/5fd43ddc/attachment-0001.obj>


More information about the Mercurial mailing list