hgext/obsolete.py
branchstable
changeset 499 a9c27df23129
parent 491 6989d8fe4ed2
child 519 9825c7da5b54
equal deleted inserted replaced
476:f17a0f801e0b 499:a9c27df23129
     3 # Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
     3 # Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
     4 #                Logilab SA        <contact@logilab.fr>
     4 #                Logilab SA        <contact@logilab.fr>
     5 #
     5 #
     6 # This software may be used and distributed according to the terms of the
     6 # This software may be used and distributed according to the terms of the
     7 # GNU General Public License version 2 or any later version.
     7 # GNU General Public License version 2 or any later version.
     8 """Introduce the Obsolete concept to mercurial
     8 """Deprecated extension that formely introduces "Changeset Obsolescence".
     9 
     9 
    10 General concept
    10 This concept is now partially in Mercurial core (starting with mercurial 2.3). The remaining logic have been grouped with the evolve extension.
    11 ===============
       
    12 
    11 
    13 This extension introduces the *obsolete* concept. It adds a new *obsolete*
    12 Some code cemains in this extensions to detect and convert prehistoric format of obsolete marker than early user may have create. Keep it enabled if you were such user.
    14 relation between two changesets. A relation ``<changeset B> obsolete <changeset
    13 """
    15 A>`` is set to denote that ``<changeset B>`` is new version of ``<changeset
       
    16 A>``.
       
    17 
    14 
    18 The *obsolete* relation act as a **perpendicular history** to the standard
    15 from mercurial import util
    19 changeset history. Standard changeset history versions files. The *obsolete*
       
    20 relation versions changesets.
       
    21 
    16 
    22 :obsolete:     a changeset that has been replaced by another one.
    17 try:
    23 :unstable:     a changeset that is not obsolete but has an obsolete ancestor.
    18     from mercurial import obsolete
    24 :suspended:    an obsolete changeset with unstable descendant.
    19     if not obsolete._enabled:
    25 :extinct:      an obsolete changeset without unstable descendant.
    20         obsolete._enabled = True
    26                (subject to garbage collection)
    21 except ImportError:
       
    22     raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)')
    27 
    23 
    28 Another name for unstable could be out of sync.
    24 import sys
       
    25 import json
       
    26 
       
    27 from mercurial import cmdutil
       
    28 from mercurial import error
       
    29 from mercurial.node import bin, nullid
    29 
    30 
    30 
    31 
    31 Usage and Feature
    32 #####################################################################
    32 =================
    33 ### Older format management                                       ###
       
    34 #####################################################################
    33 
    35 
    34 Display and Exchange
    36 # Code related to detection and management of older legacy format never
    35 --------------------
    37 # handled by core
    36 
       
    37 obsolete changesets are hidden. (except if they have non obsolete changeset)
       
    38 
       
    39 obsolete changesets are not exchanged. This will probably change later but it
       
    40 was the simpler solution for now.
       
    41 
       
    42 New commands
       
    43 ------------
       
    44 
       
    45 Note that rebased changesets are now marked obsolete instead of being stripped.
       
    46 
       
    47 Context object
       
    48 --------------
       
    49 
       
    50 Context gains a ``obsolete`` method that will return True if a changeset is
       
    51 obsolete False otherwise.
       
    52 
       
    53 revset
       
    54 ------
       
    55 
       
    56 Add an ``obsolete()`` entry.
       
    57 
       
    58 repo extension
       
    59 --------------
       
    60 
       
    61 To Do
       
    62 ~~~~~
       
    63 
       
    64 - refuse to obsolete published changesets
       
    65 
       
    66 - handle split
       
    67 
       
    68 - handle conflict
       
    69 
       
    70 - handle unstable // out of sync
       
    71 
       
    72 """
       
    73 
       
    74 import os
       
    75 try:
       
    76     from cStringIO import StringIO
       
    77 except ImportError:
       
    78     from StringIO import StringIO
       
    79 
       
    80 from mercurial.i18n import _
       
    81 
       
    82 import base64
       
    83 import json
       
    84 
       
    85 import struct
       
    86 from mercurial import util, base85
       
    87 
       
    88 _pack = struct.pack
       
    89 _unpack = struct.unpack
       
    90 
       
    91 from mercurial import util
       
    92 from mercurial import context
       
    93 from mercurial import revset
       
    94 from mercurial import scmutil
       
    95 from mercurial import extensions
       
    96 from mercurial import pushkey
       
    97 from mercurial import discovery
       
    98 from mercurial import error
       
    99 from mercurial import commands
       
   100 from mercurial import changelog
       
   101 from mercurial import phases
       
   102 from mercurial.node import hex, bin, short, nullid
       
   103 from mercurial.lock import release
       
   104 from mercurial import localrepo
       
   105 from mercurial import cmdutil
       
   106 from mercurial import templatekw
       
   107 
       
   108 try:
       
   109     from mercurial.localrepo import storecache
       
   110     storecache('babar') # to trigger import
       
   111 except (TypeError, ImportError):
       
   112     def storecache(*args):
       
   113         return scmutil.filecache(*args, instore=True)
       
   114 
    38 
   115 
    39 
   116 ### Patch changectx
    40 def reposetup(ui, repo):
   117 #############################
    41     """Detect that a repo still contains some old obsolete format
   118 
       
   119 def obsolete(ctx):
       
   120     """is the changeset obsolete by other"""
       
   121     if ctx.node()is None:
       
   122         return False
       
   123     return bool(ctx._repo.obsoletedby(ctx.node())) and ctx.phase()
       
   124 
       
   125 context.changectx.obsolete = obsolete
       
   126 
       
   127 def unstable(ctx):
       
   128     """is the changeset unstable (have obsolete ancestor)"""
       
   129     if ctx.node() is None:
       
   130         return False
       
   131     return ctx.rev() in ctx._repo._unstableset
       
   132 
       
   133 context.changectx.unstable = unstable
       
   134 
       
   135 def extinct(ctx):
       
   136     """is the changeset extinct by other"""
       
   137     if ctx.node() is None:
       
   138         return False
       
   139     return ctx.rev() in ctx._repo._extinctset
       
   140 
       
   141 context.changectx.extinct = extinct
       
   142 
       
   143 def latecomer(ctx):
       
   144     """is the changeset latecomer (Try to succeed to public change)"""
       
   145     if ctx.node() is None:
       
   146         return False
       
   147     return ctx.rev() in ctx._repo._latecomerset
       
   148 
       
   149 context.changectx.latecomer = latecomer
       
   150 
       
   151 def conflicting(ctx):
       
   152     """is the changeset conflicting (Try to succeed to public change)"""
       
   153     if ctx.node() is None:
       
   154         return False
       
   155     return ctx.rev() in ctx._repo._conflictingset
       
   156 
       
   157 context.changectx.conflicting = conflicting
       
   158 
       
   159 
       
   160 ### revset
       
   161 #############################
       
   162 
       
   163 def revsethidden(repo, subset, x):
       
   164     """hidden changesets"""
       
   165     args = revset.getargs(x, 0, 0, 'hidden takes no argument')
       
   166     return [r for r in subset if r in repo.changelog.hiddenrevs]
       
   167 
       
   168 def revsetobsolete(repo, subset, x):
       
   169     """obsolete changesets"""
       
   170     args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
       
   171     return [r for r in subset if r in repo._obsoleteset and repo._phasecache.phase(repo, r) > 0]
       
   172 
       
   173 # XXX Backward compatibility, to be removed once stabilized
       
   174 if '_phasecache' not in vars(localrepo.localrepository): # new api
       
   175     def revsetobsolete(repo, subset, x):
       
   176         """obsolete changesets"""
       
   177         args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
       
   178         return [r for r in subset if r in repo._obsoleteset and repo._phaserev[r] > 0]
       
   179 
       
   180 def revsetunstable(repo, subset, x):
       
   181     """non obsolete changesets descendant of obsolete one"""
       
   182     args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
       
   183     return [r for r in subset if r in repo._unstableset]
       
   184 
       
   185 def revsetsuspended(repo, subset, x):
       
   186     """obsolete changesets with non obsolete descendants"""
       
   187     args = revset.getargs(x, 0, 0, 'suspended takes no arguments')
       
   188     return [r for r in subset if r in repo._suspendedset]
       
   189 
       
   190 def revsetextinct(repo, subset, x):
       
   191     """obsolete changesets without obsolete descendants"""
       
   192     args = revset.getargs(x, 0, 0, 'extinct takes no arguments')
       
   193     return [r for r in subset if r in repo._extinctset]
       
   194 
       
   195 def revsetlatecomer(repo, subset, x):
       
   196     """latecomer, Try to succeed to public change"""
       
   197     args = revset.getargs(x, 0, 0, 'latecomer takes no arguments')
       
   198     return [r for r in subset if r in repo._latecomerset]
       
   199 
       
   200 def revsetconflicting(repo, subset, x):
       
   201     """conflicting, Try to succeed to public change"""
       
   202     args = revset.getargs(x, 0, 0, 'conflicting takes no arguments')
       
   203     return [r for r in subset if r in repo._conflictingset]
       
   204 
       
   205 def _precursors(repo, s):
       
   206     """Precursor of a changeset"""
       
   207     cs = set()
       
   208     nm = repo.changelog.nodemap
       
   209     markerbysubj = repo.obsstore.successors
       
   210     for r in s:
       
   211         for p in markerbysubj.get(repo[r].node(), ()):
       
   212             pr = nm.get(p[0])
       
   213             if pr is not None:
       
   214                 cs.add(pr)
       
   215     return cs
       
   216 
       
   217 def revsetprecursors(repo, subset, x):
       
   218     """precursors of a subset"""
       
   219     s = revset.getset(repo, range(len(repo)), x)
       
   220     cs = _precursors(repo, s)
       
   221     return [r for r in subset if r in cs]
       
   222 
       
   223 def _allprecursors(repo, s):  # XXX we need a better naming
       
   224     """transitive precursors of a subset"""
       
   225     toproceed = [repo[r].node() for r in s]
       
   226     seen = set()
       
   227     allsubjects = repo.obsstore.successors
       
   228     while toproceed:
       
   229         nc = toproceed.pop()
       
   230         for mark in allsubjects.get(nc, ()):
       
   231             np = mark[0]
       
   232             if np not in seen:
       
   233                 seen.add(np)
       
   234                 toproceed.append(np)
       
   235     nm = repo.changelog.nodemap
       
   236     cs = set()
       
   237     for p in seen:
       
   238         pr = nm.get(p)
       
   239         if pr is not None:
       
   240             cs.add(pr)
       
   241     return cs
       
   242 
       
   243 def revsetallprecursors(repo, subset, x):
       
   244     """obsolete parents"""
       
   245     s = revset.getset(repo, range(len(repo)), x)
       
   246     cs = _allprecursors(repo, s)
       
   247     return [r for r in subset if r in cs]
       
   248 
       
   249 def _successors(repo, s):
       
   250     """Successors of a changeset"""
       
   251     cs = set()
       
   252     nm = repo.changelog.nodemap
       
   253     markerbyobj = repo.obsstore.precursors
       
   254     for r in s:
       
   255         for p in markerbyobj.get(repo[r].node(), ()):
       
   256             for sub in p[1]:
       
   257                 sr = nm.get(sub)
       
   258                 if sr is not None:
       
   259                     cs.add(sr)
       
   260     return cs
       
   261 
       
   262 def revsetsuccessors(repo, subset, x):
       
   263     """successors of a subset"""
       
   264     s = revset.getset(repo, range(len(repo)), x)
       
   265     cs = _successors(repo, s)
       
   266     return [r for r in subset if r in cs]
       
   267 
       
   268 def _allsuccessors(repo, s):  # XXX we need a better naming
       
   269     """transitive successors of a subset"""
       
   270     toproceed = [repo[r].node() for r in s]
       
   271     seen = set()
       
   272     allobjects = repo.obsstore.precursors
       
   273     while toproceed:
       
   274         nc = toproceed.pop()
       
   275         for mark in allobjects.get(nc, ()):
       
   276             for sub in mark[1]:
       
   277                 if sub == nullid:
       
   278                     continue # should not be here!
       
   279                 if sub not in seen:
       
   280                     seen.add(sub)
       
   281                     toproceed.append(sub)
       
   282     nm = repo.changelog.nodemap
       
   283     cs = set()
       
   284     for s in seen:
       
   285         sr = nm.get(s)
       
   286         if sr is not None:
       
   287             cs.add(sr)
       
   288     return cs
       
   289 
       
   290 def revsetallsuccessors(repo, subset, x):
       
   291     """obsolete parents"""
       
   292     s = revset.getset(repo, range(len(repo)), x)
       
   293     cs = _allsuccessors(repo, s)
       
   294     return [r for r in subset if r in cs]
       
   295 
       
   296 
       
   297 ### template keywords
       
   298 #####################
       
   299 
       
   300 def obsoletekw(repo, ctx, templ, **args):
       
   301     """:obsolete: String. The obsolescence level of the node, could be
       
   302     ``stable``, ``unstable``, ``suspended`` or ``extinct``.
       
   303     """
    42     """
   304     rev = ctx.rev()
    43     if not repo.local():
   305     if rev in repo._extinctset:
    44         return
   306         return 'extinct'
    45     for arg in sys.argv:
   307     if rev in repo._suspendedset:
    46         if 'debugc' in arg:
   308         return 'suspended'
    47             break
   309     if rev in repo._unstableset:
       
   310         return 'unstable'
       
   311     return 'stable'
       
   312 
       
   313 ### Other Extension compat
       
   314 ############################
       
   315 
       
   316 
       
   317 def buildstate(orig, repo, dest, rebaseset, *ags, **kws):
       
   318     """wrapper for rebase 's buildstate that exclude obsolete changeset"""
       
   319     rebaseset = repo.revs('%ld - extinct()', rebaseset)
       
   320     return orig(repo, dest, rebaseset, *ags, **kws)
       
   321 
       
   322 def defineparents(orig, repo, rev, target, state, *args, **kwargs):
       
   323     rebasestate = getattr(repo, '_rebasestate', None)
       
   324     if rebasestate is not None:
       
   325         repo._rebasestate = dict(state)
       
   326         repo._rebasetarget = target
       
   327     return orig(repo, rev, target, state, *args, **kwargs)
       
   328 
       
   329 def concludenode(orig, repo, rev, p1, *args, **kwargs):
       
   330     """wrapper for rebase 's concludenode that set obsolete relation"""
       
   331     newrev = orig(repo, rev, p1, *args, **kwargs)
       
   332     rebasestate = getattr(repo, '_rebasestate', None)
       
   333     if rebasestate is not None:
       
   334         if newrev is not None:
       
   335             nrev = repo[newrev].rev()
       
   336         else:
       
   337             nrev = p1
       
   338         repo._rebasestate[rev] = nrev
       
   339     return newrev
       
   340 
       
   341 def cmdrebase(orig, ui, repo, *args, **kwargs):
       
   342 
       
   343     reallykeep = kwargs.get('keep', False)
       
   344     kwargs = dict(kwargs)
       
   345     kwargs['keep'] = True
       
   346 
       
   347     # We want to mark rebased revision as obsolete and set their
       
   348     # replacements if any. Doing it in concludenode() prevents
       
   349     # aborting the rebase, and is not called with all relevant
       
   350     # revisions in --collapse case. Instead, we try to track the
       
   351     # rebase state structure by sampling/updating it in
       
   352     # defineparents() and concludenode(). The obsolete markers are
       
   353     # added from this state after a successful call.
       
   354     repo._rebasestate = {}
       
   355     repo._rebasetarget = None
       
   356     try:
       
   357         res = orig(ui, repo, *args, **kwargs)
       
   358         if not reallykeep:
       
   359             # Filter nullmerge or unrebased entries
       
   360             repo._rebasestate = dict(p for p in repo._rebasestate.iteritems()
       
   361                                      if p[1] >= 0)
       
   362             if not res and not kwargs.get('abort') and repo._rebasestate:
       
   363                 # Rebased revisions are assumed to be descendants of
       
   364                 # targetrev. If a source revision is mapped to targetrev
       
   365                 # or to another rebased revision, it must have been
       
   366                 # removed.
       
   367                 targetrev = repo[repo._rebasetarget].rev()
       
   368                 newrevs = set([targetrev])
       
   369                 replacements = {}
       
   370                 for rev, newrev in sorted(repo._rebasestate.items()):
       
   371                     oldnode = repo[rev].node()
       
   372                     if newrev not in newrevs:
       
   373                         newnode = repo[newrev].node()
       
   374                         newrevs.add(newrev)
       
   375                     else:
       
   376                         newnode = nullid
       
   377                     replacements[oldnode] = newnode
       
   378 
       
   379                 if kwargs.get('collapse'):
       
   380                     newnodes = set(n for n in replacements.values() if n != nullid)
       
   381                     if newnodes:
       
   382                         # Collapsing into more than one revision?
       
   383                         assert len(newnodes) == 1, newnodes
       
   384                         newnode = newnodes.pop()
       
   385                     else:
       
   386                         newnode = nullid
       
   387                     repo.addcollapsedobsolete(replacements, newnode)
       
   388                 else:
       
   389                     for oldnode, newnode in replacements.iteritems():
       
   390                         repo.addobsolete(newnode, oldnode)
       
   391         return res
       
   392     finally:
       
   393         delattr(repo, '_rebasestate')
       
   394         delattr(repo, '_rebasetarget')
       
   395 
       
   396 
       
   397 def extsetup(ui):
       
   398 
       
   399     revset.symbols["hidden"] = revsethidden
       
   400     revset.symbols["obsolete"] = revsetobsolete
       
   401     revset.symbols["unstable"] = revsetunstable
       
   402     revset.symbols["suspended"] = revsetsuspended
       
   403     revset.symbols["extinct"] = revsetextinct
       
   404     revset.symbols["latecomer"] = revsetlatecomer
       
   405     revset.symbols["conflicting"] = revsetconflicting
       
   406     revset.symbols["obsparents"] = revsetprecursors  # DEPR
       
   407     revset.symbols["precursors"] = revsetprecursors
       
   408     revset.symbols["obsancestors"] = revsetallprecursors  # DEPR
       
   409     revset.symbols["allprecursors"] = revsetallprecursors  # bad name
       
   410     revset.symbols["successors"] = revsetsuccessors
       
   411     revset.symbols["allsuccessors"] = revsetallsuccessors  # bad name
       
   412 
       
   413     templatekw.keywords['obsolete'] = obsoletekw
       
   414 
       
   415     # warning about more obsolete
       
   416     for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']:
       
   417         entry = extensions.wrapcommand(commands.table, cmd, warnobserrors)
       
   418     try:
       
   419         rebase = extensions.find('rebase')
       
   420         if rebase:
       
   421             entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors)
       
   422             extensions.wrapfunction(rebase, 'buildstate', buildstate)
       
   423             extensions.wrapfunction(rebase, 'defineparents', defineparents)
       
   424             extensions.wrapfunction(rebase, 'concludenode', concludenode)
       
   425             extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase)
       
   426     except KeyError:
       
   427         pass  # rebase not found
       
   428 
       
   429 # Pushkey mechanism for mutable
       
   430 #########################################
       
   431 
       
   432 def listmarkers(repo):
       
   433     """List markers over pushkey"""
       
   434     if not repo.obsstore:
       
   435         return {}
       
   436     data = repo.obsstore._writemarkers()
       
   437     encdata = base85.b85encode(data)
       
   438     return {'dump0': encdata,
       
   439             'dump': encdata} # legacy compat
       
   440 
       
   441 def pushmarker(repo, key, old, new):
       
   442     """Push markers over pushkey"""
       
   443     if not key.startswith('dump'):
       
   444         repo.ui.warn(_('unknown key: %r') % key)
       
   445         return 0
       
   446     if old:
       
   447         repo.ui.warn(_('unexpected old value') % key)
       
   448         return 0
       
   449     data = base85.b85decode(new)
       
   450     lock = repo.lock()
       
   451     try:
       
   452         try:
       
   453             repo.obsstore.mergemarkers(data)
       
   454             return 1
       
   455         except util.Abort:
       
   456             return 0
       
   457     finally:
       
   458         lock.release()
       
   459 
       
   460 pushkey.register('obsolete', pushmarker, listmarkers)
       
   461 
       
   462 ### Discovery wrapping
       
   463 #############################
       
   464 
       
   465 class blist(list, object):
       
   466     """silly class to have non False but empty list"""
       
   467 
       
   468     def __nonzero__(self):
       
   469         return bool(len(self.orig))
       
   470 
       
   471 def wrapfindcommonoutgoing(orig, repo, *args, **kwargs):
       
   472     """wrap mercurial.discovery.findcommonoutgoing to remove extinct changeset
       
   473 
       
   474     Such excluded changeset are removed from excluded  and will *not* appear
       
   475     are excluded secret changeset.
       
   476     """
       
   477     outgoing = orig(repo, *args, **kwargs)
       
   478     orig = outgoing.excluded
       
   479     outgoing.excluded = blist(n for n in orig if not repo[n].extinct())
       
   480     # when no revision is specified (push everything) a shortcut is taken when
       
   481     # nothign was exclude. taking this code path when extinct changeset have
       
   482     # been excluded leads to repository corruption.
       
   483     outgoing.excluded.orig = orig
       
   484     return outgoing
       
   485 
       
   486 def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs):
       
   487     """wrap mercurial.discovery.checkheads
       
   488 
       
   489     * prevent unstability to be pushed
       
   490     * patch remote to ignore obsolete heads on remote
       
   491     """
       
   492     # do not push instability
       
   493     for h in outgoing.missingheads:
       
   494         # checking heads only is enought because any thing base on obsolete
       
   495         # changeset is either obsolete or unstable.
       
   496         ctx =  repo[h]
       
   497         if ctx.unstable():
       
   498             raise util.Abort(_("push includes an unstable changeset: %s!")
       
   499                              % ctx)
       
   500         if ctx.obsolete():
       
   501             raise util.Abort(_("push includes an  obsolete changeset: %s!")
       
   502                              % ctx)
       
   503         if ctx.latecomer():
       
   504             raise util.Abort(_("push includes an latecomer changeset: %s!")
       
   505                              % ctx)
       
   506         if ctx.conflicting():
       
   507             raise util.Abort(_("push includes conflicting changeset: %s!")
       
   508                              % ctx)
       
   509     ### patch remote branch map
       
   510     # do not read it this burn eyes
       
   511     try:
       
   512         if 'oldbranchmap' not in vars(remote):
       
   513             remote.oldbranchmap = remote.branchmap
       
   514             def branchmap():
       
   515                 newbm = {}
       
   516                 oldbm = None
       
   517                 if (util.safehasattr(phases, 'visiblebranchmap')
       
   518                     and not util.safehasattr(remote, 'ignorevisiblebranchmap')
       
   519                    ):
       
   520                     remote.ignorevisiblebranchmap = False
       
   521                     remote.branchmap = remote.oldbranchmap
       
   522                     oldbm = phases.visiblebranchmap(remote)
       
   523                     remote.branchmap = remote.newbranchmap
       
   524                     remote.ignorevisiblebranchmap = True
       
   525                 if oldbm is None:
       
   526                     oldbm = remote.oldbranchmap()
       
   527                 for branch, nodes in oldbm.iteritems():
       
   528                     nodes = list(nodes)
       
   529                     new = set()
       
   530                     while nodes:
       
   531                         n = nodes.pop()
       
   532                         if n in repo.obsstore.precursors:
       
   533                             markers = repo.obsstore.precursors[n]
       
   534                             for mark in markers:
       
   535                                 for newernode in mark[1]:
       
   536                                     if newernode is not None:
       
   537                                         nodes.append(newernode)
       
   538                         else:
       
   539                             new.add(n)
       
   540                     if new:
       
   541                         newbm[branch] = list(new)
       
   542                 return newbm
       
   543             remote.ignorevisiblebranchmap = True
       
   544             remote.branchmap = branchmap
       
   545             remote.newbranchmap = branchmap
       
   546         return orig(repo, remote, outgoing, *args, **kwargs)
       
   547     finally:
       
   548         remote.__dict__.pop('branchmap', None) # restore class one
       
   549         remote.__dict__.pop('oldbranchmap', None)
       
   550         remote.__dict__.pop('newbranchmap', None)
       
   551         remote.__dict__.pop('ignorevisiblebranchmap', None)
       
   552 
       
   553 # eye are still burning
       
   554 def wrapvisiblebranchmap(orig, repo):
       
   555     ignore = getattr(repo, 'ignorevisiblebranchmap', None)
       
   556     if ignore is None:
       
   557         return orig(repo)
       
   558     elif ignore:
       
   559         return repo.branchmap()
       
   560     else:
    48     else:
   561         return None # break recursion
    49         data = repo.opener.tryread('obsolete-relations')
   562 
    50         if not data:
   563 def wrapclearcache(orig, repo, *args, **kwargs):
    51             data = repo.sopener.tryread('obsoletemarkers')
   564     try:
       
   565         return orig(repo, *args, **kwargs)
       
   566     finally:
       
   567         repo._clearobsoletecache()
       
   568 
       
   569 
       
   570 ### New commands
       
   571 #############################
       
   572 
       
   573 cmdtable = {}
       
   574 command = cmdutil.command(cmdtable)
       
   575 
       
   576 @command('debugobsolete', [], _('SUBJECT OBJECT'))
       
   577 def cmddebugobsolete(ui, repo, subject, object):
       
   578     """add an obsolete relation between two nodes
       
   579 
       
   580     The subject is expected to be a newer version of the object.
       
   581     """
       
   582     lock = repo.lock()
       
   583     try:
       
   584         sub = repo[subject]
       
   585         obj = repo[object]
       
   586         repo.addobsolete(sub.node(), obj.node())
       
   587     finally:
       
   588         lock.release()
       
   589     return 0
       
   590 
       
   591 @command('debugconvertobsolete', [], '')
       
   592 def cmddebugconvertobsolete(ui, repo):
       
   593     """import markers from an .hg/obsolete-relations file"""
       
   594     cnt = 0
       
   595     err = 0
       
   596     l = repo.lock()
       
   597     some = False
       
   598     try:
       
   599         repo._importoldobsolete = True
       
   600         store = repo.obsstore
       
   601         ### very first format
       
   602         try:
       
   603             f = repo.opener('obsolete-relations')
       
   604             try:
       
   605                 some = True
       
   606                 for line in f:
       
   607                     subhex, objhex = line.split()
       
   608                     suc = bin(subhex)
       
   609                     prec = bin(objhex)
       
   610                     sucs = (suc==nullid) and [] or [suc]
       
   611                     meta = {
       
   612                         'date':  '%i %i' % util.makedate(),
       
   613                         'user': ui.username(),
       
   614                         }
       
   615                     try:
       
   616                         store.create(prec, sucs, 0, meta)
       
   617                         cnt += 1
       
   618                     except ValueError:
       
   619                         repo.ui.write_err("invalid old marker line: %s"
       
   620                                           % (line))
       
   621                         err += 1
       
   622             finally:
       
   623                 f.close()
       
   624             util.unlink(repo.join('obsolete-relations'))
       
   625         except IOError:
       
   626             pass
       
   627         ### second (json) format
       
   628         data = repo.sopener.tryread('obsoletemarkers')
       
   629         if data:
    52         if data:
   630             some = True
    53             raise util.Abort('old format of obsolete marker detected!\n'
   631             for oldmark in json.loads(data):
    54                              'run `hg debugconvertobsolete` once.')
   632                 del oldmark['id']  # dropped for now
       
   633                 del oldmark['reason']  # unused until then
       
   634                 oldobject = str(oldmark.pop('object'))
       
   635                 oldsubjects = [str(s) for s in oldmark.pop('subjects', [])]
       
   636                 LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError)
       
   637                 if len(oldobject) != 40:
       
   638                     try:
       
   639                         oldobject = repo[oldobject].node()
       
   640                     except LOOKUP_ERRORS:
       
   641                         pass
       
   642                 if any(len(s) != 40 for s in oldsubjects):
       
   643                     try:
       
   644                         oldsubjects = [repo[s].node() for s in oldsubjects]
       
   645                     except LOOKUP_ERRORS:
       
   646                         pass
       
   647 
       
   648                 oldmark['date'] = '%i %i' % tuple(oldmark['date'])
       
   649                 meta = dict((k.encode('utf-8'), v.encode('utf-8'))
       
   650                              for k, v in oldmark.iteritems())
       
   651                 try:
       
   652                     succs = [bin(n) for n in oldsubjects]
       
   653                     succs = [n for n in succs if n != nullid]
       
   654                     store.create(bin(oldobject), succs,
       
   655                                  0, meta)
       
   656                     cnt += 1
       
   657                 except ValueError:
       
   658                     repo.ui.write_err("invalid marker %s -> %s\n"
       
   659                                  % (oldobject, oldsubjects))
       
   660                     err += 1
       
   661             util.unlink(repo.sjoin('obsoletemarkers'))
       
   662     finally:
       
   663         del repo._importoldobsolete
       
   664         l.release()
       
   665     if not some:
       
   666             ui.warn('nothing to do\n')
       
   667     ui.status('%i obsolete marker converted\n' % cnt)
       
   668     if err:
       
   669         ui.write_err('%i conversion failed. check you graph!\n' % err)
       
   670 
       
   671 @command('debugsuccessors', [], '')
       
   672 def cmddebugsuccessors(ui, repo):
       
   673     """dump obsolete changesets and their successors
       
   674 
       
   675     Each line matches an existing marker, the first identifier is the
       
   676     obsolete changeset identifier, followed by it successors.
       
   677     """
       
   678     lock = repo.lock()
       
   679     try:
       
   680         allsuccessors = repo.obsstore.precursors
       
   681         for old in sorted(allsuccessors):
       
   682             successors = [sorted(m[1]) for m in allsuccessors[old]]
       
   683             for i, group in enumerate(sorted(successors)):
       
   684                 ui.write('%s' % short(old))
       
   685                 for new in group:
       
   686                     ui.write(' %s' % short(new))
       
   687                 ui.write('\n')
       
   688     finally:
       
   689         lock.release()
       
   690 
       
   691 ### Altering existing command
       
   692 #############################
       
   693 
       
   694 def wrapmayobsoletewc(origfn, ui, repo, *args, **opts):
       
   695     res = origfn(ui, repo, *args, **opts)
       
   696     if repo['.'].obsolete():
       
   697         ui.warn(_('Working directory parent is obsolete\n'))
       
   698     return res
       
   699 
       
   700 def warnobserrors(orig, ui, repo, *args, **kwargs):
       
   701     """display warning is the command resulted in more instable changeset"""
       
   702     priorunstables = len(repo.revs('unstable()'))
       
   703     priorlatecomers = len(repo.revs('latecomer()'))
       
   704     priorconflictings = len(repo.revs('conflicting()'))
       
   705     #print orig, priorunstables
       
   706     #print len(repo.revs('secret() - obsolete()'))
       
   707     try:
       
   708         return orig(ui, repo, *args, **kwargs)
       
   709     finally:
       
   710         newunstables = len(repo.revs('unstable()')) - priorunstables
       
   711         newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers
       
   712         newconflictings = len(repo.revs('conflicting()')) - priorconflictings
       
   713         #print orig, newunstables
       
   714         #print len(repo.revs('secret() - obsolete()'))
       
   715         if newunstables > 0:
       
   716             ui.warn(_('%i new unstables changesets\n') % newunstables)
       
   717         if newlatecomers > 0:
       
   718             ui.warn(_('%i new latecomers changesets\n') % newlatecomers)
       
   719         if newconflictings > 0:
       
   720             ui.warn(_('%i new conflictings changesets\n') % newconflictings)
       
   721 
       
   722 def noextinctsvisibleheads(orig, repo):
       
   723     repo._turn_extinct_secret()
       
   724     return orig(repo)
       
   725 
       
   726 def wrapcmdutilamend(orig, ui, repo, commitfunc, old, *args, **kwargs):
       
   727     oldnode = old.node()
       
   728     new = orig(ui, repo, commitfunc, old, *args, **kwargs)
       
   729     if new != oldnode:
       
   730         lock = repo.lock()
       
   731         try:
       
   732             meta = {
       
   733                 'subjects':  [new],
       
   734                 'object': oldnode,
       
   735                 'date':  util.makedate(),
       
   736                 'user': ui.username(),
       
   737                 'reason': 'commit --amend',
       
   738                 }
       
   739             repo.obsstore.create(oldnode, [new], 0, meta)
       
   740             repo._clearobsoletecache()
       
   741             repo._turn_extinct_secret()
       
   742         finally:
       
   743             lock.release()
       
   744     return new
       
   745 
       
   746 def uisetup(ui):
       
   747     extensions.wrapcommand(commands.table, "update", wrapmayobsoletewc)
       
   748     extensions.wrapcommand(commands.table, "pull", wrapmayobsoletewc)
       
   749     if util.safehasattr(cmdutil, 'amend'):
       
   750         extensions.wrapfunction(cmdutil, 'amend', wrapcmdutilamend)
       
   751     extensions.wrapfunction(discovery, 'findcommonoutgoing', wrapfindcommonoutgoing)
       
   752     extensions.wrapfunction(discovery, 'checkheads', wrapcheckheads)
       
   753     extensions.wrapfunction(phases, 'visibleheads', noextinctsvisibleheads)
       
   754     extensions.wrapfunction(phases, 'advanceboundary', wrapclearcache)
       
   755     if util.safehasattr(phases, 'visiblebranchmap'):
       
   756         extensions.wrapfunction(phases, 'visiblebranchmap', wrapvisiblebranchmap)
       
   757 
       
   758 ### serialisation
       
   759 #############################
       
   760 
       
   761 def _obsserialise(obssubrels, flike):
       
   762     """serialise an obsolete relation mapping in a plain text one
       
   763 
       
   764     this is for subject -> [objects] mapping
       
   765 
       
   766     format is::
       
   767 
       
   768         <subject-full-hex> <object-full-hex>\n"""
       
   769     for sub, objs in obssubrels.iteritems():
       
   770         for obj in objs:
       
   771             if sub is None:
       
   772                 sub = nullid
       
   773             flike.write('%s %s\n' % (hex(sub), hex(obj)))
       
   774 
    55 
   775 def _obsdeserialise(flike):
    56 def _obsdeserialise(flike):
   776     """read a file like object serialised with _obsserialise
    57     """read a file like object serialised with _obsserialise
   777 
    58 
   778     this desierialize into a {subject -> objects} mapping"""
    59     this desierialize into a {subject -> objects} mapping
       
    60 
       
    61     this was the very first format ever."""
   779     rels = {}
    62     rels = {}
   780     for line in flike:
    63     for line in flike:
   781         subhex, objhex = line.split()
    64         subhex, objhex = line.split()
   782         subnode = bin(subhex)
    65         subnode = bin(subhex)
   783         if subnode == nullid:
    66         if subnode == nullid:
   784             subnode = None
    67             subnode = None
   785         rels.setdefault( subnode, set()).add(bin(objhex))
    68         rels.setdefault( subnode, set()).add(bin(objhex))
   786     return rels
    69     return rels
   787 
    70 
   788 ### diagnostique tools
    71 cmdtable = {}
   789 #############################
    72 command = cmdutil.command(cmdtable)
       
    73 @command('debugconvertobsolete', [], '')
       
    74 def cmddebugconvertobsolete(ui, repo):
       
    75     """import markers from an .hg/obsolete-relations file"""
       
    76     cnt = 0
       
    77     err = 0
       
    78     l = repo.lock()
       
    79     some = False
       
    80     try:
       
    81         unlink = []
       
    82         tr = repo.transaction('convert-obsolete')
       
    83         try:
       
    84             repo._importoldobsolete = True
       
    85             store = repo.obsstore
       
    86             ### very first format
       
    87             try:
       
    88                 f = repo.opener('obsolete-relations')
       
    89                 try:
       
    90                     some = True
       
    91                     for line in f:
       
    92                         subhex, objhex = line.split()
       
    93                         suc = bin(subhex)
       
    94                         prec = bin(objhex)
       
    95                         sucs = (suc==nullid) and [] or [suc]
       
    96                         meta = {
       
    97                             'date':  '%i %i' % util.makedate(),
       
    98                             'user': ui.username(),
       
    99                             }
       
   100                         try:
       
   101                             store.create(tr, prec, sucs, 0, meta)
       
   102                             cnt += 1
       
   103                         except ValueError:
       
   104                             repo.ui.write_err("invalid old marker line: %s"
       
   105                                               % (line))
       
   106                             err += 1
       
   107                 finally:
       
   108                     f.close()
       
   109                 unlink.append(repo.join('obsolete-relations'))
       
   110             except IOError:
       
   111                 pass
       
   112             ### second (json) format
       
   113             data = repo.sopener.tryread('obsoletemarkers')
       
   114             if data:
       
   115                 some = True
       
   116                 for oldmark in json.loads(data):
       
   117                     del oldmark['id']  # dropped for now
       
   118                     del oldmark['reason']  # unused until then
       
   119                     oldobject = str(oldmark.pop('object'))
       
   120                     oldsubjects = [str(s) for s in oldmark.pop('subjects', [])]
       
   121                     LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError)
       
   122                     if len(oldobject) != 40:
       
   123                         try:
       
   124                             oldobject = repo[oldobject].node()
       
   125                         except LOOKUP_ERRORS:
       
   126                             pass
       
   127                     if any(len(s) != 40 for s in oldsubjects):
       
   128                         try:
       
   129                             oldsubjects = [repo[s].node() for s in oldsubjects]
       
   130                         except LOOKUP_ERRORS:
       
   131                             pass
   790 
   132 
   791 def unstables(repo):
   133                     oldmark['date'] = '%i %i' % tuple(oldmark['date'])
   792     """Return all unstable changeset"""
   134                     meta = dict((k.encode('utf-8'), v.encode('utf-8'))
   793     return scmutil.revrange(repo, ['obsolete():: and (not obsolete())'])
   135                                  for k, v in oldmark.iteritems())
   794 
   136                     try:
   795 def newerversion(repo, obs):
   137                         succs = [bin(n) for n in oldsubjects]
   796     """Return the newer version of an obsolete changeset"""
   138                         succs = [n for n in succs if n != nullid]
   797     toproceed = set([(obs,)])
   139                         store.create(tr, bin(oldobject), succs,
   798     # XXX known optimization available
   140                                      0, meta)
   799     newer = set()
   141                         cnt += 1
   800     objectrels = repo.obsstore.precursors
   142                     except ValueError:
   801     while toproceed:
   143                         repo.ui.write_err("invalid marker %s -> %s\n"
   802         current = toproceed.pop()
   144                                      % (oldobject, oldsubjects))
   803         assert len(current) <= 1, 'splitting not handled yet. %r' % current
   145                         err += 1
   804         current = [n for n in current if n != nullid]
   146                 unlink.append(repo.sjoin('obsoletemarkers'))
   805         if current:
   147             tr.close()
   806             n, = current
   148             for path in unlink:
   807             if n in objectrels:
   149                 util.unlink(path)
   808                 markers = objectrels[n]
   150         finally:
   809                 for mark in markers:
   151             tr.release()
   810                     toproceed.add(tuple(mark[1]))
   152     finally:
   811             else:
   153         del repo._importoldobsolete
   812                 newer.add(tuple(current))
   154         l.release()
   813         else:
   155     if not some:
   814             newer.add(())
   156             ui.warn('nothing to do\n')
   815     return sorted(newer)
   157     ui.status('%i obsolete marker converted\n' % cnt)
   816 
   158     if err:
   817 ### obsolete relation storage
   159         ui.write_err('%i conversion failed. check you graph!\n' % err)
   818 #############################
       
   819 def add2set(d, key, mark):
       
   820     """add <mark> to a `set` in <d>[<key>]"""
       
   821     d.setdefault(key, []).append(mark)
       
   822 
       
   823 def markerid(marker):
       
   824     KEYS = ['subjects', "object", "date", "user", "reason"]
       
   825     for key in KEYS:
       
   826         assert key in marker
       
   827     keys = sorted(marker.keys())
       
   828     a = util.sha1()
       
   829     for key in keys:
       
   830         if key == 'subjects':
       
   831             for sub in sorted(marker[key]):
       
   832                 a.update(sub)
       
   833         elif key == 'id':
       
   834             pass
       
   835         else:
       
   836             a.update(str(marker[key]))
       
   837     a.update('\0')
       
   838     return a.digest()
       
   839 
       
   840 # mercurial backport
       
   841 
       
   842 def encodemeta(meta):
       
   843     """Return encoded metadata string to string mapping.
       
   844 
       
   845     Assume no ':' in key and no '\0' in both key and value."""
       
   846     for key, value in meta.iteritems():
       
   847         if ':' in key or '\0' in key:
       
   848             raise ValueError("':' and '\0' are forbidden in metadata key'")
       
   849         if '\0' in value:
       
   850             raise ValueError("':' are forbidden in metadata value'")
       
   851     return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
       
   852 
       
   853 def decodemeta(data):
       
   854     """Return string to string dictionary from encoded version."""
       
   855     d = {}
       
   856     for l in data.split('\0'):
       
   857         if l:
       
   858             key, value = l.split(':')
       
   859             d[key] = value
       
   860     return d
       
   861 
       
   862 # data used for parsing and writing
       
   863 _fmversion = 0
       
   864 _fmfixed   = '>BIB20s'
       
   865 _fmnode = '20s'
       
   866 _fmfsize = struct.calcsize(_fmfixed)
       
   867 _fnodesize = struct.calcsize(_fmnode)
       
   868 
       
   869 def _readmarkers(data):
       
   870     """Read and enumerate markers from raw data"""
       
   871     off = 0
       
   872     diskversion = _unpack('>B', data[off:off + 1])[0]
       
   873     off += 1
       
   874     if diskversion != _fmversion:
       
   875         raise util.Abort(_('parsing obsolete marker: unknown version %r')
       
   876                          % diskversion)
       
   877 
       
   878     # Loop on markers
       
   879     l = len(data)
       
   880     while off + _fmfsize <= l:
       
   881         # read fixed part
       
   882         cur = data[off:off + _fmfsize]
       
   883         off += _fmfsize
       
   884         nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur)
       
   885         # read replacement
       
   886         sucs = ()
       
   887         if nbsuc:
       
   888             s = (_fnodesize * nbsuc)
       
   889             cur = data[off:off + s]
       
   890             sucs = _unpack(_fmnode * nbsuc, cur)
       
   891             off += s
       
   892         # read metadata
       
   893         # (metadata will be decoded on demand)
       
   894         metadata = data[off:off + mdsize]
       
   895         if len(metadata) != mdsize:
       
   896             raise util.Abort(_('parsing obsolete marker: metadata is too '
       
   897                                'short, %d bytes expected, got %d')
       
   898                              % (len(metadata), mdsize))
       
   899         off += mdsize
       
   900         yield (pre, sucs, flags, metadata)
       
   901 
       
   902 class obsstore(object):
       
   903     """Store obsolete markers
       
   904 
       
   905     Markers can be accessed with two mappings:
       
   906     - precursors: old -> set(new)
       
   907     - successors: new -> set(old)
       
   908     """
       
   909 
       
   910     def __init__(self):
       
   911         self._all = []
       
   912         # new markers to serialize
       
   913         self._new = []
       
   914         self.precursors = {}
       
   915         self.successors = {}
       
   916 
       
   917     def __iter__(self):
       
   918         return iter(self._all)
       
   919 
       
   920     def __nonzero__(self):
       
   921         return bool(self._all)
       
   922 
       
   923     def create(self, prec, succs=(), flag=0, metadata=None):
       
   924         """obsolete: add a new obsolete marker
       
   925 
       
   926         * ensuring it is hashable
       
   927         * check mandatory metadata
       
   928         * encode metadata
       
   929         """
       
   930         if metadata is None:
       
   931             metadata = {}
       
   932         if len(prec) != 20:
       
   933             raise ValueError(repr(prec))
       
   934         for succ in succs:
       
   935             if len(succ) != 20:
       
   936                 raise ValueError((succs))
       
   937         marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata))
       
   938         self.add(marker)
       
   939 
       
   940     def add(self, marker):
       
   941         """Add a new marker to the store
       
   942 
       
   943         This marker still needs to be written to disk"""
       
   944         self._new.append(marker)
       
   945         self._load(marker)
       
   946 
       
   947     def loadmarkers(self, data):
       
   948         """Load all markers in data, mark them as known."""
       
   949         for marker in _readmarkers(data):
       
   950             self._load(marker)
       
   951 
       
   952     def mergemarkers(self, data):
       
   953         other = set(_readmarkers(data))
       
   954         local = set(self._all)
       
   955         new = other - local
       
   956         for marker in new:
       
   957             self.add(marker)
       
   958 
       
   959     def flushmarkers(self, stream):
       
   960         """Write all markers to a stream
       
   961 
       
   962         After this operation, "new" markers are considered "known"."""
       
   963         self._writemarkers(stream)
       
   964         self._new[:] = []
       
   965 
       
   966     def _load(self, marker):
       
   967         self._all.append(marker)
       
   968         pre, sucs = marker[:2]
       
   969         self.precursors.setdefault(pre, set()).add(marker)
       
   970         for suc in sucs:
       
   971             self.successors.setdefault(suc, set()).add(marker)
       
   972 
       
   973     def _writemarkers(self, stream=None):
       
   974         # Kept separate from flushmarkers(), it will be reused for
       
   975         # markers exchange.
       
   976         if stream is None:
       
   977             final = []
       
   978             w = final.append
       
   979         else:
       
   980             w = stream.write
       
   981         w(_pack('>B', _fmversion))
       
   982         for marker in self._all:
       
   983             pre, sucs, flags, metadata = marker
       
   984             nbsuc = len(sucs)
       
   985             format = _fmfixed + (_fmnode * nbsuc)
       
   986             data = [nbsuc, len(metadata), flags, pre]
       
   987             data.extend(sucs)
       
   988             w(_pack(format, *data))
       
   989             w(metadata)
       
   990         if stream is None:
       
   991             return ''.join(final)
       
   992 
       
   993 
       
   994 ### repo subclassing
       
   995 #############################
       
   996 
       
   997 def reposetup(ui, repo):
       
   998     if not repo.local():
       
   999         return
       
  1000 
       
  1001     if not util.safehasattr(repo.opener, 'tryread'):
       
  1002         raise util.Abort('Obsolete extension requires Mercurial 2.2 (or later)')
       
  1003     opull = repo.pull
       
  1004     opush = repo.push
       
  1005     olock = repo.lock
       
  1006     o_rollback = repo._rollback
       
  1007     o_updatebranchcache = repo.updatebranchcache
       
  1008 
       
  1009     # /!\ api change in  Hg 2.2 (97efd26eb9576f39590812ea9) /!\
       
  1010     if util.safehasattr(repo, '_journalfiles'): # Hg 2.2
       
  1011         o_journalfiles = repo._journalfiles
       
  1012     o_writejournal = repo._writejournal
       
  1013     o_hook = repo.hook
       
  1014 
       
  1015 
       
  1016     class obsoletingrepo(repo.__class__):
       
  1017 
       
  1018         # workaround
       
  1019         def hook(self, name, throw=False, **args):
       
  1020             if 'pushkey' in name:
       
  1021                 args.pop('new')
       
  1022                 args.pop('old')
       
  1023             return o_hook(name, throw=False, **args)
       
  1024 
       
  1025         ### Public method
       
  1026         def obsoletedby(self, node):
       
  1027             """return the set of node that make <node> obsolete (obj)"""
       
  1028             others = set()
       
  1029             for marker in self.obsstore.precursors.get(node, []):
       
  1030                 others.update(marker[1])
       
  1031             return others
       
  1032 
       
  1033         def obsolete(self, node):
       
  1034             """return the set of node that <node> make obsolete (sub)"""
       
  1035             return set(marker[0] for marker in self.obsstore.successors.get(node, []))
       
  1036 
       
  1037         @storecache('obsstore')
       
  1038         def obsstore(self):
       
  1039             if not getattr(self, '_importoldobsolete', False):
       
  1040                 data = repo.opener.tryread('obsolete-relations')
       
  1041                 if not data:
       
  1042                     data = repo.sopener.tryread('obsoletemarkers')
       
  1043                 if data:
       
  1044                     raise util.Abort('old format of obsolete marker detected!\n'
       
  1045                                      'run `hg debugconvertobsolete` once.')
       
  1046             store = obsstore()
       
  1047             data = self.sopener.tryread('obsstore')
       
  1048             if data:
       
  1049                 store.loadmarkers(data)
       
  1050             return store
       
  1051 
       
  1052         @util.propertycache
       
  1053         def _obsoleteset(self):
       
  1054             """the set of obsolete revision"""
       
  1055             obs = set()
       
  1056             nm = self.changelog.nodemap
       
  1057             for obj in self.obsstore.precursors:
       
  1058                 try: # /!\api change in Hg 2.2 (e8d37b78acfb22ae2c1fb126c2)/!\
       
  1059                     rev = nm.get(obj)
       
  1060                 except TypeError:  #XXX to remove while breaking Hg 2.1 support
       
  1061                     rev = nm.get(obj, None)
       
  1062                 if rev is not None:
       
  1063                     obs.add(rev)
       
  1064             return obs
       
  1065 
       
  1066         @util.propertycache
       
  1067         def _unstableset(self):
       
  1068             """the set of non obsolete revision with obsolete parent"""
       
  1069             return set(self.revs('(obsolete()::) - obsolete()'))
       
  1070 
       
  1071         @util.propertycache
       
  1072         def _suspendedset(self):
       
  1073             """the set of obsolete parent with non obsolete descendant"""
       
  1074             return set(self.revs('obsolete() and obsolete()::unstable()'))
       
  1075 
       
  1076         @util.propertycache
       
  1077         def _extinctset(self):
       
  1078             """the set of obsolete parent without non obsolete descendant"""
       
  1079             return set(self.revs('obsolete() - obsolete()::unstable()'))
       
  1080 
       
  1081         @util.propertycache
       
  1082         def _latecomerset(self):
       
  1083             """the set of rev trying to obsolete public revision"""
       
  1084             query = 'allsuccessors(public()) - obsolete() - public()'
       
  1085             return set(self.revs(query))
       
  1086 
       
  1087         @util.propertycache
       
  1088         def _conflictingset(self):
       
  1089             """the set of rev trying to obsolete public revision"""
       
  1090             conflicting = set()
       
  1091             obsstore = self.obsstore
       
  1092             newermap = {}
       
  1093             for ctx in self.set('(not public()) - obsolete()'):
       
  1094                 prec = obsstore.successors.get(ctx.node(), ())
       
  1095                 toprocess = set(prec)
       
  1096                 while toprocess:
       
  1097                     prec = toprocess.pop()[0]
       
  1098                     if prec not in newermap:
       
  1099                         newermap[prec] = newerversion(self, prec)
       
  1100                     newer = [n for n in newermap[prec] if n] # filter kill
       
  1101                     if len(newer) > 1:
       
  1102                         conflicting.add(ctx.rev())
       
  1103                         break
       
  1104                 toprocess.update(obsstore.successors.get(prec, ()))
       
  1105             return conflicting
       
  1106 
       
  1107         def _clearobsoletecache(self):
       
  1108             if '_obsoleteset' in vars(self):
       
  1109                 del self._obsoleteset
       
  1110             self._clearunstablecache()
       
  1111 
       
  1112         def updatebranchcache(self):
       
  1113             o_updatebranchcache()
       
  1114             self._clearunstablecache()
       
  1115 
       
  1116         def _clearunstablecache(self):
       
  1117             if '_unstableset' in vars(self):
       
  1118                 del self._unstableset
       
  1119             if '_suspendedset' in vars(self):
       
  1120                 del self._suspendedset
       
  1121             if '_extinctset' in vars(self):
       
  1122                 del self._extinctset
       
  1123             if '_latecomerset' in vars(self):
       
  1124                 del self._latecomerset
       
  1125             if '_conflictingset' in vars(self):
       
  1126                 del self._conflictingset
       
  1127 
       
  1128         def addobsolete(self, sub, obj):
       
  1129             """Add a relation marking that node <sub> is a new version of <obj>"""
       
  1130             assert sub != obj
       
  1131             if not repo[obj].phase():
       
  1132                 if sub is None:
       
  1133                     self.ui.warn(
       
  1134                         _("trying to kill immutable changeset %(obj)s\n")
       
  1135                         % {'obj': short(obj)})
       
  1136                 if sub is not None:
       
  1137                     self.ui.warn(
       
  1138                         _("%(sub)s try to obsolete immutable changeset %(obj)s\n")
       
  1139                         % {'sub': short(sub), 'obj': short(obj)})
       
  1140             lock = self.lock()
       
  1141             try:
       
  1142                 meta = {
       
  1143                     'date':  util.makedate(),
       
  1144                     'user': ui.username(),
       
  1145                     'reason': 'unknown',
       
  1146                     }
       
  1147                 subs = (sub == nullid) and [] or [sub]
       
  1148                 mid = self.obsstore.create(obj, subs, 0, meta)
       
  1149                 self._clearobsoletecache()
       
  1150                 self._turn_extinct_secret()
       
  1151                 return mid
       
  1152             finally:
       
  1153                 lock.release()
       
  1154 
       
  1155         def addcollapsedobsolete(self, oldnodes, newnode):
       
  1156             """Mark oldnodes as collapsed into newnode."""
       
  1157             # Assume oldnodes are all descendants of a single rev
       
  1158             rootrevs = self.revs('roots(%ln)', oldnodes)
       
  1159             assert len(rootrevs) == 1, rootrevs
       
  1160             rootnode = self[rootrevs[0]].node()
       
  1161             for n in oldnodes:
       
  1162                 self.addobsolete(newnode, n)
       
  1163 
       
  1164         def _turn_extinct_secret(self):
       
  1165             """ensure all extinct changeset are secret"""
       
  1166             self._clearobsoletecache()
       
  1167             # this is mainly for safety purpose
       
  1168             # both pull and push
       
  1169             query = '(obsolete() - obsolete()::(unstable() - secret())) - secret()'
       
  1170             expobs = [c.node() for c in repo.set(query)]
       
  1171             phases.retractboundary(repo, 2, expobs)
       
  1172 
       
  1173         ### Disk IO
       
  1174 
       
  1175         def lock(self, *args, **kwargs):
       
  1176             l = olock(*args, **kwargs)
       
  1177             if not getattr(l.releasefn, 'obspatched', False):
       
  1178                 oreleasefn = l.releasefn
       
  1179                 def releasefn(*args, **kwargs):
       
  1180                     if 'obsstore' in vars(self) and self.obsstore._new:
       
  1181                         f = self.sopener('obsstore', 'wb', atomictemp=True)
       
  1182                         try:
       
  1183                             self.obsstore.flushmarkers(f)
       
  1184                             f.close()
       
  1185                         except: # re-raises
       
  1186                             f.discard()
       
  1187                             raise
       
  1188                     oreleasefn(*args, **kwargs)
       
  1189                 releasefn.obspatched = True
       
  1190                 l.releasefn = releasefn
       
  1191             return l
       
  1192 
       
  1193 
       
  1194         ### pull // push support
       
  1195 
       
  1196         def pull(self, remote, *args, **kwargs):
       
  1197             """wrapper around push that push obsolete relation"""
       
  1198             l = repo.lock()
       
  1199             try:
       
  1200                 result = opull(remote, *args, **kwargs)
       
  1201                 remoteobs = remote.listkeys('obsolete')
       
  1202                 if 'dump' in remoteobs:
       
  1203                     remoteobs['dump0'] = remoteobs.pop('dump')
       
  1204                 if 'dump0' in remoteobs:
       
  1205                     for key, values in remoteobs.iteritems():
       
  1206                         if key.startswith('dump'):
       
  1207                             data = base85.b85decode(remoteobs['dump0'])
       
  1208                             self.obsstore.mergemarkers(data)
       
  1209                     self._clearobsoletecache()
       
  1210                     self._turn_extinct_secret()
       
  1211                     return result
       
  1212             finally:
       
  1213                 l.release()
       
  1214 
       
  1215         def push(self, remote, *args, **opts):
       
  1216             """wrapper around pull that pull obsolete relation"""
       
  1217             self._turn_extinct_secret()
       
  1218             try:
       
  1219                 result = opush(remote, *args, **opts)
       
  1220             except util.Abort, ex:
       
  1221                 hint = _("use 'hg stabilize' to get a stable history (or --force to proceed)")
       
  1222                 if (len(ex.args) >= 1
       
  1223                     and ex.args[0].startswith('push includes ')
       
  1224                     and ex.hint is None):
       
  1225                     ex.hint = hint
       
  1226                 raise
       
  1227             if 'obsolete' in remote.listkeys('namespaces') and self.obsstore:
       
  1228                 data = self.obsstore._writemarkers()
       
  1229                 r = remote.pushkey('obsolete', 'dump0', '',
       
  1230                                    base85.b85encode(data))
       
  1231                 if not r:
       
  1232                     self.ui.warn(_('failed to push obsolete markers!\n'))
       
  1233             self._turn_extinct_secret()
       
  1234 
       
  1235             return result
       
  1236 
       
  1237 
       
  1238         ### rollback support
       
  1239 
       
  1240         # /!\ api change in  Hg 2.2 (97efd26eb9576f39590812ea9) /!\
       
  1241         if util.safehasattr(repo, '_journalfiles'): # Hg 2.2
       
  1242             def _journalfiles(self):
       
  1243                 return o_journalfiles() + (self.sjoin('journal.obsstore'),) 
       
  1244 
       
  1245             def _writejournal(self, desc):
       
  1246                 """wrapped version of _writejournal that save obsolete data"""
       
  1247                 o_writejournal(desc)
       
  1248                 filename = 'obsstore'
       
  1249                 filepath = self.sjoin(filename)
       
  1250                 if os.path.exists(filepath):
       
  1251                     journalname = 'journal.' + filename
       
  1252                     journalpath = self.sjoin(journalname)
       
  1253                     util.copyfile(filepath, journalpath)
       
  1254 
       
  1255         else: # XXX removing this bloc will break Hg 2.1 support
       
  1256             def _writejournal(self, desc):
       
  1257                 """wrapped version of _writejournal that save obsolete data"""
       
  1258                 entries = list(o_writejournal(desc))
       
  1259                 filename = 'obsstore'
       
  1260                 filepath = self.sjoin(filename)
       
  1261                 if  os.path.exists(filepath):
       
  1262                     journalname = 'journal.' + filename
       
  1263                     journalpath = self.sjoin(journalname)
       
  1264                     util.copyfile(filepath, journalpath)
       
  1265                     entries.append(journalpath)
       
  1266                 return tuple(entries)
       
  1267 
       
  1268         def _rollback(self, dryrun, force):
       
  1269             """wrapped version of _rollback that restore obsolete data"""
       
  1270             ret = o_rollback(dryrun, force)
       
  1271             if not (ret or dryrun): #rollback did not failed
       
  1272                 src = self.sjoin('undo.obsstore')
       
  1273                 dst = self.sjoin('obsstore')
       
  1274                 if os.path.exists(src):
       
  1275                     util.rename(src, dst)
       
  1276                 elif os.path.exists(dst):
       
  1277                     # If no state was saved because the file did not existed before.
       
  1278                     os.unlink(dst)
       
  1279                 # invalidate cache
       
  1280                 self.__dict__.pop('obsstore', None)
       
  1281             return ret
       
  1282 
       
  1283         @storecache('00changelog.i')
       
  1284         def changelog(self):
       
  1285             # << copy pasted from mercurial source
       
  1286             c = changelog.changelog(self.sopener)
       
  1287             if 'HG_PENDING' in os.environ:
       
  1288                 p = os.environ['HG_PENDING']
       
  1289                 if p.startswith(self.root):
       
  1290                     c.readpending('00changelog.i.a')
       
  1291             # >> end of the copy paste
       
  1292             old = c.__dict__.pop('hiddenrevs', ())
       
  1293             if old:
       
  1294                 ui.warn("old wasn't empty ? %r" % old)
       
  1295             def _sethidden(c, value):
       
  1296                 assert not value
       
  1297 
       
  1298 
       
  1299             class hchangelog(c.__class__):
       
  1300                 @util.propertycache
       
  1301                 def hiddenrevs(c):
       
  1302                     shown = ['not obsolete()', '.', 'bookmark()', 'tagged()',
       
  1303                              'public()']
       
  1304                     basicquery = 'obsolete() - (::(%s))' % (' or '.join(shown))
       
  1305                     # !!! self is repo not changelog
       
  1306                     result = set(scmutil.revrange(self, [basicquery]))
       
  1307                     return result
       
  1308             c.__class__ = hchangelog
       
  1309             return c
       
  1310 
       
  1311     repo.__class__ = obsoletingrepo