--- a/hgext/obsolete.py Tue Aug 21 12:43:21 2012 +0200
+++ b/hgext/obsolete.py Fri Aug 24 11:53:55 2012 +0200
@@ -5,777 +5,60 @@
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
-"""Introduce the Obsolete concept to mercurial
-
-General concept
-===============
-
-This extension introduces the *obsolete* concept. It adds a new *obsolete*
-relation between two changesets. A relation ``<changeset B> obsolete <changeset
-A>`` is set to denote that ``<changeset B>`` is new version of ``<changeset
-A>``.
-
-The *obsolete* relation act as a **perpendicular history** to the standard
-changeset history. Standard changeset history versions files. The *obsolete*
-relation versions changesets.
-
-:obsolete: a changeset that has been replaced by another one.
-:unstable: a changeset that is not obsolete but has an obsolete ancestor.
-:suspended: an obsolete changeset with unstable descendant.
-:extinct: an obsolete changeset without unstable descendant.
- (subject to garbage collection)
-
-Another name for unstable could be out of sync.
-
-
-Usage and Feature
-=================
-
-Display and Exchange
---------------------
-
-obsolete changesets are hidden. (except if they have non obsolete changeset)
+"""Deprecated extension that formely introduces "Changeset Obsolescence".
-obsolete changesets are not exchanged. This will probably change later but it
-was the simpler solution for now.
-
-New commands
-------------
-
-Note that rebased changesets are now marked obsolete instead of being stripped.
-
-Context object
---------------
-
-Context gains a ``obsolete`` method that will return True if a changeset is
-obsolete False otherwise.
-
-revset
-------
+This concept is now partially in Mercurial core (starting with mercurial 2.3). The remaining logic have been grouped with the evolve extension.
-Add an ``obsolete()`` entry.
-
-repo extension
---------------
-
-To Do
-~~~~~
-
-- refuse to obsolete published changesets
-
-- handle split
-
-- handle conflict
-
-- handle unstable // out of sync
-
+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.
"""
-import os
-try:
- from cStringIO import StringIO
-except ImportError:
- from StringIO import StringIO
-
-from mercurial.i18n import _
-
-import base64
-import json
-
-import struct
-from mercurial import util, base85
-
-_pack = struct.pack
-_unpack = struct.unpack
-
from mercurial import util
-from mercurial import context
-from mercurial import revset
-from mercurial import scmutil
-from mercurial import extensions
-from mercurial import pushkey
-from mercurial import discovery
-from mercurial import error
-from mercurial import commands
-from mercurial import changelog
-from mercurial import phases
-from mercurial.node import hex, bin, short, nullid
-from mercurial.lock import release
-from mercurial import localrepo
-from mercurial import cmdutil
-from mercurial import templatekw
try:
- from mercurial.localrepo import storecache
- storecache('babar') # to trigger import
-except (TypeError, ImportError):
- def storecache(*args):
- return scmutil.filecache(*args, instore=True)
-
-
-### Patch changectx
-#############################
-
-def obsolete(ctx):
- """is the changeset obsolete by other"""
- if ctx.node()is None:
- return False
- return bool(ctx._repo.obsoletedby(ctx.node())) and ctx.phase()
-
-context.changectx.obsolete = obsolete
-
-def unstable(ctx):
- """is the changeset unstable (have obsolete ancestor)"""
- if ctx.node() is None:
- return False
- return ctx.rev() in ctx._repo._unstableset
-
-context.changectx.unstable = unstable
-
-def extinct(ctx):
- """is the changeset extinct by other"""
- if ctx.node() is None:
- return False
- return ctx.rev() in ctx._repo._extinctset
-
-context.changectx.extinct = extinct
-
-def latecomer(ctx):
- """is the changeset latecomer (Try to succeed to public change)"""
- if ctx.node() is None:
- return False
- return ctx.rev() in ctx._repo._latecomerset
-
-context.changectx.latecomer = latecomer
-
-def conflicting(ctx):
- """is the changeset conflicting (Try to succeed to public change)"""
- if ctx.node() is None:
- return False
- return ctx.rev() in ctx._repo._conflictingset
-
-context.changectx.conflicting = conflicting
-
-
-### revset
-#############################
-
-def revsethidden(repo, subset, x):
- """hidden changesets"""
- args = revset.getargs(x, 0, 0, 'hidden takes no argument')
- return [r for r in subset if r in repo.changelog.hiddenrevs]
-
-def revsetobsolete(repo, subset, x):
- """obsolete changesets"""
- args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
- return [r for r in subset if r in repo._obsoleteset and repo._phasecache.phase(repo, r) > 0]
-
-# XXX Backward compatibility, to be removed once stabilized
-if '_phasecache' not in vars(localrepo.localrepository): # new api
- def revsetobsolete(repo, subset, x):
- """obsolete changesets"""
- args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
- return [r for r in subset if r in repo._obsoleteset and repo._phaserev[r] > 0]
-
-def revsetunstable(repo, subset, x):
- """non obsolete changesets descendant of obsolete one"""
- args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
- return [r for r in subset if r in repo._unstableset]
-
-def revsetsuspended(repo, subset, x):
- """obsolete changesets with non obsolete descendants"""
- args = revset.getargs(x, 0, 0, 'suspended takes no arguments')
- return [r for r in subset if r in repo._suspendedset]
-
-def revsetextinct(repo, subset, x):
- """obsolete changesets without obsolete descendants"""
- args = revset.getargs(x, 0, 0, 'extinct takes no arguments')
- return [r for r in subset if r in repo._extinctset]
-
-def revsetlatecomer(repo, subset, x):
- """latecomer, Try to succeed to public change"""
- args = revset.getargs(x, 0, 0, 'latecomer takes no arguments')
- return [r for r in subset if r in repo._latecomerset]
-
-def revsetconflicting(repo, subset, x):
- """conflicting, Try to succeed to public change"""
- args = revset.getargs(x, 0, 0, 'conflicting takes no arguments')
- return [r for r in subset if r in repo._conflictingset]
+ from mercurial import obsolete
+ if not obsolete._enabled:
+ obsolete._enabled = True
+except ImportError:
+ raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)')
-def _precursors(repo, s):
- """Precursor of a changeset"""
- cs = set()
- nm = repo.changelog.nodemap
- markerbysubj = repo.obsstore.successors
- for r in s:
- for p in markerbysubj.get(repo[r].node(), ()):
- pr = nm.get(p[0])
- if pr is not None:
- cs.add(pr)
- return cs
-
-def revsetprecursors(repo, subset, x):
- """precursors of a subset"""
- s = revset.getset(repo, range(len(repo)), x)
- cs = _precursors(repo, s)
- return [r for r in subset if r in cs]
-
-def _allprecursors(repo, s): # XXX we need a better naming
- """transitive precursors of a subset"""
- toproceed = [repo[r].node() for r in s]
- seen = set()
- allsubjects = repo.obsstore.successors
- while toproceed:
- nc = toproceed.pop()
- for mark in allsubjects.get(nc, ()):
- np = mark[0]
- if np not in seen:
- seen.add(np)
- toproceed.append(np)
- nm = repo.changelog.nodemap
- cs = set()
- for p in seen:
- pr = nm.get(p)
- if pr is not None:
- cs.add(pr)
- return cs
-
-def revsetallprecursors(repo, subset, x):
- """obsolete parents"""
- s = revset.getset(repo, range(len(repo)), x)
- cs = _allprecursors(repo, s)
- return [r for r in subset if r in cs]
+import sys
+import json
-def _successors(repo, s):
- """Successors of a changeset"""
- cs = set()
- nm = repo.changelog.nodemap
- markerbyobj = repo.obsstore.precursors
- for r in s:
- for p in markerbyobj.get(repo[r].node(), ()):
- for sub in p[1]:
- sr = nm.get(sub)
- if sr is not None:
- cs.add(sr)
- return cs
-
-def revsetsuccessors(repo, subset, x):
- """successors of a subset"""
- s = revset.getset(repo, range(len(repo)), x)
- cs = _successors(repo, s)
- return [r for r in subset if r in cs]
-
-def _allsuccessors(repo, s): # XXX we need a better naming
- """transitive successors of a subset"""
- toproceed = [repo[r].node() for r in s]
- seen = set()
- allobjects = repo.obsstore.precursors
- while toproceed:
- nc = toproceed.pop()
- for mark in allobjects.get(nc, ()):
- for sub in mark[1]:
- if sub == nullid:
- continue # should not be here!
- if sub not in seen:
- seen.add(sub)
- toproceed.append(sub)
- nm = repo.changelog.nodemap
- cs = set()
- for s in seen:
- sr = nm.get(s)
- if sr is not None:
- cs.add(sr)
- return cs
-
-def revsetallsuccessors(repo, subset, x):
- """obsolete parents"""
- s = revset.getset(repo, range(len(repo)), x)
- cs = _allsuccessors(repo, s)
- return [r for r in subset if r in cs]
-
-
-### template keywords
-#####################
-
-def obsoletekw(repo, ctx, templ, **args):
- """:obsolete: String. The obsolescence level of the node, could be
- ``stable``, ``unstable``, ``suspended`` or ``extinct``.
- """
- rev = ctx.rev()
- if rev in repo._extinctset:
- return 'extinct'
- if rev in repo._suspendedset:
- return 'suspended'
- if rev in repo._unstableset:
- return 'unstable'
- return 'stable'
-
-### Other Extension compat
-############################
+from mercurial import cmdutil
+from mercurial import error
+from mercurial.node import bin, nullid
-def buildstate(orig, repo, dest, rebaseset, *ags, **kws):
- """wrapper for rebase 's buildstate that exclude obsolete changeset"""
- rebaseset = repo.revs('%ld - extinct()', rebaseset)
- return orig(repo, dest, rebaseset, *ags, **kws)
-
-def defineparents(orig, repo, rev, target, state, *args, **kwargs):
- rebasestate = getattr(repo, '_rebasestate', None)
- if rebasestate is not None:
- repo._rebasestate = dict(state)
- repo._rebasetarget = target
- return orig(repo, rev, target, state, *args, **kwargs)
-
-def concludenode(orig, repo, rev, p1, *args, **kwargs):
- """wrapper for rebase 's concludenode that set obsolete relation"""
- newrev = orig(repo, rev, p1, *args, **kwargs)
- rebasestate = getattr(repo, '_rebasestate', None)
- if rebasestate is not None:
- if newrev is not None:
- nrev = repo[newrev].rev()
- else:
- nrev = p1
- repo._rebasestate[rev] = nrev
- return newrev
-
-def cmdrebase(orig, ui, repo, *args, **kwargs):
-
- reallykeep = kwargs.get('keep', False)
- kwargs = dict(kwargs)
- kwargs['keep'] = True
+#####################################################################
+### Older format management ###
+#####################################################################
- # We want to mark rebased revision as obsolete and set their
- # replacements if any. Doing it in concludenode() prevents
- # aborting the rebase, and is not called with all relevant
- # revisions in --collapse case. Instead, we try to track the
- # rebase state structure by sampling/updating it in
- # defineparents() and concludenode(). The obsolete markers are
- # added from this state after a successful call.
- repo._rebasestate = {}
- repo._rebasetarget = None
- try:
- res = orig(ui, repo, *args, **kwargs)
- if not reallykeep:
- # Filter nullmerge or unrebased entries
- repo._rebasestate = dict(p for p in repo._rebasestate.iteritems()
- if p[1] >= 0)
- if not res and not kwargs.get('abort') and repo._rebasestate:
- # Rebased revisions are assumed to be descendants of
- # targetrev. If a source revision is mapped to targetrev
- # or to another rebased revision, it must have been
- # removed.
- targetrev = repo[repo._rebasetarget].rev()
- newrevs = set([targetrev])
- replacements = {}
- for rev, newrev in sorted(repo._rebasestate.items()):
- oldnode = repo[rev].node()
- if newrev not in newrevs:
- newnode = repo[newrev].node()
- newrevs.add(newrev)
- else:
- newnode = nullid
- replacements[oldnode] = newnode
-
- if kwargs.get('collapse'):
- newnodes = set(n for n in replacements.values() if n != nullid)
- if newnodes:
- # Collapsing into more than one revision?
- assert len(newnodes) == 1, newnodes
- newnode = newnodes.pop()
- else:
- newnode = nullid
- repo.addcollapsedobsolete(replacements, newnode)
- else:
- for oldnode, newnode in replacements.iteritems():
- repo.addobsolete(newnode, oldnode)
- return res
- finally:
- delattr(repo, '_rebasestate')
- delattr(repo, '_rebasetarget')
+# Code related to detection and management of older legacy format never
+# handled by core
-def extsetup(ui):
-
- revset.symbols["hidden"] = revsethidden
- revset.symbols["obsolete"] = revsetobsolete
- revset.symbols["unstable"] = revsetunstable
- revset.symbols["suspended"] = revsetsuspended
- revset.symbols["extinct"] = revsetextinct
- revset.symbols["latecomer"] = revsetlatecomer
- revset.symbols["conflicting"] = revsetconflicting
- revset.symbols["obsparents"] = revsetprecursors # DEPR
- revset.symbols["precursors"] = revsetprecursors
- revset.symbols["obsancestors"] = revsetallprecursors # DEPR
- revset.symbols["allprecursors"] = revsetallprecursors # bad name
- revset.symbols["successors"] = revsetsuccessors
- revset.symbols["allsuccessors"] = revsetallsuccessors # bad name
-
- templatekw.keywords['obsolete'] = obsoletekw
-
- # warning about more obsolete
- for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']:
- entry = extensions.wrapcommand(commands.table, cmd, warnobserrors)
- try:
- rebase = extensions.find('rebase')
- if rebase:
- entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors)
- extensions.wrapfunction(rebase, 'buildstate', buildstate)
- extensions.wrapfunction(rebase, 'defineparents', defineparents)
- extensions.wrapfunction(rebase, 'concludenode', concludenode)
- extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase)
- except KeyError:
- pass # rebase not found
-
-# Pushkey mechanism for mutable
-#########################################
-
-def listmarkers(repo):
- """List markers over pushkey"""
- if not repo.obsstore:
- return {}
- data = repo.obsstore._writemarkers()
- encdata = base85.b85encode(data)
- return {'dump0': encdata,
- 'dump': encdata} # legacy compat
-
-def pushmarker(repo, key, old, new):
- """Push markers over pushkey"""
- if not key.startswith('dump'):
- repo.ui.warn(_('unknown key: %r') % key)
- return 0
- if old:
- repo.ui.warn(_('unexpected old value') % key)
- return 0
- data = base85.b85decode(new)
- lock = repo.lock()
- try:
- try:
- repo.obsstore.mergemarkers(data)
- return 1
- except util.Abort:
- return 0
- finally:
- lock.release()
-
-pushkey.register('obsolete', pushmarker, listmarkers)
-
-### Discovery wrapping
-#############################
-
-class blist(list, object):
- """silly class to have non False but empty list"""
-
- def __nonzero__(self):
- return bool(len(self.orig))
-
-def wrapfindcommonoutgoing(orig, repo, *args, **kwargs):
- """wrap mercurial.discovery.findcommonoutgoing to remove extinct changeset
-
- Such excluded changeset are removed from excluded and will *not* appear
- are excluded secret changeset.
- """
- outgoing = orig(repo, *args, **kwargs)
- orig = outgoing.excluded
- outgoing.excluded = blist(n for n in orig if not repo[n].extinct())
- # when no revision is specified (push everything) a shortcut is taken when
- # nothign was exclude. taking this code path when extinct changeset have
- # been excluded leads to repository corruption.
- outgoing.excluded.orig = orig
- return outgoing
-
-def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs):
- """wrap mercurial.discovery.checkheads
-
- * prevent unstability to be pushed
- * patch remote to ignore obsolete heads on remote
- """
- # do not push instability
- for h in outgoing.missingheads:
- # checking heads only is enought because any thing base on obsolete
- # changeset is either obsolete or unstable.
- ctx = repo[h]
- if ctx.unstable():
- raise util.Abort(_("push includes an unstable changeset: %s!")
- % ctx)
- if ctx.obsolete():
- raise util.Abort(_("push includes an obsolete changeset: %s!")
- % ctx)
- if ctx.latecomer():
- raise util.Abort(_("push includes an latecomer changeset: %s!")
- % ctx)
- if ctx.conflicting():
- raise util.Abort(_("push includes conflicting changeset: %s!")
- % ctx)
- ### patch remote branch map
- # do not read it this burn eyes
- try:
- if 'oldbranchmap' not in vars(remote):
- remote.oldbranchmap = remote.branchmap
- def branchmap():
- newbm = {}
- oldbm = None
- if (util.safehasattr(phases, 'visiblebranchmap')
- and not util.safehasattr(remote, 'ignorevisiblebranchmap')
- ):
- remote.ignorevisiblebranchmap = False
- remote.branchmap = remote.oldbranchmap
- oldbm = phases.visiblebranchmap(remote)
- remote.branchmap = remote.newbranchmap
- remote.ignorevisiblebranchmap = True
- if oldbm is None:
- oldbm = remote.oldbranchmap()
- for branch, nodes in oldbm.iteritems():
- nodes = list(nodes)
- new = set()
- while nodes:
- n = nodes.pop()
- if n in repo.obsstore.precursors:
- markers = repo.obsstore.precursors[n]
- for mark in markers:
- for newernode in mark[1]:
- if newernode is not None:
- nodes.append(newernode)
- else:
- new.add(n)
- if new:
- newbm[branch] = list(new)
- return newbm
- remote.ignorevisiblebranchmap = True
- remote.branchmap = branchmap
- remote.newbranchmap = branchmap
- return orig(repo, remote, outgoing, *args, **kwargs)
- finally:
- remote.__dict__.pop('branchmap', None) # restore class one
- remote.__dict__.pop('oldbranchmap', None)
- remote.__dict__.pop('newbranchmap', None)
- remote.__dict__.pop('ignorevisiblebranchmap', None)
-
-# eye are still burning
-def wrapvisiblebranchmap(orig, repo):
- ignore = getattr(repo, 'ignorevisiblebranchmap', None)
- if ignore is None:
- return orig(repo)
- elif ignore:
- return repo.branchmap()
- else:
- return None # break recursion
-
-def wrapclearcache(orig, repo, *args, **kwargs):
- try:
- return orig(repo, *args, **kwargs)
- finally:
- repo._clearobsoletecache()
-
-
-### New commands
-#############################
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
-@command('debugobsolete', [], _('SUBJECT OBJECT'))
-def cmddebugobsolete(ui, repo, subject, object):
- """add an obsolete relation between two nodes
-
- The subject is expected to be a newer version of the object.
+def reposetup(ui, repo):
+ """Detect that a repo still contains some old obsolete format
"""
- lock = repo.lock()
- try:
- sub = repo[subject]
- obj = repo[object]
- repo.addobsolete(sub.node(), obj.node())
- finally:
- lock.release()
- return 0
-
-@command('debugconvertobsolete', [], '')
-def cmddebugconvertobsolete(ui, repo):
- """import markers from an .hg/obsolete-relations file"""
- cnt = 0
- err = 0
- l = repo.lock()
- some = False
- try:
- repo._importoldobsolete = True
- store = repo.obsstore
- ### very first format
- try:
- f = repo.opener('obsolete-relations')
- try:
- some = True
- for line in f:
- subhex, objhex = line.split()
- suc = bin(subhex)
- prec = bin(objhex)
- sucs = (suc==nullid) and [] or [suc]
- meta = {
- 'date': '%i %i' % util.makedate(),
- 'user': ui.username(),
- }
- try:
- store.create(prec, sucs, 0, meta)
- cnt += 1
- except ValueError:
- repo.ui.write_err("invalid old marker line: %s"
- % (line))
- err += 1
- finally:
- f.close()
- util.unlink(repo.join('obsolete-relations'))
- except IOError:
- pass
- ### second (json) format
- data = repo.sopener.tryread('obsoletemarkers')
+ if not repo.local():
+ return
+ for arg in sys.argv:
+ if 'debugc' in arg:
+ break
+ else:
+ data = repo.opener.tryread('obsolete-relations')
+ if not data:
+ data = repo.sopener.tryread('obsoletemarkers')
if data:
- some = True
- for oldmark in json.loads(data):
- del oldmark['id'] # dropped for now
- del oldmark['reason'] # unused until then
- oldobject = str(oldmark.pop('object'))
- oldsubjects = [str(s) for s in oldmark.pop('subjects', [])]
- LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError)
- if len(oldobject) != 40:
- try:
- oldobject = repo[oldobject].node()
- except LOOKUP_ERRORS:
- pass
- if any(len(s) != 40 for s in oldsubjects):
- try:
- oldsubjects = [repo[s].node() for s in oldsubjects]
- except LOOKUP_ERRORS:
- pass
-
- oldmark['date'] = '%i %i' % tuple(oldmark['date'])
- meta = dict((k.encode('utf-8'), v.encode('utf-8'))
- for k, v in oldmark.iteritems())
- try:
- succs = [bin(n) for n in oldsubjects]
- succs = [n for n in succs if n != nullid]
- store.create(bin(oldobject), succs,
- 0, meta)
- cnt += 1
- except ValueError:
- repo.ui.write_err("invalid marker %s -> %s\n"
- % (oldobject, oldsubjects))
- err += 1
- util.unlink(repo.sjoin('obsoletemarkers'))
- finally:
- del repo._importoldobsolete
- l.release()
- if not some:
- ui.warn('nothing to do\n')
- ui.status('%i obsolete marker converted\n' % cnt)
- if err:
- ui.write_err('%i conversion failed. check you graph!\n' % err)
-
-@command('debugsuccessors', [], '')
-def cmddebugsuccessors(ui, repo):
- """dump obsolete changesets and their successors
-
- Each line matches an existing marker, the first identifier is the
- obsolete changeset identifier, followed by it successors.
- """
- lock = repo.lock()
- try:
- allsuccessors = repo.obsstore.precursors
- for old in sorted(allsuccessors):
- successors = [sorted(m[1]) for m in allsuccessors[old]]
- for i, group in enumerate(sorted(successors)):
- ui.write('%s' % short(old))
- for new in group:
- ui.write(' %s' % short(new))
- ui.write('\n')
- finally:
- lock.release()
-
-### Altering existing command
-#############################
-
-def wrapmayobsoletewc(origfn, ui, repo, *args, **opts):
- res = origfn(ui, repo, *args, **opts)
- if repo['.'].obsolete():
- ui.warn(_('Working directory parent is obsolete\n'))
- return res
-
-def warnobserrors(orig, ui, repo, *args, **kwargs):
- """display warning is the command resulted in more instable changeset"""
- priorunstables = len(repo.revs('unstable()'))
- priorlatecomers = len(repo.revs('latecomer()'))
- priorconflictings = len(repo.revs('conflicting()'))
- #print orig, priorunstables
- #print len(repo.revs('secret() - obsolete()'))
- try:
- return orig(ui, repo, *args, **kwargs)
- finally:
- newunstables = len(repo.revs('unstable()')) - priorunstables
- newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers
- newconflictings = len(repo.revs('conflicting()')) - priorconflictings
- #print orig, newunstables
- #print len(repo.revs('secret() - obsolete()'))
- if newunstables > 0:
- ui.warn(_('%i new unstables changesets\n') % newunstables)
- if newlatecomers > 0:
- ui.warn(_('%i new latecomers changesets\n') % newlatecomers)
- if newconflictings > 0:
- ui.warn(_('%i new conflictings changesets\n') % newconflictings)
-
-def noextinctsvisibleheads(orig, repo):
- repo._turn_extinct_secret()
- return orig(repo)
-
-def wrapcmdutilamend(orig, ui, repo, commitfunc, old, *args, **kwargs):
- oldnode = old.node()
- new = orig(ui, repo, commitfunc, old, *args, **kwargs)
- if new != oldnode:
- lock = repo.lock()
- try:
- meta = {
- 'subjects': [new],
- 'object': oldnode,
- 'date': util.makedate(),
- 'user': ui.username(),
- 'reason': 'commit --amend',
- }
- repo.obsstore.create(oldnode, [new], 0, meta)
- repo._clearobsoletecache()
- repo._turn_extinct_secret()
- finally:
- lock.release()
- return new
-
-def uisetup(ui):
- extensions.wrapcommand(commands.table, "update", wrapmayobsoletewc)
- extensions.wrapcommand(commands.table, "pull", wrapmayobsoletewc)
- if util.safehasattr(cmdutil, 'amend'):
- extensions.wrapfunction(cmdutil, 'amend', wrapcmdutilamend)
- extensions.wrapfunction(discovery, 'findcommonoutgoing', wrapfindcommonoutgoing)
- extensions.wrapfunction(discovery, 'checkheads', wrapcheckheads)
- extensions.wrapfunction(phases, 'visibleheads', noextinctsvisibleheads)
- extensions.wrapfunction(phases, 'advanceboundary', wrapclearcache)
- if util.safehasattr(phases, 'visiblebranchmap'):
- extensions.wrapfunction(phases, 'visiblebranchmap', wrapvisiblebranchmap)
-
-### serialisation
-#############################
-
-def _obsserialise(obssubrels, flike):
- """serialise an obsolete relation mapping in a plain text one
-
- this is for subject -> [objects] mapping
-
- format is::
-
- <subject-full-hex> <object-full-hex>\n"""
- for sub, objs in obssubrels.iteritems():
- for obj in objs:
- if sub is None:
- sub = nullid
- flike.write('%s %s\n' % (hex(sub), hex(obj)))
+ raise util.Abort('old format of obsolete marker detected!\n'
+ 'run `hg debugconvertobsolete` once.')
def _obsdeserialise(flike):
"""read a file like object serialised with _obsserialise
- this desierialize into a {subject -> objects} mapping"""
+ this desierialize into a {subject -> objects} mapping
+
+ this was the very first format ever."""
rels = {}
for line in flike:
subhex, objhex = line.split()
@@ -785,527 +68,92 @@
rels.setdefault( subnode, set()).add(bin(objhex))
return rels
-### diagnostique tools
-#############################
-
-def unstables(repo):
- """Return all unstable changeset"""
- return scmutil.revrange(repo, ['obsolete():: and (not obsolete())'])
-
-def newerversion(repo, obs):
- """Return the newer version of an obsolete changeset"""
- toproceed = set([(obs,)])
- # XXX known optimization available
- newer = set()
- objectrels = repo.obsstore.precursors
- while toproceed:
- current = toproceed.pop()
- assert len(current) <= 1, 'splitting not handled yet. %r' % current
- current = [n for n in current if n != nullid]
- if current:
- n, = current
- if n in objectrels:
- markers = objectrels[n]
- for mark in markers:
- toproceed.add(tuple(mark[1]))
- else:
- newer.add(tuple(current))
- else:
- newer.add(())
- return sorted(newer)
-
-### obsolete relation storage
-#############################
-def add2set(d, key, mark):
- """add <mark> to a `set` in <d>[<key>]"""
- d.setdefault(key, []).append(mark)
-
-def markerid(marker):
- KEYS = ['subjects', "object", "date", "user", "reason"]
- for key in KEYS:
- assert key in marker
- keys = sorted(marker.keys())
- a = util.sha1()
- for key in keys:
- if key == 'subjects':
- for sub in sorted(marker[key]):
- a.update(sub)
- elif key == 'id':
- pass
- else:
- a.update(str(marker[key]))
- a.update('\0')
- return a.digest()
-
-# mercurial backport
-
-def encodemeta(meta):
- """Return encoded metadata string to string mapping.
-
- Assume no ':' in key and no '\0' in both key and value."""
- for key, value in meta.iteritems():
- if ':' in key or '\0' in key:
- raise ValueError("':' and '\0' are forbidden in metadata key'")
- if '\0' in value:
- raise ValueError("':' are forbidden in metadata value'")
- return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
-
-def decodemeta(data):
- """Return string to string dictionary from encoded version."""
- d = {}
- for l in data.split('\0'):
- if l:
- key, value = l.split(':')
- d[key] = value
- return d
-
-# data used for parsing and writing
-_fmversion = 0
-_fmfixed = '>BIB20s'
-_fmnode = '20s'
-_fmfsize = struct.calcsize(_fmfixed)
-_fnodesize = struct.calcsize(_fmnode)
-
-def _readmarkers(data):
- """Read and enumerate markers from raw data"""
- off = 0
- diskversion = _unpack('>B', data[off:off + 1])[0]
- off += 1
- if diskversion != _fmversion:
- raise util.Abort(_('parsing obsolete marker: unknown version %r')
- % diskversion)
-
- # Loop on markers
- l = len(data)
- while off + _fmfsize <= l:
- # read fixed part
- cur = data[off:off + _fmfsize]
- off += _fmfsize
- nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur)
- # read replacement
- sucs = ()
- if nbsuc:
- s = (_fnodesize * nbsuc)
- cur = data[off:off + s]
- sucs = _unpack(_fmnode * nbsuc, cur)
- off += s
- # read metadata
- # (metadata will be decoded on demand)
- metadata = data[off:off + mdsize]
- if len(metadata) != mdsize:
- raise util.Abort(_('parsing obsolete marker: metadata is too '
- 'short, %d bytes expected, got %d')
- % (len(metadata), mdsize))
- off += mdsize
- yield (pre, sucs, flags, metadata)
-
-class obsstore(object):
- """Store obsolete markers
-
- Markers can be accessed with two mappings:
- - precursors: old -> set(new)
- - successors: new -> set(old)
- """
-
- def __init__(self):
- self._all = []
- # new markers to serialize
- self._new = []
- self.precursors = {}
- self.successors = {}
-
- def __iter__(self):
- return iter(self._all)
-
- def __nonzero__(self):
- return bool(self._all)
-
- def create(self, prec, succs=(), flag=0, metadata=None):
- """obsolete: add a new obsolete marker
-
- * ensuring it is hashable
- * check mandatory metadata
- * encode metadata
- """
- if metadata is None:
- metadata = {}
- if len(prec) != 20:
- raise ValueError(repr(prec))
- for succ in succs:
- if len(succ) != 20:
- raise ValueError((succs))
- marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata))
- self.add(marker)
-
- def add(self, marker):
- """Add a new marker to the store
-
- This marker still needs to be written to disk"""
- self._new.append(marker)
- self._load(marker)
-
- def loadmarkers(self, data):
- """Load all markers in data, mark them as known."""
- for marker in _readmarkers(data):
- self._load(marker)
-
- def mergemarkers(self, data):
- other = set(_readmarkers(data))
- local = set(self._all)
- new = other - local
- for marker in new:
- self.add(marker)
-
- def flushmarkers(self, stream):
- """Write all markers to a stream
-
- After this operation, "new" markers are considered "known"."""
- self._writemarkers(stream)
- self._new[:] = []
-
- def _load(self, marker):
- self._all.append(marker)
- pre, sucs = marker[:2]
- self.precursors.setdefault(pre, set()).add(marker)
- for suc in sucs:
- self.successors.setdefault(suc, set()).add(marker)
-
- def _writemarkers(self, stream=None):
- # Kept separate from flushmarkers(), it will be reused for
- # markers exchange.
- if stream is None:
- final = []
- w = final.append
- else:
- w = stream.write
- w(_pack('>B', _fmversion))
- for marker in self._all:
- pre, sucs, flags, metadata = marker
- nbsuc = len(sucs)
- format = _fmfixed + (_fmnode * nbsuc)
- data = [nbsuc, len(metadata), flags, pre]
- data.extend(sucs)
- w(_pack(format, *data))
- w(metadata)
- if stream is None:
- return ''.join(final)
-
-
-### repo subclassing
-#############################
-
-def reposetup(ui, repo):
- if not repo.local():
- return
-
- if not util.safehasattr(repo.opener, 'tryread'):
- raise util.Abort('Obsolete extension requires Mercurial 2.2 (or later)')
- opull = repo.pull
- opush = repo.push
- olock = repo.lock
- o_rollback = repo._rollback
- o_updatebranchcache = repo.updatebranchcache
-
- # /!\ api change in Hg 2.2 (97efd26eb9576f39590812ea9) /!\
- if util.safehasattr(repo, '_journalfiles'): # Hg 2.2
- o_journalfiles = repo._journalfiles
- o_writejournal = repo._writejournal
- o_hook = repo.hook
-
-
- class obsoletingrepo(repo.__class__):
-
- # workaround
- def hook(self, name, throw=False, **args):
- if 'pushkey' in name:
- args.pop('new')
- args.pop('old')
- return o_hook(name, throw=False, **args)
-
- ### Public method
- def obsoletedby(self, node):
- """return the set of node that make <node> obsolete (obj)"""
- others = set()
- for marker in self.obsstore.precursors.get(node, []):
- others.update(marker[1])
- return others
-
- def obsolete(self, node):
- """return the set of node that <node> make obsolete (sub)"""
- return set(marker[0] for marker in self.obsstore.successors.get(node, []))
-
- @storecache('obsstore')
- def obsstore(self):
- if not getattr(self, '_importoldobsolete', False):
- data = repo.opener.tryread('obsolete-relations')
- if not data:
- data = repo.sopener.tryread('obsoletemarkers')
- if data:
- raise util.Abort('old format of obsolete marker detected!\n'
- 'run `hg debugconvertobsolete` once.')
- store = obsstore()
- data = self.sopener.tryread('obsstore')
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+@command('debugconvertobsolete', [], '')
+def cmddebugconvertobsolete(ui, repo):
+ """import markers from an .hg/obsolete-relations file"""
+ cnt = 0
+ err = 0
+ l = repo.lock()
+ some = False
+ try:
+ unlink = []
+ tr = repo.transaction('convert-obsolete')
+ try:
+ repo._importoldobsolete = True
+ store = repo.obsstore
+ ### very first format
+ try:
+ f = repo.opener('obsolete-relations')
+ try:
+ some = True
+ for line in f:
+ subhex, objhex = line.split()
+ suc = bin(subhex)
+ prec = bin(objhex)
+ sucs = (suc==nullid) and [] or [suc]
+ meta = {
+ 'date': '%i %i' % util.makedate(),
+ 'user': ui.username(),
+ }
+ try:
+ store.create(tr, prec, sucs, 0, meta)
+ cnt += 1
+ except ValueError:
+ repo.ui.write_err("invalid old marker line: %s"
+ % (line))
+ err += 1
+ finally:
+ f.close()
+ unlink.append(repo.join('obsolete-relations'))
+ except IOError:
+ pass
+ ### second (json) format
+ data = repo.sopener.tryread('obsoletemarkers')
if data:
- store.loadmarkers(data)
- return store
-
- @util.propertycache
- def _obsoleteset(self):
- """the set of obsolete revision"""
- obs = set()
- nm = self.changelog.nodemap
- for obj in self.obsstore.precursors:
- try: # /!\api change in Hg 2.2 (e8d37b78acfb22ae2c1fb126c2)/!\
- rev = nm.get(obj)
- except TypeError: #XXX to remove while breaking Hg 2.1 support
- rev = nm.get(obj, None)
- if rev is not None:
- obs.add(rev)
- return obs
-
- @util.propertycache
- def _unstableset(self):
- """the set of non obsolete revision with obsolete parent"""
- return set(self.revs('(obsolete()::) - obsolete()'))
-
- @util.propertycache
- def _suspendedset(self):
- """the set of obsolete parent with non obsolete descendant"""
- return set(self.revs('obsolete() and obsolete()::unstable()'))
-
- @util.propertycache
- def _extinctset(self):
- """the set of obsolete parent without non obsolete descendant"""
- return set(self.revs('obsolete() - obsolete()::unstable()'))
-
- @util.propertycache
- def _latecomerset(self):
- """the set of rev trying to obsolete public revision"""
- query = 'allsuccessors(public()) - obsolete() - public()'
- return set(self.revs(query))
-
- @util.propertycache
- def _conflictingset(self):
- """the set of rev trying to obsolete public revision"""
- conflicting = set()
- obsstore = self.obsstore
- newermap = {}
- for ctx in self.set('(not public()) - obsolete()'):
- prec = obsstore.successors.get(ctx.node(), ())
- toprocess = set(prec)
- while toprocess:
- prec = toprocess.pop()[0]
- if prec not in newermap:
- newermap[prec] = newerversion(self, prec)
- newer = [n for n in newermap[prec] if n] # filter kill
- if len(newer) > 1:
- conflicting.add(ctx.rev())
- break
- toprocess.update(obsstore.successors.get(prec, ()))
- return conflicting
-
- def _clearobsoletecache(self):
- if '_obsoleteset' in vars(self):
- del self._obsoleteset
- self._clearunstablecache()
-
- def updatebranchcache(self):
- o_updatebranchcache()
- self._clearunstablecache()
-
- def _clearunstablecache(self):
- if '_unstableset' in vars(self):
- del self._unstableset
- if '_suspendedset' in vars(self):
- del self._suspendedset
- if '_extinctset' in vars(self):
- del self._extinctset
- if '_latecomerset' in vars(self):
- del self._latecomerset
- if '_conflictingset' in vars(self):
- del self._conflictingset
-
- def addobsolete(self, sub, obj):
- """Add a relation marking that node <sub> is a new version of <obj>"""
- assert sub != obj
- if not repo[obj].phase():
- if sub is None:
- self.ui.warn(
- _("trying to kill immutable changeset %(obj)s\n")
- % {'obj': short(obj)})
- if sub is not None:
- self.ui.warn(
- _("%(sub)s try to obsolete immutable changeset %(obj)s\n")
- % {'sub': short(sub), 'obj': short(obj)})
- lock = self.lock()
- try:
- meta = {
- 'date': util.makedate(),
- 'user': ui.username(),
- 'reason': 'unknown',
- }
- subs = (sub == nullid) and [] or [sub]
- mid = self.obsstore.create(obj, subs, 0, meta)
- self._clearobsoletecache()
- self._turn_extinct_secret()
- return mid
- finally:
- lock.release()
-
- def addcollapsedobsolete(self, oldnodes, newnode):
- """Mark oldnodes as collapsed into newnode."""
- # Assume oldnodes are all descendants of a single rev
- rootrevs = self.revs('roots(%ln)', oldnodes)
- assert len(rootrevs) == 1, rootrevs
- rootnode = self[rootrevs[0]].node()
- for n in oldnodes:
- self.addobsolete(newnode, n)
-
- def _turn_extinct_secret(self):
- """ensure all extinct changeset are secret"""
- self._clearobsoletecache()
- # this is mainly for safety purpose
- # both pull and push
- query = '(obsolete() - obsolete()::(unstable() - secret())) - secret()'
- expobs = [c.node() for c in repo.set(query)]
- phases.retractboundary(repo, 2, expobs)
-
- ### Disk IO
+ some = True
+ for oldmark in json.loads(data):
+ del oldmark['id'] # dropped for now
+ del oldmark['reason'] # unused until then
+ oldobject = str(oldmark.pop('object'))
+ oldsubjects = [str(s) for s in oldmark.pop('subjects', [])]
+ LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError)
+ if len(oldobject) != 40:
+ try:
+ oldobject = repo[oldobject].node()
+ except LOOKUP_ERRORS:
+ pass
+ if any(len(s) != 40 for s in oldsubjects):
+ try:
+ oldsubjects = [repo[s].node() for s in oldsubjects]
+ except LOOKUP_ERRORS:
+ pass
- def lock(self, *args, **kwargs):
- l = olock(*args, **kwargs)
- if not getattr(l.releasefn, 'obspatched', False):
- oreleasefn = l.releasefn
- def releasefn(*args, **kwargs):
- if 'obsstore' in vars(self) and self.obsstore._new:
- f = self.sopener('obsstore', 'wb', atomictemp=True)
- try:
- self.obsstore.flushmarkers(f)
- f.close()
- except: # re-raises
- f.discard()
- raise
- oreleasefn(*args, **kwargs)
- releasefn.obspatched = True
- l.releasefn = releasefn
- return l
-
-
- ### pull // push support
-
- def pull(self, remote, *args, **kwargs):
- """wrapper around push that push obsolete relation"""
- l = repo.lock()
- try:
- result = opull(remote, *args, **kwargs)
- remoteobs = remote.listkeys('obsolete')
- if 'dump' in remoteobs:
- remoteobs['dump0'] = remoteobs.pop('dump')
- if 'dump0' in remoteobs:
- for key, values in remoteobs.iteritems():
- if key.startswith('dump'):
- data = base85.b85decode(remoteobs['dump0'])
- self.obsstore.mergemarkers(data)
- self._clearobsoletecache()
- self._turn_extinct_secret()
- return result
- finally:
- l.release()
-
- def push(self, remote, *args, **opts):
- """wrapper around pull that pull obsolete relation"""
- self._turn_extinct_secret()
- try:
- result = opush(remote, *args, **opts)
- except util.Abort, ex:
- hint = _("use 'hg stabilize' to get a stable history (or --force to proceed)")
- if (len(ex.args) >= 1
- and ex.args[0].startswith('push includes ')
- and ex.hint is None):
- ex.hint = hint
- raise
- if 'obsolete' in remote.listkeys('namespaces') and self.obsstore:
- data = self.obsstore._writemarkers()
- r = remote.pushkey('obsolete', 'dump0', '',
- base85.b85encode(data))
- if not r:
- self.ui.warn(_('failed to push obsolete markers!\n'))
- self._turn_extinct_secret()
-
- return result
-
-
- ### rollback support
-
- # /!\ api change in Hg 2.2 (97efd26eb9576f39590812ea9) /!\
- if util.safehasattr(repo, '_journalfiles'): # Hg 2.2
- def _journalfiles(self):
- return o_journalfiles() + (self.sjoin('journal.obsstore'),)
-
- def _writejournal(self, desc):
- """wrapped version of _writejournal that save obsolete data"""
- o_writejournal(desc)
- filename = 'obsstore'
- filepath = self.sjoin(filename)
- if os.path.exists(filepath):
- journalname = 'journal.' + filename
- journalpath = self.sjoin(journalname)
- util.copyfile(filepath, journalpath)
-
- else: # XXX removing this bloc will break Hg 2.1 support
- def _writejournal(self, desc):
- """wrapped version of _writejournal that save obsolete data"""
- entries = list(o_writejournal(desc))
- filename = 'obsstore'
- filepath = self.sjoin(filename)
- if os.path.exists(filepath):
- journalname = 'journal.' + filename
- journalpath = self.sjoin(journalname)
- util.copyfile(filepath, journalpath)
- entries.append(journalpath)
- return tuple(entries)
-
- def _rollback(self, dryrun, force):
- """wrapped version of _rollback that restore obsolete data"""
- ret = o_rollback(dryrun, force)
- if not (ret or dryrun): #rollback did not failed
- src = self.sjoin('undo.obsstore')
- dst = self.sjoin('obsstore')
- if os.path.exists(src):
- util.rename(src, dst)
- elif os.path.exists(dst):
- # If no state was saved because the file did not existed before.
- os.unlink(dst)
- # invalidate cache
- self.__dict__.pop('obsstore', None)
- return ret
-
- @storecache('00changelog.i')
- def changelog(self):
- # << copy pasted from mercurial source
- c = changelog.changelog(self.sopener)
- if 'HG_PENDING' in os.environ:
- p = os.environ['HG_PENDING']
- if p.startswith(self.root):
- c.readpending('00changelog.i.a')
- # >> end of the copy paste
- old = c.__dict__.pop('hiddenrevs', ())
- if old:
- ui.warn("old wasn't empty ? %r" % old)
- def _sethidden(c, value):
- assert not value
-
-
- class hchangelog(c.__class__):
- @util.propertycache
- def hiddenrevs(c):
- shown = ['not obsolete()', '.', 'bookmark()', 'tagged()',
- 'public()']
- basicquery = 'obsolete() - (::(%s))' % (' or '.join(shown))
- # !!! self is repo not changelog
- result = set(scmutil.revrange(self, [basicquery]))
- return result
- c.__class__ = hchangelog
- return c
-
- repo.__class__ = obsoletingrepo
+ oldmark['date'] = '%i %i' % tuple(oldmark['date'])
+ meta = dict((k.encode('utf-8'), v.encode('utf-8'))
+ for k, v in oldmark.iteritems())
+ try:
+ succs = [bin(n) for n in oldsubjects]
+ succs = [n for n in succs if n != nullid]
+ store.create(tr, bin(oldobject), succs,
+ 0, meta)
+ cnt += 1
+ except ValueError:
+ repo.ui.write_err("invalid marker %s -> %s\n"
+ % (oldobject, oldsubjects))
+ err += 1
+ unlink.append(repo.sjoin('obsoletemarkers'))
+ tr.close()
+ for path in unlink:
+ util.unlink(path)
+ finally:
+ tr.release()
+ finally:
+ del repo._importoldobsolete
+ l.release()
+ if not some:
+ ui.warn('nothing to do\n')
+ ui.status('%i obsolete marker converted\n' % cnt)
+ if err:
+ ui.write_err('%i conversion failed. check you graph!\n' % err)