Commits

Brodie Rao committed 7c57993

Refreshing queue

  • Participants
  • Parent commits af34d53

Comments (0)

Files changed (11)

File branch-cache

+# HG changeset patch
+# Parent 0027a5cec9d00873ca14129b4589975083e5e71d
+branchmap: cache open/closed branch head information
+
+This gets rid of having to read the changelog in order to figure out what
+branches are still open, what the tipmost head of a branch is, and checking if
+a changeset closes its branch in general.
+
+On the PyPy repo with 837 branches, 731 of which are closed, running hg
+branches --profile --time over NFS before this change:
+
+   CallCount    Recursive     Total(s)    Inline(s) module:lineno(function)
+        1735            0      0.6673      0.6673   <open>
+        1732            0      0.3186      0.3186   <method 'close' of 'file' objects>
+        4440            0      0.2228      0.0468   mercurial.repoview:162(changelog)
+        8878            0      0.0861      0.0342       mercurial.changelog:132(tip)
+        4440            0      0.0506      0.0208       mercurial.localrepo:26(__get__)
+        8878            0      0.0264      0.0153       <len>
+        4440            0      0.0085      0.0039       mercurial.repoview:113(filterrevs)
+        8878            0      0.0022      0.0022       <hash>
+        1731            0      0.0419      0.0419   <method 'read' of 'file' objects>
+        1729            0      0.0419      0.0419   <method 'seek' of 'file' objects>
+          61           35      0.0433      0.0392   <__import__>
+           1            0      0.0009      0.0004       mercurial.context:8(<module>)
+           1            0      0.0011      0.0004       hashlib:72(<module>)
+           1            0      0.0057      0.0002       mercurial.revlog:12(<module>)
+           1            0      0.0003      0.0002       mercurial.obsolete:84(<module>)
+           1            0      0.0004      0.0002       mercurial.dirstate:7(<module>)
+        2683            0      0.1933      0.0385   mercurial.context:22(__init__)
+        2683            0      0.1223      0.0289       mercurial.repoview:162(changelog)
+        2683            0      0.0224      0.0082       mercurial.changelog:182(rev)
+        5366            0      0.0070      0.0070       <isinstance>
+         937            0      0.0018      0.0018       <binascii.unhexlify>
+        3620            0      0.0013      0.0013       <len>
+        1746            0      1.4835      0.0368   mercurial.changelog:269(read)
+        1746            0      1.3690      0.0351       mercurial.revlog:880(revision)
+        1744            0      0.0305      0.0152       mercurial.changelog:28(decodeextra)
+        3492            0      0.0092      0.0092       <method 'split' of 'str' objects>
+        3492            0      0.0311      0.0053       mercurial.encoding:61(tolocal)
+        1746            0      0.0032      0.0032       <method 'index' of 'str' objects>
+        1746            0      1.3690      0.0351   mercurial.revlog:880(revision)
+        1746            0      1.1827      0.0074       mercurial.revlog:846(_chunkraw)
+time: real 1.960 secs (user 0.870+0.000 sys 0.200+0.000)
+
+After this change:
+
+   CallCount    Recursive     Total(s)    Inline(s) module:lineno(function)
+         935            0      0.3758      0.3758   <open>
+         932            0      0.1725      0.1725   <method 'close' of 'file' objects>
+        8206            0      0.3317      0.0730   mercurial.repoview:162(changelog)
+       16410            0      0.1339      0.0517       mercurial.changelog:132(tip)
+        8206            0      0.0678      0.0306       mercurial.localrepo:26(__get__)
+       16410            0      0.0399      0.0224       <len>
+        8206            0      0.0113      0.0066       mercurial.repoview:113(filterrevs)
+       16410            0      0.0034      0.0034       <hash>
+          61           35      0.0561      0.0517   <__import__>
+           1            0      0.0010      0.0004       mercurial.context:8(<module>)
+           1            0      0.0011      0.0004       hashlib:72(<module>)
+           1            0      0.0062      0.0002       mercurial.revlog:12(<module>)
+           1            0      0.0006      0.0002       copy:49(<module>)
+           1            0      0.0004      0.0002       mercurial.dirstate:7(<module>)
+       16410            0      0.1339      0.0517   mercurial.changelog:132(tip)
+       16410            0      0.0514      0.0392       mercurial.changelog:189(node)
+       16410            0      0.0308      0.0162       <len>
+       75222        33740      0.0762      0.0504   <len>
+       33740            0      0.0329      0.0258       mercurial.revlog:262(__len__)
+       19280            0      0.0637      0.0467   mercurial.changelog:189(node)
+       19280            0      0.0169      0.0169       mercurial.revlog:317(node)
+        2683            0      0.1710      0.0353   mercurial.context:22(__init__)
+        2683            0      0.1058      0.0252       mercurial.repoview:162(changelog)
+        2683            0      0.0214      0.0071       mercurial.changelog:182(rev)
+        5366            0      0.0053      0.0053       <isinstance>
+         937            0      0.0018      0.0018       <binascii.unhexlify>
+        3620            0      0.0013      0.0013       <len>
+        8427            1      0.0722      0.0315   mercurial.localrepo:26(__get__)
+        8427            1      0.0382      0.0144       mercurial.scmutil:963(__get__)
+        8215            0      0.0024      0.0024       mercurial.localrepo:310(unfiltered)
+time: real 1.510 secs (user 0.880+0.000 sys 0.120+0.000)
+
+diff --git a/mercurial/branchmap.py b/mercurial/branchmap.py
+--- a/mercurial/branchmap.py
++++ b/mercurial/branchmap.py
+@@ -11,7 +11,7 @@ import util, repoview
+ 
+ def _filename(repo):
+     """name of a branchcache file for a given repo or repoview"""
+-    filename = "cache/branchheads"
++    filename = "cache/branch2"
+     if repo.filtername:
+         filename = '%s-%s' % (filename, repo.filtername)
+     return filename
+@@ -39,11 +39,16 @@ def read(repo):
+         for l in lines:
+             if not l:
+                 continue
+-            node, label = l.split(" ", 1)
++            node, state, label = l.split(" ", 2)
++            if state not in 'oc':
++                raise ValueError('invalid branch state')
+             label = encoding.tolocal(label.strip())
+             if not node in repo:
+                 raise ValueError('node %s does not exist' % node)
+-            partial.setdefault(label, []).append(bin(node))
++            node = bin(node)
++            partial.setdefault(label, []).append(node)
++            if state == 'c':
++                partial._closednodes.add(node)
+     except KeyboardInterrupt:
+         raise
+     except Exception, inst:
+@@ -86,11 +91,18 @@ class branchcache(dict):
+     """A dict like object that hold branches heads cache"""
+ 
+     def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
+-                 filteredhash=None):
++                 filteredhash=None, closednodes=None):
+         super(branchcache, self).__init__(entries)
+         self.tipnode = tipnode
+         self.tiprev = tiprev
+         self.filteredhash = filteredhash
++        # closednodes is a set of nodes that close their branch. If the branch
++        # cache has been updated, it may contain nodes that are no longer
++        # heads.
++        if closednodes is None:
++            self._closednodes = set()
++        else:
++            self._closednodes = closednodes
+ 
+     def _hashfiltered(self, repo):
+         """build hash of revision filtered in the current cache
+@@ -124,9 +136,32 @@ class branchcache(dict):
+         except IndexError:
+             return False
+ 
++    def _branchtip(self, heads):
++        tip = heads[-1]
++        closed = True
++        for h in reversed(heads):
++            if h not in self._closednodes:
++                tip = h
++                closed = False
++                break
++        return tip, closed
++
++    def branchtip(self, branch):
++        return self._branchtip(self[branch])[0]
++
++    def iterheads(self):
++        for bn, heads in self.iteritems():
++            for head in heads:
++                yield (bn, head, head in self._closednodes)
++
++    def itertips(self):
++        for bn, heads in self.iteritems():
++            yield (bn,) + self._branchtip(heads)
++
+     def copy(self):
+         """return an deep copy of the branchcache object"""
+-        return branchcache(self, self.tipnode, self.tiprev, self.filteredhash)
++        return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
++                           self._closednodes)
+ 
+     def write(self, repo):
+         try:
+@@ -137,7 +172,12 @@ class branchcache(dict):
+             f.write(" ".join(cachekey) + '\n')
+             for label, nodes in sorted(self.iteritems()):
+                 for node in nodes:
+-                    f.write("%s %s\n" % (hex(node), encoding.fromlocal(label)))
++                    if node in self._closednodes:
++                        state = 'c'
++                    else:
++                        state = 'o'
++                    f.write("%s %s %s\n" % (hex(node), state,
++                                            encoding.fromlocal(label)))
+             f.close()
+         except (IOError, OSError, util.Abort):
+             # Abort may be raise by read only opener
+@@ -151,9 +191,13 @@ class branchcache(dict):
+         cl = repo.changelog
+         # collect new branch entries
+         newbranches = {}
+-        getbranch = cl.branch
++        getbranchinfo = cl.branchinfo
+         for r in revgen:
+-            newbranches.setdefault(getbranch(r), []).append(cl.node(r))
++            branch, closesbranch = getbranchinfo(r)
++            node = cl.node(r)
++            newbranches.setdefault(branch, []).append(node)
++            if closesbranch:
++                self._closednodes.add(node)
+         # if older branchheads are reachable from new ones, they aren't
+         # really branchheads. Note checking parents is insufficient:
+         # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
+diff --git a/mercurial/changelog.py b/mercurial/changelog.py
+--- a/mercurial/changelog.py
++++ b/mercurial/changelog.py
+@@ -341,9 +341,10 @@ class changelog(revlog.revlog):
+         text = "\n".join(l)
+         return self.addrevision(text, transaction, len(self), p1, p2)
+ 
+-    def branch(self, rev):
+-        """return the branch of a revision
++    def branchinfo(self, rev):
++        """return the branch name and open/close state of a revision
+ 
+         This function exists because creating a changectx object
+         just to access this is costly."""
+-        return encoding.tolocal(self.read(rev)[5].get("branch"))
++        extra = self.read(rev)[5]
++        return encoding.tolocal(extra.get("branch")), 'close' in extra
+diff --git a/mercurial/commands.py b/mercurial/commands.py
+--- a/mercurial/commands.py
++++ b/mercurial/commands.py
+@@ -964,17 +964,9 @@ def branches(ui, repo, active=False, clo
+ 
+     activebranches = set([repo[n].branch() for n in repo.heads()])
+     branches = []
+-    for tag, heads in repo.branchmap().iteritems():
+-        for h in reversed(heads):
+-            ctx = repo[h]
+-            isopen = not ctx.closesbranch()
+-            if isopen:
+-                tip = ctx
+-                break
+-        else:
+-            tip = repo[heads[-1]]
+-        isactive = tag in activebranches and isopen
+-        branches.append((tip, isactive, isopen))
++    for tag, tip, closesbranch in repo.branchmap().itertips():
++        isactive = tag in activebranches and not closesbranch
++        branches.append((tip, isactive, not closesbranch))
+     branches.sort(key=lambda i: (i[1], i[0].rev(), i[0].branch(), i[2]),
+                   reverse=True)
+ 
+diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
+--- a/mercurial/localrepo.py
++++ b/mercurial/localrepo.py
+@@ -634,29 +634,17 @@ class localrepository(object):
+         branchmap.updatecache(self)
+         return self._branchcaches[self.filtername]
+ 
+-
+-    def _branchtip(self, heads):
+-        '''return the tipmost branch head in heads'''
+-        tip = heads[-1]
+-        for h in reversed(heads):
+-            if not self[h].closesbranch():
+-                tip = h
+-                break
+-        return tip
+-
+     def branchtip(self, branch):
+         '''return the tip node for a given branch'''
+-        if branch not in self.branchmap():
++        try:
++            return self.branchmap().branchtip(branch)
++        except KeyError:
+             raise error.RepoLookupError(_("unknown branch '%s'") % branch)
+-        return self._branchtip(self.branchmap()[branch])
+ 
+     def branchtags(self):
+         '''return a dict where branch names map to the tipmost head of
+         the branch, open heads come before closed'''
+-        bt = {}
+-        for bn, heads in self.branchmap().iteritems():
+-            bt[bn] = self._branchtip(heads)
+-        return bt
++        return dict(self.branchmap().itertips())
+ 
+     def lookup(self, key):
+         return self[key].node()
+# HG changeset patch
+# Parent fd903f89e42b8e90b6f27e3cb87ca336c200a038
+diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
+--- a/mercurial/localrepo.py
++++ b/mercurial/localrepo.py
+@@ -622,13 +622,13 @@ class localrepository(object):
+         '''returns a dictionary {branch: [branchheads]}'''
+         if self.changelog.filteredrevs:
+             # some changeset are excluded we can't use the cache
+-            branchmap = {}
+-            self._updatebranchcache(branchmap, (self[r] for r in self))
+-            return branchmap
++            branchcache = {}
++            self._updatebranchcache(branchcache, (self[r] for r in self))
+         else:
+             self.updatebranchcache()
+-            return self._branchcache
+-
++            branchcache = self._branchcache
++        return dict([(branch, [node for (node, isopen) in heads])
++                     for (branch, heads) in branchcache.iteritems()])
+ 
+     def _branchtip(self, heads):
+         '''return the tipmost branch head in heads'''
+@@ -656,7 +656,7 @@ class localrepository(object):
+     def _readbranchcache(self):
+         partial = {}
+         try:
+-            f = self.opener("cache/branchheads")
++            f = self.opener("cache/branches")
+             lines = f.read().split('\n')
+             f.close()
+         except (IOError, OSError):
+@@ -671,12 +671,19 @@ class localrepository(object):
+             for l in lines:
+                 if not l:
+                     continue
+-                node, label = l.split(" ", 1)
++                node, state, label = l.split(" ", 2)
+                 label = encoding.tolocal(label.strip())
+                 if not node in self:
+                     raise ValueError('invalidating branch cache because node '+
+                                      '%s does not exist' % node)
+-                partial.setdefault(label, []).append(bin(node))
++                if state == 'o':
++                    isopen = True
++                elif state == 'c':
++                    isopen = False
++                else:
++                    raise ValueError('invalid branch state %r for node %s'
++                                     % (state, node))
++                partial.setdefault(label, []).append((bin(node), isopen))
+         except KeyboardInterrupt:
+             raise
+         except Exception, inst:
+@@ -687,11 +694,16 @@ class localrepository(object):
+ 
+     def _writebranchcache(self, branches, tip, tiprev):
+         try:
+-            f = self.opener("cache/branchheads", "w", atomictemp=True)
++            f = self.opener("cache/branches", "w", atomictemp=True)
+             f.write("%s %s\n" % (hex(tip), tiprev))
+             for label, nodes in branches.iteritems():
+-                for node in nodes:
+-                    f.write("%s %s\n" % (hex(node), encoding.fromlocal(label)))
++                for node, isopen in nodes:
++                    if isopen:
++                        state = 'o'
++                    else:
++                        state = 'c'
++                    f.write("%s %s %s\n" % (hex(node), state,
++                                            encoding.fromlocal(label)))
+             f.close()
+         except (IOError, OSError):
+             pass
+@@ -704,7 +716,8 @@ class localrepository(object):
+         # collect new branch entries
+         newbranches = {}
+         for c in ctxgen:
+-            newbranches.setdefault(c.branch(), []).append(c.node())
++            newbranches.setdefault(c.branch(), []).append(
++                (c.node(), c.closesbranch()))
+         # if older branchheads are reachable from new ones, they aren't
+         # really branchheads. Note checking parents is insufficient:
+         # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
+@@ -714,10 +727,10 @@ class localrepository(object):
+             # the result of a strip that just happened).  Avoid using 'node in
+             # self' here because that dives down into branchcache code somewhat
+             # recursively.
+-            bheadrevs = [self.changelog.rev(node) for node in bheads
++            bheadrevs = [self.changelog.rev(node) for (node, isopen) in bheads
+                          if self.changelog.hasnode(node)]
+-            newheadrevs = [self.changelog.rev(node) for node in newnodes
+-                           if self.changelog.hasnode(node)]
++            newheadrevs = [self.changelog.rev(node) for (node, isopen)
++                           in newnodes if self.changelog.hasnode(node)]
+             ctxisnew = bheadrevs and min(newheadrevs) > max(bheadrevs)
+             # Remove duplicates - nodes that are in newheadrevs and are already
+             # in bheadrevs.  This can happen if you strip a node whose parent
+@@ -743,7 +756,9 @@ class localrepository(object):
+                                                          bheadrevs[0]))
+                 if ancestors:
+                     bheadrevs = [b for b in bheadrevs if b not in ancestors]
+-            partial[branch] = [self.changelog.node(rev) for rev in bheadrevs]
++            partial[branch] = [(self.changelog.node(rev),
++                                'close' not in self.changelog.read(rev)[5])
++                               for rev in bheadrevs]
+ 
+         # There may be branches that cease to exist when the last commit in the
+         # branch was stripped.  This code filters them out.  Note that the
+@@ -751,7 +766,7 @@ class localrepository(object):
+         # newbranches is the set of candidate heads, which when you strip the
+         # last commit in a branch will be the parent branch.
+         for branch in partial.keys():
+-            nodes = [head for head in partial[branch]
++            nodes = [head for (head, isopen) in partial[branch]
+                      if self.changelog.hasnode(head)]
+             if not nodes:
+                 del partial[branch]
+@@ -2464,12 +2479,6 @@ class localrepository(object):
+     def stream_in(self, remote, requirements):
+         lock = self.lock()
+         try:
+-            # Save remote branchmap. We will use it later
+-            # to speed up branchcache creation
+-            rbranchmap = None
+-            if remote.capable("branchmap"):
+-                rbranchmap = remote.branchmap()
+-
+             fp = remote.stream_out()
+             l = fp.readline()
+             try:
+@@ -2529,18 +2538,6 @@ class localrepository(object):
+             requirements.update(set(self.requirements) - self.supportedformats)
+             self._applyrequirements(requirements)
+             self._writerequirements()
+-
+-            if rbranchmap:
+-                rbheads = []
+-                for bheads in rbranchmap.itervalues():
+-                    rbheads.extend(bheads)
+-
+-                self.branchcache = rbranchmap
+-                if rbheads:
+-                    rtiprev = max((int(self.changelog.rev(node))
+-                            for node in rbheads))
+-                    self._writebranchcache(self.branchcache,
+-                            self[rtiprev].node(), rtiprev)
+             self.invalidate()
+             return len(self.heads()) + 1
+         finally:
+# HG changeset patch
+# Parent 72c234081ae1350220132c69750f5a093902a1e7
+diff --git a/hgext/mq.py b/hgext/mq.py
+--- a/hgext/mq.py
++++ b/hgext/mq.py
+@@ -3459,7 +3459,7 @@ def reposetup(ui, repo):
+ 
+             return result
+ 
+-        def _branchtags(self, partial, lrev):
++        def _branchtags(self, partial, states, lrev):
+             q = self.mq
+             cl = self.changelog
+             qbase = None
+@@ -3475,14 +3475,15 @@ def reposetup(ui, repo):
+                     self.ui.warn(_('mq status file refers to unknown node %s\n')
+                                  % short(qbasenode))
+             if qbase is None:
+-                return super(mqrepo, self)._branchtags(partial, lrev)
++                return super(mqrepo, self)._branchtags(partial, states, lrev)
+ 
+             start = lrev + 1
+             if start < qbase:
+                 # update the cache (excluding the patches) and save it
+                 ctxgen = (self[r] for r in xrange(lrev + 1, qbase))
+-                self._updatebranchcache(partial, ctxgen)
+-                self._writebranchcache(partial, cl.node(qbase - 1), qbase - 1)
++                self._updatebranchcache(partial, states, ctxgen)
++                self._writebranchcache(partial, states, cl.node(qbase - 1),
++                                       qbase - 1)
+                 start = qbase
+             # if start = qbase, the cache is as updated as it should be.
+             # if start > qbase, the cache includes (part of) the patches.
+@@ -3490,7 +3491,7 @@ def reposetup(ui, repo):
+ 
+             # update the cache up to the tip
+             ctxgen = (self[r] for r in xrange(start, len(cl)))
+-            self._updatebranchcache(partial, ctxgen)
++            self._updatebranchcache(partial, states, ctxgen)
+ 
+             return partial
+ 
+diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
+--- a/mercurial/localrepo.py
++++ b/mercurial/localrepo.py
+@@ -602,13 +602,14 @@ class localrepository(object):
+                 marks.append(bookmark)
+         return sorted(marks)
+ 
+-    def _branchtags(self, partial, lrev):
++    def _branchtags(self, partial, states, lrev):
+         # TODO: rename this function?
+         tiprev = len(self) - 1
+         if lrev != tiprev:
+             ctxgen = (self[r] for r in self.changelog.revs(lrev + 1, tiprev))
+-            self._updatebranchcache(partial, ctxgen)
+-            self._writebranchcache(partial, self.changelog.tip(), tiprev)
++            self._updatebranchcache(partial, states, ctxgen)
++            self._writebranchcache(partial, states, self.changelog.tip(),
++                                   tiprev)
+ 
+         return partial
+ 
+@@ -620,21 +621,24 @@ class localrepository(object):
+         oldtip = self._branchcachetip
+         self._branchcachetip = tip
+         if oldtip is None or oldtip not in self.changelog.nodemap:
+-            partial, last, lrev = self._readbranchcache()
++            partial, states, last, lrev = self._readbranchcache()
+         else:
+             lrev = self.changelog.rev(oldtip)
+             partial = self._branchcache
++            states = self._branchstatecache
+ 
+-        self._branchtags(partial, lrev)
++        self._branchtags(partial, states, lrev)
+         # this private cache holds all heads (not just the branch tips)
+         self._branchcache = partial
++        self._branchstatecache = states
+ 
+     def branchmap(self):
+         '''returns a dictionary {branch: [branchheads]}'''
+         if self.changelog.filteredrevs:
+             # some changeset are excluded we can't use the cache
+             branchmap = {}
+-            self._updatebranchcache(branchmap, (self[r] for r in self))
++            states = {}
++            self._updatebranchcache(branchmap, states, (self[r] for r in self))
+             return branchmap
+         else:
+             self.updatebranchcache()
+@@ -666,12 +670,13 @@ class localrepository(object):
+ 
+     def _readbranchcache(self):
+         partial = {}
++        states = {}
+         try:
+-            f = self.opener("cache/branchheads")
++            f = self.opener("cache/branches")
+             lines = f.read().split('\n')
+             f.close()
+         except (IOError, OSError):
+-            return {}, nullid, nullrev
++            return {}, {}, nullid, nullrev
+ 
+         try:
+             last, lrev = lines.pop(0).split(" ", 1)
+@@ -682,32 +687,44 @@ class localrepository(object):
+             for l in lines:
+                 if not l:
+                     continue
+-                node, label = l.split(" ", 1)
++                node, state, label = l.split(" ", 2)
+                 label = encoding.tolocal(label.strip())
+                 if not node in self:
+                     raise ValueError('invalidating branch cache because node '+
+                                      '%s does not exist' % node)
++                if state == 'o':
++                    states[node] = True
++                elif state == 'c':
++                    states[node] = False
++                else:
++                    raise ValueError('invalid branch state %r for node %s'
++                                     % (state, node))
+                 partial.setdefault(label, []).append(bin(node))
+         except KeyboardInterrupt:
+             raise
+         except Exception, inst:
+             if self.ui.debugflag:
+                 self.ui.warn(str(inst), '\n')
+-            partial, last, lrev = {}, nullid, nullrev
+-        return partial, last, lrev
++            partial, states, last, lrev = {}, {}, nullid, nullrev
++        return partial, states, last, lrev
+ 
+-    def _writebranchcache(self, branches, tip, tiprev):
++    def _writebranchcache(self, branches, states, tip, tiprev):
+         try:
+-            f = self.opener("cache/branchheads", "w", atomictemp=True)
++            f = self.opener("cache/branches", "w", atomictemp=True)
+             f.write("%s %s\n" % (hex(tip), tiprev))
+             for label, nodes in branches.iteritems():
+                 for node in nodes:
+-                    f.write("%s %s\n" % (hex(node), encoding.fromlocal(label)))
++                    if states[node]:
++                        state = 'o'
++                    else:
++                        state = 'c'
++                    f.write("%s %s %s\n" % (hex(node), state,
++                                            encoding.fromlocal(label)))
+             f.close()
+         except (IOError, OSError):
+             pass
+ 
+-    def _updatebranchcache(self, partial, ctxgen):
++    def _updatebranchcache(self, partial, states, ctxgen):
+         """Given a branchhead cache, partial, that may have extra nodes or be
+         missing heads, and a generator of nodes that are at least a superset of
+         heads missing, this function updates partial to be correct.
+@@ -716,6 +733,7 @@ class localrepository(object):
+         newbranches = {}
+         for c in ctxgen:
+             newbranches.setdefault(c.branch(), []).append(c.node())
++            states[c.node()] = not c.closesbranch()
+         # if older branchheads are reachable from new ones, they aren't
+         # really branchheads. Note checking parents is insufficient:
+         # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
+@@ -755,6 +773,10 @@ class localrepository(object):
+                 if ancestors:
+                     bheadrevs = [b for b in bheadrevs if b not in ancestors]
+             partial[branch] = [self.changelog.node(rev) for rev in bheadrevs]
++            for node in partial[branch]:
++                if node not in states:
++                    isopen = 'close' not in self.changelog.read(node)[5]
++                    states[node] = isopen
+ 
+         # There may be branches that cease to exist when the last commit in the
+         # branch was stripped.  This code filters them out.  Note that the
+@@ -1501,9 +1523,10 @@ class localrepository(object):
+             tiprev = len(self) - 1
+             ctxgen = (self[node] for node in newheadnodes
+                       if self.changelog.hasnode(node))
+-            self._updatebranchcache(self._branchcache, ctxgen)
+-            self._writebranchcache(self._branchcache, self.changelog.tip(),
+-                                   tiprev)
++            self._updatebranchcache(self._branchcache, self._branchstatecache,
++                                    ctxgen)
++            self._writebranchcache(self._branchcache, self._branchstatecache,
++                                   self.changelog.tip(), tiprev)
+ 
+         # Ensure the persistent tag cache is updated.  Doing it now
+         # means that the tag cache only has to worry about destroyed
+@@ -2543,12 +2566,17 @@ class localrepository(object):
+                 for bheads in rbranchmap.itervalues():
+                     rbheads.extend(bheads)
+ 
+-                self.branchcache = rbranchmap
++                self._branchcache = rbranchmap
++                self._branchstatecache = {}
++                for node in rbheads:
++                    isopen = 'close' not in self.changelog.read(node)[5]
++                    self._branchstatecache[node] = isopen
+                 if rbheads:
+                     rtiprev = max((int(self.changelog.rev(node))
+                             for node in rbheads))
+-                    self._writebranchcache(self.branchcache,
+-                            self[rtiprev].node(), rtiprev)
++                    self._writebranchcache(self._branchcache,
++                                           self._branchstatecache,
++                                           self[rtiprev].node(), rtiprev)
+             self.invalidate()
+             return len(self.heads()) + 1
+         finally:
+# HG changeset patch
+# Parent 2ac08d8b21aa7b6e0a062afed5a3f357ccef67f9
+diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
+--- a/mercurial/scmutil.py
++++ b/mercurial/scmutil.py
+@@ -819,7 +819,24 @@ class filecacheentry(object):
+         try:
+             return util.cachestat(path)
+         except OSError, e:
+-            if e.errno != errno.ENOENT:
++            if e.errno == errno.ENOENT:
++                return None
++            elif e.errno == errno.ESTALE:
++                try:
++                    open(path).close()
++                except OSError, e2:
++                    if e2.errno == errno.ENOENT:
++                        return None
++                    else:
++                        raise
++                try:
++                    return util.cachestat(path)
++                except OSError, e2:
++                    if e.errno == errno.ENOENT:
++                        return None
++                    else:
++                        raise
++            else:
+                 raise
+ 
+ class filecache(object):
+# HG changeset patch
+# Parent 351a9292e430e35766c552066ed3e87c557b803b
+diff --git a/setup.py b/setup.py
+--- a/setup.py
++++ b/setup.py
+@@ -413,11 +413,7 @@ if os.name == 'nt':
+ if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
+     # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
+     # distutils.sysconfig
+-    version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[0].splitlines()[0]
+-    # Also parse only first digit, because 3.2.1 can't be parsed nicely
+-    if (version.startswith('Xcode') and
+-        StrictVersion(version.split()[1]) >= StrictVersion('4.0')):
+-        os.environ['ARCHFLAGS'] = ''
++    os.environ['ARCHFLAGS'] = ''
+ 
+ setup(name='mercurial',
+       version=setupversion,

File hghave-print-missing

+# HG changeset patch
+# Parent 3b5113be190300032cff1ffc0cd3163b7a439a07
+diff --git a/tests/hghave b/tests/hghave
+--- a/tests/hghave
++++ b/tests/hghave
+@@ -287,7 +287,8 @@ def test_features():
+     for name, feature in checks.iteritems():
+         check, _ = feature
+         try:
+-            check()
++            if not check():
++                print 'feature %s missing' % name
+         except Exception, e:
+             print "feature %s failed:  %s" % (name, e)
+             failed += 1
 # HG changeset patch
-# Parent 99f369f5a8dbc192cd520aece8437f3182b1ac50
-pager: add tests
+# User Brodie Rao <brodie@sf.io>
+# Date 1337381249 25200
+# Node ID 79bfc14ebef7a7da2b4bf801f10c000f6bbd2974
+# Parent d0b9ebba41e9a1733294d5fa1b497ada5eda93c8
+pager: add test-pager.t
+
+This test has some issues that should be addressed before being committed:
+
+- It currently fails on Python 2.4 due to a bug in its version of subprocess.
+  See issue3226 for more information.
+
+- For testing purposes, it adds two new internal/undocumented settings:
+  pager.assume-tty and pager.assume-stderr-tty. This logic should probably be
+  moved into ui._isatty(). (The same applies to test-progress.t and
+  progress.assume-tty.)
+
+- It hasn't been tested on Windows. In theory, it should work under MinGW.
 
 diff --git a/hgext/pager.py b/hgext/pager.py
 --- a/hgext/pager.py
 +++ b/hgext/pager.py
-@@ -69,8 +69,11 @@ def _runpager(p):
-         os.dup2(stderr, sys.stderr.fileno())
+@@ -61,7 +61,7 @@ def _runpager(ui, p):
+     stdout = os.dup(sys.stdout.fileno())
+     stderr = os.dup(sys.stderr.fileno())
+     os.dup2(pager.stdin.fileno(), sys.stdout.fileno())
+-    if ui._isatty(sys.stderr):
++    if ui._isatty(sys.stderr) or ui.configbool('pager', 'assume-stderr-tty'):
+         os.dup2(pager.stdin.fileno(), sys.stderr.fileno())
+ 
+     @atexit.register
+@@ -72,7 +72,9 @@ def _runpager(ui, p):
          pager.wait()
  
-+def isatty(ui):
-+    return util.isatty(sys.stdout) or ui.configbool('pager', 'assume-tty')
-+
  def uisetup(ui):
--    if ui.plain() or '--debugger' in sys.argv or not util.isatty(sys.stdout):
-+    if ui.plain() or '--debugger' in sys.argv or not isatty(ui):
+-    if '--debugger' in sys.argv or not ui.formatted():
++    if ('--debugger' in sys.argv or
++        (not ui.formatted() and
++         not ui.configbool('pager', 'assume-tty'))):
          return
  
      def pagecmd(orig, ui, options, cmd, cmdfunc):
 new file mode 100644
 --- /dev/null
 +++ b/tests/test-pager.t
-@@ -0,0 +1,108 @@
+@@ -0,0 +1,112 @@
 +Create a dummy pager script:
 +
 +  $ cat > pager.py <<EOF
 +  writing to stderr
 +  PAGED: writing to stdout
 +  [123]
++  $ hg --config extensions.exit=exit.py --config pager.assume-stderr-tty=1 exit --pager=yes
++  PAGED: writing to stderr
++  PAGED: writing to stdout
++  [123]

File profile-run-tests

+# HG changeset patch
+# Parent 0027a5cec9d00873ca14129b4589975083e5e71d
+diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
+--- a/mercurial/dispatch.py
++++ b/mercurial/dispatch.py
+@@ -8,7 +8,7 @@
+ from i18n import _
+ import os, sys, atexit, signal, pdb, socket, errno, shlex, time, traceback, re
+ import util, commands, hg, fancyopts, extensions, hook, error
+-import cmdutil, encoding
++import cmdutil, encoding, lock
+ import ui as uimod
+ 
+ class request(object):
+@@ -745,7 +745,39 @@ def _dispatch(req):
+         if repo and repo != req.repo:
+             repo.close()
+ 
++def cumulativeprofile(ui, func, fp):
++    if not isinstance(fp, basestring):
++        raise util.Abort(_('profiling.output must be set when using the '
++                           'cumulative profiler'))
++    try:
++        from cProfile import Profile
++    except ImportError:
++        raise util.Abort(_('cProfile not available - Python 2.5 or newer '
++                           'required'))
++    try:
++        from pstats import Stats
++    except ImportError:
++        raise util.Abort(_('pstats not available'))
++
++    p = Profile()
++    p.enable()
++    try:
++        return func()
++    finally:
++        p.disable()
++        l = lock.lock(fp + '.lock')
++        l.lock()
++        if os.path.exists(fp):
++            stats = Stats(fp)
++            stats.add(p)
++        else:
++            stats = Stats(p)
++        stats.dump_stats(fp)
++
+ def lsprofile(ui, func, fp):
++    if isinstance(fp, basestring):
++        fp = open(fp, 'wb')
++
+     format = ui.config('profiling', 'format', default='text')
+     field = ui.config('profiling', 'sort', default='inlinetime')
+     limit = ui.configint('profiling', 'limit', default=30)
+@@ -780,6 +812,9 @@ def lsprofile(ui, func, fp):
+             stats.pprint(limit=limit, file=fp, climit=climit)
+ 
+ def statprofile(ui, func, fp):
++    if isinstance(fp, basestring):
++        fp = open(fp, 'wb')
++
+     try:
+         import statprof
+     except ImportError:
+@@ -806,29 +841,28 @@ def _runcommand(ui, options, cmd, cmdfun
+         except error.SignatureError:
+             raise error.CommandError(cmd, _("invalid arguments"))
+ 
+-    if options['profile']:
++    if options['profile'] or ui.configbool('ui', 'profile'):
++        profilers = {'cumulative': cumulativeprofile,
++                     'ls': lsprofile,
++                     'stat': statprofile}
+         profiler = os.getenv('HGPROF')
+         if profiler is None:
+             profiler = ui.config('profiling', 'type', default='ls')
+-        if profiler not in ('ls', 'stat'):
++        if profiler not in profilers:
+             ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
+             profiler = 'ls'
+ 
+         output = ui.config('profiling', 'output')
+ 
+         if output:
+-            path = ui.expandpath(output)
+-            fp = open(path, 'wb')
++            fp = ui.expandpath(output)
+         else:
+             fp = sys.stderr
+ 
+         try:
+-            if profiler == 'ls':
+-                return lsprofile(ui, checkargs, fp)
+-            else:
+-                return statprofile(ui, checkargs, fp)
++            return profilers[profiler](ui, checkargs, fp)
+         finally:
+-            if output:
++            if not isinstance(fp, basestring):
+                 fp.close()
+     else:
+         return checkargs()
+branch-cache
+profile-run-tests
+testing-stuff
+branchcache
+cache-fail
+fix-xcode
+estale-1
 pager-test
+time-tests
+hghave-print-missing
 pager-help
 cache-branch-close
 multirepo

File testing-stuff

+# HG changeset patch
+# Parent ac4b064dde742fbdeaea4818569ca3d134ed92bf
+diff --git a/mercurial/patch.py b/mercurial/patch.py
+--- a/mercurial/patch.py
++++ b/mercurial/patch.py
+@@ -1708,9 +1708,18 @@ def trydiff(repo, revs, ctx1, ctx2, modi
+         if f not in removed:
+             tn = getfilectx(f, ctx2).data()
+         a, b = f, f
++        print opts.git, losedatafn
+         if opts.git or losedatafn:
++            print 'f', f
++            print 'modified', modified
++            print 'added', added
++            print 'removed', removed
++            print 'f in added', f in added
+             if f in added:
+                 mode = gitmode[ctx2.flags(f)]
++                print 'mode', mode
++                print 'f in copy', f in copy
++                print 'f in copyto', f in copyto
+                 if f in copy or f in copyto:
+                     if opts.git:
+                         if f in copy:
+@@ -1719,6 +1728,8 @@ def trydiff(repo, revs, ctx1, ctx2, modi
+                             a = copyto[f]
+                         omode = gitmode[man1.flags(a)]
+                         addmodehdr(header, omode, mode)
++                        print 'a in removed', a in removed
++                        print 'a not in gone', a not in gone
+                         if a in removed and a not in gone:
+                             op = 'rename'
+                             gone.add(a)
+diff --git a/tests/test-commit-amend.t b/tests/test-commit-amend.t
+--- a/tests/test-commit-amend.t
++++ b/tests/test-commit-amend.t
+@@ -525,6 +525,7 @@ Amend a merge changeset (with renames fr
+   (branches are permanent and global, did you want a bookmark?)
+   $ hg cp a aa
+   $ hg mv z zz
++  $ echo zz >> zz
+   $ echo cc > cc
+   $ hg add cc
+   $ hg ci -m aazzcc
+@@ -537,7 +538,7 @@ Amend a merge changeset (with renames fr
+   2 files updated, 1 files merged, 1 files removed, 0 files unresolved
+   (branch merge, don't forget to commit)
+   $ hg ci -m 'merge bar'
+-  $ hg log --debug -r .
++  $ hg log --config diff.git=1 --debug -p -r .
+   changeset:   23:328607007cf9443df8344c585b87af07e0625c8b
+   tag:         tip
+   phase:       draft
+@@ -560,7 +561,7 @@ Amend a merge changeset (with renames fr
+   $ hg debugrename cc
+   cc not renamed
+   $ hg ci --amend -m 'merge bar (amend message)'
+-  $ hg log --debug -r .
++  $ hg log --config diff.git=1 --debug -r . -p
+   changeset:   24:499c685c2f5e0bc60baff5804b3971fd8716e412
+   tag:         tip
+   phase:       draft
+@@ -585,7 +586,7 @@ Amend a merge changeset (with renames fr
+   cc not renamed
+   $ hg mv zz z
+   $ hg ci --amend -m 'merge bar (undo rename)'
+-  $ hg log --debug -r .
++  $ hg log --config diff.git=1 --debug -p -r .
+   changeset:   26:dc515e583c85f92ca06c2e284ca18bff869df5a8
+   tag:         tip
+   phase:       draft
+@@ -617,8 +618,9 @@ Amend a merge changeset (with renames du
+   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+   (branch merge, don't forget to commit)
+   $ hg mv aa aaa
++  $ echo aa >> aaa
+   $ hg ci -m 'merge bar again'
+-  $ hg log --debug -r .
++  $ hg log --config diff.git=1 --debug -p -r .
+   changeset:   28:805630024adb77fbc52d0d49352e3ac0cf61e999
+   tag:         tip
+   phase:       draft
+@@ -637,9 +639,11 @@ Amend a merge changeset (with renames du
+   $ hg debugrename aaa
+   aaa renamed from aa:37d9b5d994eab34eda9c16b195ace52c7b129980
+   $ hg ci --amend -m 'merge bar again (amend message)'
++  $ hg debugrename aaa
++  aaa renamed from aa:37d9b5d994eab34eda9c16b195ace52c7b129980
+   $ hg mv aaa aa
+   $ hg ci --amend -m 'merge bar again (undo rename)'
+-  $ hg log --debug -r .
++  $ hg log --config diff.git=1 --debug -p -r .
+   changeset:   31:bdb0e875df5a657ff24e3fadcacd6727d3f8005f
+   tag:         tip
+   phase:       draft
+@@ -657,3 +661,5 @@ Amend a merge changeset (with renames du
+   
+   $ hg debugrename aa
+   aa renamed from a:a80d06849b333b8a3d5c445f8ba3142010dcdc9e
++
++  $ hg log --config diff.git=1 --debug -p -f aa
+# HG changeset patch
+# Parent 1deeaee25bd133a2c0f649a936cce22158436cc6
+
+diff --git a/contrib/perfcmd.py b/contrib/perfcmd.py
+new file mode 100644
+--- /dev/null
++++ b/contrib/perfcmd.py
+@@ -0,0 +1,61 @@
++"""Time a command and put the results in /tmp/hg-perfcmd.db.
++
++Test usage:
++
++  $ cd tests
++  $ ./run-tests.py --extra-config-opt=extensions.perfcmd=`pwd`/../contrib/perfcmd.py -j4
++"""
++
++import os
++import sqlite3
++import time
++from mercurial import dispatch, extensions
++
++def uisetup(ui):
++    extensions._order.remove('perfcmd')
++    extensions._extensions.pop('perfcmd', None)
++
++    def timecmd(orig, lui, repo, cmd, fullargs, ui, options, d, cmdpats,
++                cmdoptions):
++        conn = sqlite3.connect('/tmp/hg-perfcmd.db')
++        conn.text_factory = str
++        c = conn.cursor()
++        c.execute("""
++CREATE TABLE IF NOT EXISTS results (cmd text, fullcmd text, test text,
++                                    start real, wall real, user real,
++                                    sys real, comb real);
++                  """)
++        c.execute('CREATE INDEX IF NOT EXISTS cmd_index ON results (cmd);')
++        c.execute('CREATE INDEX IF NOT EXISTS start_index ON results (start);')
++        c.execute('CREATE INDEX IF NOT EXISTS wall_index ON results (wall);')
++        c.execute('CREATE INDEX IF NOT EXISTS user_index ON results (user);')
++        c.execute('CREATE INDEX IF NOT EXISTS sys_index ON results (sys);')
++        c.execute('CREATE INDEX IF NOT EXISTS comb_index ON results (comb);')
++        conn.commit()
++
++        ostart = os.times()
++        cstart = time.time()
++        try:
++            return orig(lui, repo, cmd, fullargs, ui, options, d, cmdpats,
++                        cmdoptions)
++        finally:
++            cstop = time.time()
++            ostop = os.times()
++
++            result = {'cmd': cmd,
++                      'fullcmd': ' '.join(fullargs),
++                      'test': os.environ.get('HGTEST'),
++                      'start': cstart,
++                      'wall': cstop - cstart,
++                      'user': ostop[0] - ostart[0],
++                      'sys': ostop[1] - ostart[1]}
++            result['comb'] = result['user'] + result['sys']
++            c.execute("""
++INSERT INTO results VALUES (:cmd, :fullcmd, :test, :start, :wall, :user,
++                            :sys, :comb);
++                      """, result)
++            conn.commit()
++            c.close()
++            conn.close()
++
++    extensions.wrapfunction(dispatch, 'runcommand', timecmd)
+diff --git a/tests/run-tests.py b/tests/run-tests.py
+--- a/tests/run-tests.py
++++ b/tests/run-tests.py
+@@ -896,6 +896,7 @@ def runone(options, test):
+         replacements.append((re.escape(testtmp), '$TESTTMP'))
+ 
+     os.mkdir(testtmp)
++    os.environ['HGTEST'] = test
+     ret, out = runner(testpath, testtmp, options, replacements)
+     vlog("# Ret was:", ret)
+