diff -r f17a0f801e0b -r a9c27df23129 hgext/obsolete.py --- 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 `` obsolete `` is set to denote that ```` is new version of ````. - -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:: - - \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 to a `set` in []""" - 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 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 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 is a new version of """ - 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)