Commits

Martin Geisler  committed 9057a02

Add changelog.hidden, move changelog.abandoned to localrepo.abandoned

  • Participants
  • Parent commits aaf94ae

Comments (0)

Files changed (10)

File abandon-command

 # User Martin Geisler <mg@lazybytes.net>
 # Date 1306919141 -7200
 # Node ID 48ec4adb7c86ab9449878134ac0f8e1b30aae8bf
-# Parent 1ffeeb91c55d0b00445ceabde14a0d0faf906a33
+# Parent 0e72f70a828eb5db93fc2f5042a7a229134a2252
 commands: add abandon command
 
 Abandoned changesets are hidden from 'hg log' by default.
+* * *
+changelog: add cache for abandoned
+
+  Some issues:
+
+  * The cache is written inside the store...
+
+  * The delaywriter stuff changelog does was not prepared for creating
+    a file with atomictemp=True.
+
+  Perhaps this suggests that localrepo.heads should be the one that
+  maintains the cache. Most of the code calls repo.heads and not
+  repo.changelog.heads, so this would speedup those calls. However,
+  discovery uses repo.changelog so that might give problems.
+
+The cache maps each abandoned node to the node that abandoned it.
+
+Changes perfabandoned from
+
+  ! wall 0.095847 comb 0.090000 user 0.090000 sys 0.000000 (best of 100)
+
+to
+
+  ! wall 0.000004 comb 0.000000 user 0.000000 sys 0.000000 (best of 345012)
+
+on the OpenOffice repository with 276,000 changesets.
 
 diff --git a/contrib/perf.py b/contrib/perf.py
 --- a/contrib/perf.py
      'perftags': (perftags, []),
      'perfdirstate': (perfdirstate, []),
      'perfdirstatedirs': (perfdirstate, []),
-diff --git a/mercurial/changelog.py b/mercurial/changelog.py
---- a/mercurial/changelog.py
-+++ b/mercurial/changelog.py
-@@ -240,3 +240,24 @@
-         l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
-         text = "\n".join(l)
-         return self.addrevision(text, transaction, len(self), p1, p2)
-+
-+    def abandoned(self, node):
-+        # TODO: need cache
-+        abandoned = set()
-+
-+        for head in self.heads():
-+            extra = self.read(head)[5]
-+            if 'abandon' in extra:
-+                abandoned.update(map(bin, extra['abandon'].split(' ')))
-+                abandoned.add(head)
-+
-+        if node in abandoned:
-+            for head in self.heads():
-+                if head not in abandoned and self.ancestor(head, node) == node:
-+                    return None
-+
-+            return [h for h in self.heads() if
-+                    hex(node) in self.read(h)[5].get('abandon', '').split(' ')
-+                    or node == h][0]
-+
-+        return None
 diff --git a/mercurial/commands.py b/mercurial/commands.py
 --- a/mercurial/commands.py
 +++ b/mercurial/commands.py
  @command('^add',
      walkopts + subrepoopts + dryrunopts,
      _('[OPTION]... [FILE]...'))
-@@ -3281,6 +3293,7 @@
-      _('show changesets within the given named branch'), _('BRANCH')),
-     ('P', 'prune', [],
-      _('do not display revision or any of its ancestors'), _('REV')),
-+    ('', 'abandoned', False, _('show abandoned changesets')),
-     ] + logopts + walkopts,
-     _('[OPTION]... [FILE]'))
- def log(ui, repo, *pats, **opts):
-@@ -3336,6 +3349,8 @@
-         rev = ctx.rev()
-         parents = [p for p in repo.changelog.parentrevs(rev)
-                    if p != nullrev]
-+        if not opts.get('abandoned') and ctx.abandoned():
-+            return
-         if opts.get('no_merges') and len(parents) == 2:
-             return
-         if opts.get('only_merges') and len(parents) != 2:
-diff --git a/mercurial/context.py b/mercurial/context.py
---- a/mercurial/context.py
-+++ b/mercurial/context.py
-@@ -110,6 +110,8 @@
-         return self._changeset[4]
-     def branch(self):
-         return encoding.tolocal(self._changeset[5].get("branch"))
-+    def abandoned(self):
-+        return self._repo.abandoned(self._node)
-     def extra(self):
-         return self._changeset[5]
-     def tags(self):
 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
 --- a/mercurial/localrepo.py
 +++ b/mercurial/localrepo.py
-@@ -517,6 +517,9 @@
+@@ -105,6 +105,9 @@
+ 
+         self._branchcache = None
+         self._branchcachetip = None
++        self._abandonedcache = {}
++        self._abandonedcachetip = nullid
++
+         self.nodetagscache = None
+         self.filterpats = {}
+         self._datafilters = {}
+@@ -174,6 +177,7 @@
+             p = os.environ['HG_PENDING']
+             if p.startswith(self.root):
+                 c.readpending('00changelog.i.a')
++        self.updateabandonedcache(c)
+         return c
+ 
+     @propertycache
+@@ -517,6 +521,98 @@
                      bheads = [b for b in bheads if b not in reachable]
              partial[branch] = bheads
  
-+    def abandoned(self, rev):
-+        return self.changelog.abandoned(self[rev].node())
++    def _readabandonedcache(self, tip):
++        import os
++        if 'DEBUG' in os.environ:
++            print ' reading abandoned cache for', short(tip)
++        abandoned = {}
++        try:
++            f = self.opener("cache/abandoned")
++            lasttip = bin(f.readline().strip())
++            if 'DEBUG' in os.environ:
++                print '  our tip: ', short(tip)
++                print '  last tip:', short(lasttip)
++
++            if tip != lasttip:
++                # stale cache
++                f.close()
++                return None
++
++            for line in f:
++                node, abandoner = line.strip().split()[:2]
++                abandoned[bin(node)] = bin(abandoner)
++            f.close()
++            if 'DEBUG' in os.environ:
++                print ' read up-to-date abandoned cache for', short(lasttip)
++                for k, v in abandoned.iteritems():
++                    print '  ', short(k), '->', short(v)
++            return abandoned
++        except (IOError, OSError), inst:
++            if 'DEBUG' in os.environ:
++                print ' error while reading abandoned cache:'
++                print '  ', inst
++            return None
++
++    def _writeabandonedcache(self, lasttip, abandoned):
++        import os
++        if 'DEBUG' in os.environ:
++            print ' writing abandoned cache for', short(lasttip)
++            for k, v in abandoned.iteritems():
++                print '  ', short(k)[:12], '->', short(v)
++
++        try:
++            f = self.opener("cache/abandoned", "w", atomictemp=True)
++            f.write("%s\n" % hex(lasttip))
++            for node, abandoner in abandoned.iteritems():
++                f.write("%s %s\n" % (hex(node), hex(abandoner)))
++            f.rename()
++        except (IOError, OSError), inst:
++            if 'DEBUG' in os.environ:
++                print ' error while writing abandoned cache:', inst
++            pass
++
++    def updateabandonedcache(self, cl=None):
++        if cl is None:
++            cl = self.changelog
++        import os
++        tip = cl.tip()
++        if tip == self._abandonedcachetip:
++            if 'DEBUG' in os.environ:
++                print 'no need to update cache'
++            return
++
++        if 'DEBUG' in os.environ:
++            print 'must update cache'
++
++        abandoned = self._readabandonedcache(tip)
++        if abandoned is None:
++            if 'DEBUG' in os.environ:
++                print 'found no cache on disk, rebuilding'
++            # no suitable cache found on disk, recompute
++            abandoned = {}
++            heads = cl.heads()
++            for head in heads:
++                extra = cl.read(head)[5]
++                if 'abandon' in extra:
++                    for node in extra['abandon'].split(' '):
++                        abandoned[bin(node)] = head
++                    abandoned[head] = head
++
++            for node in abandoned.copy():
++                for head in heads:
++                    if (head not in abandoned and cl.ancestor(head, node) == node):
++                        del abandoned[node]
++
++            self._writeabandonedcache(tip, abandoned)
++
++        self._abandonedcache = abandoned
++        self._abandonedcachetip = tip
++        cl.hide(*[cl.rev(a) for a in abandoned])
++
++    def abandoned(self, changeid):
++        self.updateabandonedcache()
++        return self._abandonedcache.get(self[changeid].node())
 +
      def lookup(self, key):
          if isinstance(key, int):
 +  $ hg log
 +  0 54dbcd775ef0 init
 +
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
    add
    addremove
    annotate
-@@ -193,7 +195,7 @@
+@@ -194,7 +196,7 @@
    export: output, switch-parent, rev, text, git, nodates
    forget: include, exclude
    init: ssh, remotecmd, insecure
 -  log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, branch, prune, patch, git, limit, no-merges, stat, style, template, include, exclude
-+  log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, branch, prune, abandoned, patch, git, limit, no-merges, stat, style, template, include, exclude
++  log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, branch, prune, hidden, patch, git, limit, no-merges, stat, style, template, include, exclude
    merge: force, tool, rev, preview
    pull: update, force, rev, bookmark, branch, ssh, remotecmd, insecure
    push: force, rev, bookmark, branch, new-branch, ssh, remotecmd, insecure
-@@ -202,6 +204,7 @@
+@@ -203,6 +205,7 @@
    status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos
    summary: remote
    update: clean, check, date, rev

File abandoned-revset

 +
 +new revset keyword
 +
-+  $ hg log --abandoned -r 'abandoned()'
++  $ hg log --hidden -r 'abandoned()'
 +  1 0b00c28422ee x
 +  2 339976ff5010 abandoned

File changelog-abandon-cache

-# HG changeset patch
-# Parent 5a1e69c9704808dde1086f14b7275a9b45ae8d4b
-changelog: add cache for abandoned
-
-  Some issues:
-
-  * The cache is written inside the store...
-
-  * The delaywriter stuff changelog does was not prepared for creating
-    a file with atomictemp=True.
-
-  Perhaps this suggests that localrepo.heads should be the one that
-  maintains the cache. Most of the code calls repo.heads and not
-  repo.changelog.heads, so this would speedup those calls. However,
-  discovery uses repo.changelog so that might give problems.
-
-The cache maps each abandoned node to the node that abandoned it.
-
-Changes perfabandoned from
-
-  ! wall 0.095847 comb 0.090000 user 0.090000 sys 0.000000 (best of 100)
-
-to
-
-  ! wall 0.000004 comb 0.000000 user 0.000000 sys 0.000000 (best of 345012)
-
-on the OpenOffice repository with 276,000 changesets.
-
-diff --git a/mercurial/changelog.py b/mercurial/changelog.py
---- a/mercurial/changelog.py
-+++ b/mercurial/changelog.py
-@@ -5,7 +5,7 @@
- # This software may be used and distributed according to the terms of the
- # GNU General Public License version 2 or any later version.
- 
--from node import bin, hex, nullid
-+from node import bin, hex, nullid, short
- from i18n import _
- import util, error, revlog, encoding
- 
-@@ -88,13 +88,13 @@
-         self.offset += len(s)
- 
- def delayopener(opener, target, divert, buf):
--    def o(name, mode='r'):
-+    def o(name, mode='r',  text=False, atomictemp=False):
-         if name != target:
--            return opener(name, mode)
-+            return opener(name, mode, text, atomictemp)
-         if divert:
--            return opener(name + ".a", mode.replace('a', 'w'))
-+            return opener(name + ".a", mode.replace('a', 'w'), text, atomictemp)
-         # otherwise, divert to memory
--        return appender(opener(name, mode), buf)
-+        return appender(opener(name, mode, text, atomictemp), buf)
-     return o
- 
- class changelog(revlog.revlog):
-@@ -107,6 +107,8 @@
-         self._realopener = opener
-         self._delayed = False
-         self._divert = False
-+        self._abandonedcache = {}
-+        self._abandonedtip = nullid
- 
-     def delayupdate(self):
-         "delay visibility of index updates to other readers"
-@@ -241,23 +243,92 @@
-         text = "\n".join(l)
-         return self.addrevision(text, transaction, len(self), p1, p2)
- 
-+    def _readabandonedcache(self, tip):
-+        import os
-+        if 'DEBUG' in os.environ:
-+            print ' reading abandoned cache for', short(tip)
-+        abandoned = {}
-+        try:
-+            f = self.opener("cache/abandoned")
-+            lasttip = bin(f.readline().strip())
-+            if 'DEBUG' in os.environ:
-+                print '  our tip: ', short(tip)
-+                print '  last tip:', short(lasttip)
-+
-+            if tip != lasttip:
-+                # stale cache
-+                f.close()
-+                return None
-+
-+            for line in f:
-+                node, abandoner = line.strip().split()[:2]
-+                abandoned[bin(node)] = bin(abandoner)
-+            f.close()
-+            if 'DEBUG' in os.environ:
-+                print ' read up-to-date abandoned cache for', short(lasttip)
-+                for k, v in abandoned.iteritems():
-+                    print '  ', short(k), '->', short(v)
-+            return abandoned
-+        except (IOError, OSError), inst:
-+            if 'DEBUG' in os.environ:
-+                print ' error while reading abandoned cache:'
-+                print '  ', inst
-+            return None
-+
-+    def _writeabandonedcache(self, lasttip, abandoned):
-+        import os
-+        if 'DEBUG' in os.environ:
-+            print ' writing abandoned cache for', short(lasttip)
-+            for k, v in abandoned.iteritems():
-+                print '  ', short(k)[:12], '->', short(v)
-+
-+        try:
-+            # TODO: this creates the cache in
-+            # .hg/store/cache/abandoned.
-+            f = self.opener("cache/abandoned", "w", atomictemp=True)
-+            f.write("%s\n" % hex(lasttip))
-+            for node, abandoner in abandoned.iteritems():
-+                f.write("%s %s\n" % (hex(node), hex(abandoner)))
-+            f.rename()
-+        except (IOError, OSError), inst:
-+            if 'DEBUG' in os.environ:
-+                print ' error while writing abandoned cache:', inst
-+            pass
-+
-+    def updateabandonedcache(self):
-+        import os
-+        tip = self.tip()
-+        if tip == self._abandonedtip:
-+            if 'DEBUG' in os.environ:
-+                print 'no need to update cache'
-+            return
-+
-+        if 'DEBUG' in os.environ:
-+            print 'must update cache'
-+
-+        abandoned = self._readabandonedcache(tip)
-+        if abandoned is None:
-+            if 'DEBUG' in os.environ:
-+                print 'found no cache on disk, rebuilding'
-+            # no suitable cache found on disk, recompute
-+            abandoned = {}
-+            for head in self.heads():
-+                extra = self.read(head)[5]
-+                if 'abandon' in extra:
-+                    for node in extra['abandon'].split(' '):
-+                        abandoned[bin(node)] = head
-+                    abandoned[head] = head
-+
-+            for node in abandoned.copy():
-+                for head in self.heads():
-+                    if (head not in abandoned and self.ancestor(head, node) == node):
-+                        del abandoned[node]
-+
-+            self._writeabandonedcache(tip, abandoned)
-+
-+        self._abandonedcache = abandoned
-+        self._abandonedtip = tip
-+
-     def abandoned(self, node):
--        # TODO: need cache
--        abandoned = set()
--
--        for head in self.heads():
--            extra = self.read(head)[5]
--            if 'abandon' in extra:
--                abandoned.update(map(bin, extra['abandon'].split(' ')))
--                abandoned.add(head)
--
--        if node in abandoned:
--            for head in self.heads():
--                if head not in abandoned and self.ancestor(head, node) == node:
--                    return None
--
--            return [h for h in self.heads() if
--                    hex(node) in self.read(h)[5].get('abandon', '').split(' ')
--                    or node == h][0]
--
--        return None
-+        self.updateabandonedcache()
-+        return self._abandonedcache.get(node)
-diff --git a/tests/test-abandoned.t b/tests/test-abandoned.t
---- a/tests/test-abandoned.t
-+++ b/tests/test-abandoned.t
-@@ -40,6 +40,11 @@
-   $ hg log
-   0 54dbcd775ef0 init
- 
-+  $ cat .hg/store/cache/abandoned
-+  339976ff501061539429787f2d34c191ba2f426d
-+  339976ff501061539429787f2d34c191ba2f426d 339976ff501061539429787f2d34c191ba2f426d
-+  0b00c28422ee839b7b5b2a8f0408bd804c350df2 339976ff501061539429787f2d34c191ba2f426d
-+
-   $ hg log --abandoned
-   2 339976ff5010 abandoned
-   1 0b00c28422ee x
 +  $ hg clone repo hardlink-clone
 +  updating to branch default
 +  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-+  $ hg -R hardlink-clone log --abandoned
++  $ hg -R hardlink-clone log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  added 1 changesets with 1 changes to 1 files
 +  updating to branch default
 +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-+  $ hg -R pull-clone log --abandoned
++  $ hg -R pull-clone log --hidden
 +  0 54dbcd775ef0 init
 +  $ rm -r pull-clone
 +
 +  added 1 changesets with 1 changes to 1 files
 +  updating to branch default
 +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-+  $ hg -R ssh-clone log --abandoned
++  $ hg -R ssh-clone log --hidden
 +  0 54dbcd775ef0 init
 +  $ rm -r ssh-clone
 +
 +  added 3 changesets with 2 changes to 2 files
 +  updating to branch default
 +  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-+  $ hg -R full-ssh-clone log --abandoned
++  $ hg -R full-ssh-clone log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  $ hg clone repo hardlink-clone
 +  updating to branch default
 +  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
-+  $ hg -R hardlink-clone log --abandoned
++  $ hg -R hardlink-clone log --hidden
 +  3 4936e3be10e0 y
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  added 3 changesets with 3 changes to 3 files
 +  updating to branch default
 +  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
-+  $ hg -R pull-clone log --abandoned
++  $ hg -R pull-clone log --hidden
 +  2 4936e3be10e0 y
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  added 3 changesets with 3 changes to 3 files
 +  updating to branch default
 +  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
-+  $ hg -R ssh-clone log --abandoned
++  $ hg -R ssh-clone log --hidden
 +  2 4936e3be10e0 y
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  added 4 changesets with 3 changes to 3 files (+1 heads)
 +  updating to branch default
 +  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
-+  $ hg -R full-ssh-clone log --abandoned
++  $ hg -R full-ssh-clone log --hidden
 +  3 4936e3be10e0 y
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 # HG changeset patch
-# Parent 03d6ef99f7b0a862bd737788f415d8f40441f20b
+# Parent a01e1389623ada435e094c75e10b470e9aa16c49
 changelog: exclude abandoned changesets from heads
 
 This effectively cuts off the abandoned changesets from the changelog
 diff --git a/mercurial/changelog.py b/mercurial/changelog.py
 --- a/mercurial/changelog.py
 +++ b/mercurial/changelog.py
-@@ -311,8 +311,9 @@
-             if 'DEBUG' in os.environ:
-                 print 'found no cache on disk, rebuilding'
-             # no suitable cache found on disk, recompute
-+            heads = [self.node(r) for r in self.headrevs()]
-             abandoned = {}
--            for head in self.heads():
-+            for head in heads:
-                 extra = self.read(head)[5]
-                 if 'abandon' in extra:
-                     for node in extra['abandon'].split(' '):
-@@ -320,7 +321,7 @@
-                     abandoned[head] = head
+@@ -5,7 +5,7 @@
+ # This software may be used and distributed according to the terms of the
+ # GNU General Public License version 2 or any later version.
  
-             for node in abandoned.copy():
--                for head in self.heads():
-+                for head in heads:
-                     if (head not in abandoned and self.ancestor(head, node) == node):
-                         del abandoned[node]
+-from node import bin, hex, nullid
++from node import bin, hex, nullid, nullrev, short
+ from i18n import _
+ import util, error, revlog, encoding
  
-@@ -329,6 +330,15 @@
-         self._abandonedcache = abandoned
-         self._abandonedtip = tip
+@@ -109,8 +109,34 @@
+         self._divert = False
+         self._hidden = set()
  
-+    def heads(self, start=None, stop=None, abandoned=False):
-+        if not abandoned:
-+            stop = self.allabandoned().keys()
++    def heads(self, start=None, stop=None, hidden=False):
++        if not hidden:
++            import os
++            # revlog.heads will actually convert nodes in stop back to
++            # revs before using them.
++            stoprevs = set()
++            for rev in self._hidden:
++                for parentrev in self.parentrevs(rev):
++                    if parentrev != nullrev:
++                        stoprevs.add(parentrev)
++
++            if stoprevs:
++                stoproots = stoprevs - set(self.descendants(*stoprevs))
++                stop = map(self.node, stoproots)
++            else:
++                stop = None
++
++            if 'DEBUG'in os.environ:
++                print 'changelog.heads stoprevs:', stoprevs
++                print 'changelog.heads stoproots:', stoproots
++                print 'changelog.heads stop:', map(short, stop)
 +        return revlog.revlog.heads(self, start, stop)
 +
-+    def allabandoned(self):
-+        self.updateabandonedcache()
-+        return self._abandonedcache
-+
-     def abandoned(self, node):
-         self.updateabandonedcache()
-         return self._abandonedcache.get(node)
+     def hide(self, *revs):
+         """Hide one or more revisions."""
++        import os
++        if 'DEBUG'in os.environ:
++            print 'hiding:', revs
+         self._hidden.update(revs)
+ 
+     def hidden(self, rev):
 diff --git a/mercurial/commands.py b/mercurial/commands.py
 --- a/mercurial/commands.py
 +++ b/mercurial/commands.py
-@@ -2502,6 +2502,7 @@
+@@ -2509,6 +2509,7 @@
      ('t', 'topo', False, _('show topological heads only')),
      ('a', 'active', False, _('show active branchheads only (DEPRECATED)')),
      ('c', 'closed', False, _('show normal and closed branch heads')),
-+    ('', 'abandoned', False, _('show abandoned heads')),
++    ('', 'hidden', False, _('show hidden heads')),
      ] + templateopts,
      _('[-ac] [-r STARTREV] [REV]...'))
  def heads(ui, repo, *branchrevs, **opts):
+@@ -2541,7 +2542,7 @@
+         start = scmutil.revsingle(repo, opts['rev'], None).node()
+ 
+     if opts.get('topo'):
+-        heads = [repo[h] for h in repo.heads(start)]
++        heads = [repo[h] for h in repo.heads(start, opts.get('hidden'))]
+     else:
+         heads = []
+         for branch in repo.branchmap():
 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
 --- a/mercurial/localrepo.py
 +++ b/mercurial/localrepo.py
-@@ -1367,8 +1367,8 @@
+@@ -1460,8 +1460,11 @@
              l.sort()
          return r
  
 -    def heads(self, start=None):
 -        heads = self.changelog.heads(start)
-+    def heads(self, start=None, abandoned=False):
-+        heads = self.changelog.heads(start, abandoned=abandoned)
++    def heads(self, start=None, hidden=False):
++        import os
++        if 'DEBUG'in os.environ:
++            print 'localrepo.heads: start=%s, hidden=%s' % (start, hidden)
++        heads = self.changelog.heads(start, hidden)
          # sort the output in rev descending order
          return sorted(heads, key=self.changelog.rev, reverse=True)
  
 diff --git a/tests/test-abandoned.t b/tests/test-abandoned.t
 --- a/tests/test-abandoned.t
 +++ b/tests/test-abandoned.t
-@@ -50,6 +50,10 @@
+@@ -45,6 +45,14 @@
    1 0b00c28422ee x
    0 54dbcd775ef0 init
  
-+abandoned changesets are hidden in heads
++abandoned changesets are hidden in heads by default
 +
-+  $ hg heads
++  $ hg heads --topo
++  0 54dbcd775ef0 init
++
++  $ hg heads --hidden
++  2 339976ff5010 abandoned
 +
  new revset keyword
  
-   $ hg log --abandoned -r 'abandoned()'
+   $ hg log --hidden -r 'abandoned()'
+diff --git a/tests/test-debugcomplete.t b/tests/test-debugcomplete.t
+--- a/tests/test-debugcomplete.t
++++ b/tests/test-debugcomplete.t
+@@ -245,7 +245,7 @@
+   debugwalk: include, exclude
+   debugwireargs: three, four, five, ssh, remotecmd, insecure
+   grep: print0, all, text, follow, ignore-case, files-with-matches, line-number, rev, user, date, include, exclude
+-  heads: rev, topo, active, closed, style, template
++  heads: rev, topo, active, closed, hidden, style, template
+   help: extension, command
+   identify: rev, num, id, branch, tags, bookmarks
+   import: strip, base, force, no-commit, exact, import-branch, message, logfile, date, user, similarity
+# HG changeset patch
+# Parent a67e866f46f9919a963afb4e4f1bb4a2c56a3c42
+changelog: add infrastructure for hiding changesets
+
+diff --git a/mercurial/changelog.py b/mercurial/changelog.py
+--- a/mercurial/changelog.py
++++ b/mercurial/changelog.py
+@@ -107,6 +107,15 @@
+         self._realopener = opener
+         self._delayed = False
+         self._divert = False
++        self._hidden = set()
++
++    def hide(self, *revs):
++        """Hide one or more revisions."""
++        self._hidden.update(revs)
++
++    def hidden(self, rev):
++        """Is rev hidden?"""
++        return rev in self._hidden
+ 
+     def delayupdate(self):
+         "delay visibility of index updates to other readers"
+diff --git a/mercurial/commands.py b/mercurial/commands.py
+--- a/mercurial/commands.py
++++ b/mercurial/commands.py
+@@ -3288,6 +3288,7 @@
+      _('show changesets within the given named branch'), _('BRANCH')),
+     ('P', 'prune', [],
+      _('do not display revision or any of its ancestors'), _('REV')),
++    ('h', 'hidden', False, _('show hidden changesets')),
+     ] + logopts + walkopts,
+     _('[OPTION]... [FILE]'))
+ def log(ui, repo, *pats, **opts):
+@@ -3349,6 +3350,8 @@
+             return
+         if opts.get('branch') and ctx.branch() not in opts['branch']:
+             return
++        if not opts.get('hidden') and ctx.hidden():
++            return
+         if df and not df(ctx.date()[0]):
+             return
+         if opts['user'] and not [k for k in opts['user']
+diff --git a/mercurial/context.py b/mercurial/context.py
+--- a/mercurial/context.py
++++ b/mercurial/context.py
+@@ -110,6 +110,8 @@
+         return self._changeset[4]
+     def branch(self):
+         return encoding.tolocal(self._changeset[5].get("branch"))
++    def hidden(self):
++        return self._repo.changelog.hidden(self._rev)
+     def extra(self):
+         return self._changeset[5]
+     def tags(self):
 # User Martin Geisler <mg@lazybytes.net>
 # Date 1306919172 -7200
 # Node ID 42c450076c85e537049306f87b4d2bdffbd2d926
-# Parent 459996b74ce052c8b055bef1192badf464abae28
+# Parent e885e5b30465337a6dd9e663a03d56f6e378c1d6
 commands: add --abandoned flag to push, pull, outgoing, and incoming
 
 The flag makes the commands include abandoned changesets, the default
 diff --git a/mercurial/commands.py b/mercurial/commands.py
 --- a/mercurial/commands.py
 +++ b/mercurial/commands.py
-@@ -3171,6 +3171,7 @@
+@@ -3178,6 +3178,7 @@
  @command('incoming|in',
      [('f', 'force', None,
       _('run even if remote repository is unrelated')),
      ('n', 'newest-first', None, _('show newest record first')),
      ('', 'bundle', '',
       _('file to store the bundles into'), _('FILE')),
-@@ -3542,6 +3543,7 @@
+@@ -3549,6 +3550,7 @@
  
  @command('outgoing|out',
      [('f', 'force', None, _('run even when the destination is unrelated')),
      ('r', 'rev', [],
       _('a changeset intended to be included in the destination'), _('REV')),
      ('n', 'newest-first', None, _('show newest record first')),
-@@ -3690,6 +3692,7 @@
+@@ -3697,6 +3699,7 @@
      [('u', 'update', None,
       _('update to new branch head if changesets were pulled')),
      ('f', 'force', None, _('run even when remote repository is unrelated')),
      ('r', 'rev', [], _('a remote changeset intended to be added'), _('REV')),
      ('B', 'bookmark', [], _("bookmark to pull"), _('BOOKMARK')),
      ('b', 'branch', [], _('a specific branch you would like to pull'),
-@@ -3738,7 +3741,8 @@
+@@ -3745,7 +3748,8 @@
                      "so a rev cannot be specified.")
              raise util.Abort(err)
  
      bookmarks.updatefromremote(ui, repo, other)
      if checkout:
          checkout = str(repo.changelog.rev(other.lookup(checkout)))
-@@ -3761,6 +3765,7 @@
+@@ -3768,6 +3772,7 @@
  
  @command('^push',
      [('f', 'force', None, _('force push')),
      ('r', 'rev', [],
       _('a changeset intended to be included in the destination'),
       _('REV')),
-@@ -3829,7 +3834,8 @@
+@@ -3836,7 +3841,8 @@
      finally:
          del repo._subtoppath
      result = repo.push(other, opts.get('force'), revs=revs,
 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
 --- a/mercurial/localrepo.py
 +++ b/mercurial/localrepo.py
-@@ -520,6 +520,96 @@
-     def abandoned(self, rev):
-         return self.changelog.abandoned(self[rev].node())
+@@ -613,6 +613,96 @@
+         self.updateabandonedcache()
+         return self._abandonedcache.get(self[changeid].node())
  
 +    def nonabandoned(self, candidates, clientheads):
 +        """remove unnecessary abandoned heads from candidates
 +
 +
 +        for h in candidates:
-+            abandoner = self[h].abandoned()
++            abandoner = self.abandoned(h)
 +
 +            debug('   our head', short(h), 'is abandoned:', bool(abandoner))
 +            if not abandoner:
 +                    # rh is a descendant of root
 +                    debug('     ', short(rh), 'is a descendant of root', short(root))
 +                    addhead(rh)
-+                    if self[rh].abandoned():
-+                        addhead(self[rh].abandoned())
++                    if self.abandoned(rh):
++                        addhead(self.abandoned(rh))
 +                    continue
 +
 +                if an == rh:
      def lookup(self, key):
          if isinstance(key, int):
              return self.changelog.node(key)
-@@ -1339,11 +1429,11 @@
+@@ -1432,11 +1522,11 @@
  
          return r
  
              common, fetch, rheads = tmp
              if not fetch:
                  self.ui.status(_("no changes found\n"))
-@@ -1380,7 +1470,8 @@
+@@ -1473,7 +1563,8 @@
          """
          pass
  
          '''Push outgoing changesets (limited by revs) from the current
          repository to remote. Return an integer:
            - 0 means HTTP error *or* nothing to push
-@@ -1403,7 +1494,7 @@
+@@ -1496,7 +1587,7 @@
              lock = remote.lock()
          try:
              cg, remote_heads = discovery.prepush(self, remote, force, revs,
 diff --git a/tests/test-abandoned.t b/tests/test-abandoned.t
 --- a/tests/test-abandoned.t
 +++ b/tests/test-abandoned.t
-@@ -54,3 +54,576 @@
-   $ hg log --abandoned -r 'abandoned()'
+@@ -49,3 +49,576 @@
+   $ hg log --hidden -r 'abandoned()'
    1 0b00c28422ee x
    2 339976ff5010 abandoned
 +
 +  adding manifests
 +  adding file changes
 +  added 1 changesets with 1 changes to 1 files
-+  $ hg -R ../empty log --abandoned
++  $ hg -R ../empty log --hidden
 +  0 54dbcd775ef0 init
 +  $ hg -R ../empty rollback -q
 +
 +  adding manifests
 +  adding file changes
 +  added 3 changesets with 2 changes to 2 files
-+  $ hg -R ../empty log --abandoned
++  $ hg -R ../empty log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding manifests
 +  adding file changes
 +  added 2 changesets with 1 changes to 1 files
-+  $ hg -R ../base-i log --abandoned
++  $ hg -R ../base-i log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding manifests
 +  adding file changes
 +  added 1 changesets with 0 changes to 0 files
-+  $ hg -R ../base-x log --abandoned
++  $ hg -R ../base-x log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding manifests
 +  adding file changes
 +  added 1 changesets with 0 changes to 0 files
-+  $ hg -R ../base-x log --abandoned
++  $ hg -R ../base-x log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding file changes
 +  added 1 changesets with 1 changes to 1 files
 +  (run 'hg update' to get a working copy)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  0 54dbcd775ef0 init
 +  $ hg rollback -q
 +
 +  adding file changes
 +  added 3 changesets with 2 changes to 2 files
 +  (run 'hg update' to get a working copy)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding file changes
 +  added 2 changesets with 1 changes to 1 files
 +  (run 'hg update' to get a working copy)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding file changes
 +  added 1 changesets with 0 changes to 0 files
 +  (run 'hg update' to get a working copy)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding file changes
 +  added 1 changesets with 0 changes to 0 files
 +  (run 'hg update' to get a working copy)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding manifests
 +  adding file changes
 +  added 3 changesets with 3 changes to 3 files
-+  $ hg -R ../empty log --abandoned
++  $ hg -R ../empty log --hidden
 +  2 4936e3be10e0 y
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding manifests
 +  adding file changes
 +  added 2 changesets with 2 changes to 2 files
-+  $ hg -R ../base-i log --abandoned
++  $ hg -R ../base-i log --hidden
 +  2 4936e3be10e0 y
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding manifests
 +  adding file changes
 +  added 4 changesets with 3 changes to 3 files (+1 heads)
-+  $ hg -R ../empty log --abandoned
++  $ hg -R ../empty log --hidden
 +  3 4936e3be10e0 y
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  adding manifests
 +  adding file changes
 +  added 3 changesets with 2 changes to 2 files (+1 heads)
-+  $ hg -R ../base-i log --abandoned
++  $ hg -R ../base-i log --hidden
 +  3 4936e3be10e0 y
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  adding manifests
 +  adding file changes
 +  added 1 changesets with 1 changes to 1 files
-+  $ hg -R ../base-x log --abandoned
++  $ hg -R ../base-x log --hidden
 +  2 4936e3be10e0 y
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding manifests
 +  adding file changes
 +  added 2 changesets with 1 changes to 1 files (+1 heads)
-+  $ hg -R ../base-x log --abandoned
++  $ hg -R ../base-x log --hidden
 +  3 4936e3be10e0 y
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  adding file changes
 +  added 3 changesets with 3 changes to 3 files
 +  (run 'hg update' to get a working copy)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  2 4936e3be10e0 y
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding file changes
 +  added 2 changesets with 2 changes to 2 files
 +  (run 'hg update' to get a working copy)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  2 4936e3be10e0 y
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding file changes
 +  added 4 changesets with 3 changes to 3 files (+1 heads)
 +  (run 'hg heads' to see heads, 'hg merge' to merge)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  3 4936e3be10e0 y
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  adding file changes
 +  added 3 changesets with 2 changes to 2 files (+1 heads)
 +  (run 'hg heads' to see heads, 'hg merge' to merge)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  3 4936e3be10e0 y
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  adding file changes
 +  added 1 changesets with 1 changes to 1 files
 +  (run 'hg update' to get a working copy)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  2 4936e3be10e0 y
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
 +  adding file changes
 +  added 2 changesets with 1 changes to 1 files (+1 heads)
 +  (run 'hg heads' to see heads, 'hg merge' to merge)
-+  $ hg log --abandoned
++  $ hg log --hidden
 +  3 4936e3be10e0 y
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x

File postincoming

 # HG changeset patch
-# Parent 6c8234ed761d6ae765377dd16ffd8033c2afbb58
+# Parent 582777fbdc5174bae2b07ed2eea64066c6849319
 postincoming: output a warning if the working copy is abandoned
 
 diff --git a/mercurial/commands.py b/mercurial/commands.py
 --- a/mercurial/commands.py
 +++ b/mercurial/commands.py
-@@ -3679,7 +3679,7 @@
+@@ -3675,7 +3675,7 @@
              else:
                  ui.write("%s = %s\n" % (name, util.hidepassword(path)))
  
      if modheads == 0:
          return
      if optupdate:
-@@ -3688,6 +3688,8 @@
+@@ -3684,6 +3684,8 @@
          except util.Abort, inst:
              ui.warn(_("not updating: %s\n" % str(inst)))
              return 0
-+    if not abandoned and repo['.'].abandoned():
++    if not abandoned and repo.abandoned('.'):
 +        ui.warn(_("working copy has been abandoned\n"))
      if modheads > 1:
          currentbranchheads = len(repo.branchheads())
          if currentbranchheads == modheads:
-@@ -3752,6 +3754,7 @@
+@@ -3748,6 +3750,7 @@
                      "so a rev cannot be specified.")
              raise util.Abort(err)
  
      modheads = repo.pull(other, heads=revs, force=opts.get('force'),
                           abandoned=opts.get('abandoned'))
      bookmarks.updatefromremote(ui, repo, other)
-@@ -3759,8 +3762,8 @@
+@@ -3755,8 +3758,8 @@
          checkout = str(repo.changelog.rev(other.lookup(checkout)))
      repo._subtoppath = source
      try:
      finally:
          del repo._subtoppath
  
-@@ -4986,6 +4989,7 @@
+@@ -4982,6 +4985,7 @@
  
      lock = repo.lock()
      wc = repo['.']
-+    abandoned = wc.abandoned()
++    abandoned = repo.abandoned('.')
      try:
          for fname in fnames:
              f = url.open(ui, fname)
-@@ -4995,7 +4999,8 @@
+@@ -4991,7 +4995,8 @@
          bookmarks.updatecurrentbookmark(repo, wc.node(), wc.branch())
      finally:
          lock.release()
 diff --git a/tests/test-abandoned.t b/tests/test-abandoned.t
 --- a/tests/test-abandoned.t
 +++ b/tests/test-abandoned.t
-@@ -389,6 +389,7 @@
+@@ -383,6 +383,7 @@
    adding manifests
    adding file changes
    added 1 changesets with 0 changes to 0 files
 +  working copy has been abandoned
    (run 'hg update' to get a working copy)
-   $ hg log --abandoned
+   $ hg log --hidden
    2 339976ff5010 abandoned
-@@ -403,6 +404,7 @@
+@@ -397,6 +398,7 @@
    adding manifests
    adding file changes
    added 1 changesets with 0 changes to 0 files
 +  working copy has been abandoned
    (run 'hg update' to get a working copy)
-   $ hg log --abandoned
+   $ hg log --hidden
    2 339976ff5010 abandoned
+hidden
 abandon-command
-changelog-abandon-cache
 abandoned-revset
 localrepo
 wireproto
 # User Martin Geisler <mg@lazybytes.net>
 # Date 1306923811 -7200
 # Node ID dc6390ced27044d8711da7995a93f6babdacfba3
-# Parent c64031c2591a669eb251e96e9cc0b91776413187
+# Parent 55f74ab4ce0956aa0d1e3583f697d508b260422f
 wireproto: add nonabandoned wireproto command
 
 Used with httprepo and sshrepo to let the server prune the list of
 +  remote: adding manifests
 +  remote: adding file changes
 +  remote: added 1 changesets with 1 changes to 1 files
-+  $ hg -R ../empty log --abandoned
++  $ hg -R ../empty log --hidden
 +  0 54dbcd775ef0 init
 +  $ hg -R ../empty rollback -q
 +
 +  remote: adding manifests
 +  remote: adding file changes
 +  remote: added 3 changesets with 2 changes to 2 files
-+  $ hg -R ../empty log --abandoned
++  $ hg -R ../empty log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
    > [defaults]
    > abandon = -d "0 0"
    > [extensions]
-@@ -65,12 +66,23 @@
+@@ -60,12 +61,23 @@
    searching for changes
    0 54dbcd775ef0 init
  
  testing outgoing --abandoned with no shared abandoned changesets
  
    $ hg out --abandoned ../empty
-@@ -80,12 +92,25 @@
+@@ -75,12 +87,25 @@
    1 0b00c28422ee x
    2 339976ff5010 abandoned
  
  testing outgoing with a shared abandoned changeset
  
    $ hg out ../base-x
-@@ -93,11 +118,21 @@
+@@ -88,11 +113,21 @@
    searching for changes
    2 339976ff5010 abandoned
  
  hg push
  =======
  
-@@ -114,11 +149,27 @@
+@@ -109,11 +144,27 @@
    0 54dbcd775ef0 init
    $ hg -R ../empty rollback -q
  
 +  remote: adding manifests
 +  remote: adding file changes
 +  remote: added 1 changesets with 1 changes to 1 files
-+  $ hg -R ../empty log --abandoned
++  $ hg -R ../empty log --hidden
 +  0 54dbcd775ef0 init
 +  $ hg -R ../empty rollback -q
 +
  testing push --abandoned with no shared abandoned changesets
  
    $ hg push --abandoned ../empty
-@@ -134,6 +185,19 @@
+@@ -129,6 +180,19 @@
    0 54dbcd775ef0 init
    $ hg -R ../empty rollback -q
  
 +  remote: adding manifests
 +  remote: adding file changes
 +  remote: added 3 changesets with 2 changes to 2 files
-+  $ hg -R ../empty log --abandoned
++  $ hg -R ../empty log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
    $ hg push --abandoned ../base-i
    pushing to ../base-i
    searching for changes
-@@ -162,6 +226,19 @@
+@@ -157,6 +221,19 @@
    0 54dbcd775ef0 init
    $ hg -R ../base-x rollback -q
  
 +  remote: adding manifests
 +  remote: adding file changes
 +  remote: added 1 changesets with 0 changes to 0 files
-+  $ hg -R ../base-x log --abandoned
++  $ hg -R ../base-x log --hidden
 +  2 339976ff5010 abandoned
 +  1 0b00c28422ee x
 +  0 54dbcd775ef0 init
    $ hg push --abandoned ../base-x
    pushing to ../base-x
    searching for changes
-@@ -185,6 +262,11 @@
+@@ -180,6 +257,11 @@
    comparing with ../repo
    0 54dbcd775ef0 init
  
    $ cd ../base-i
    $ hg in ../repo
    comparing with ../repo
-@@ -192,6 +274,13 @@
+@@ -187,6 +269,13 @@
    no changes found
    [1]
  
  testing incoming --abandoned with no shared abandoned changesets
  
    $ cd ../empty
-@@ -201,6 +290,12 @@
+@@ -196,6 +285,12 @@
    1 0b00c28422ee x
    2 339976ff5010 abandoned
  
    $ cd ../base-i
    $ hg in --abandoned ../repo
    comparing with ../repo
-@@ -208,6 +303,12 @@
+@@ -203,6 +298,12 @@
    1 0b00c28422ee x
    2 339976ff5010 abandoned
  
 diff --git a/tests/test-debugcomplete.t b/tests/test-debugcomplete.t
 --- a/tests/test-debugcomplete.t
 +++ b/tests/test-debugcomplete.t
-@@ -197,8 +197,8 @@
+@@ -198,8 +198,8 @@
    init: ssh, remotecmd, insecure
-   log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, branch, prune, abandoned, patch, git, limit, no-merges, stat, style, template, include, exclude
+   log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, branch, prune, hidden, patch, git, limit, no-merges, stat, style, template, include, exclude
    merge: force, tool, rev, preview
 -  pull: update, force, rev, bookmark, branch, ssh, remotecmd, insecure
 -  push: force, rev, bookmark, branch, new-branch, ssh, remotecmd, insecure
    remove: after, force, include, exclude
    serve: accesslog, daemon, daemon-pipefds, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, templates, style, ipv6, certificate
    status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos
-@@ -247,10 +247,10 @@
+@@ -249,10 +249,10 @@
    help: extension, command
    identify: rev, num, id, branch, tags, bookmarks
    import: strip, base, force, no-commit, exact, import-branch, message, logfile, date, user, similarity