diff -r 1c82147e9395 -r 079b231b8ea4 hgext/obsolete.py --- a/hgext/obsolete.py Tue Aug 07 11:35:45 2012 +0200 +++ b/hgext/obsolete.py Tue Aug 07 14:08:31 2012 +0200 @@ -76,46 +76,253 @@ obsolete._enabled = True +### Extension helper +############################# + +### setup code + +class exthelper(object): + """Helper for modular extension setup + + A single helper should be intanciated for each extension. Helper method are + then used as decorator for various purpose. + + All decorator returns the original function and may be chained. + """ + + def __init__(self): + self._uicallables = [] + self._extcallables = [] + self._repocallables = [] + self._revsetsymbols = [] + self._commandwrappers = [] + self._extcommandwrappers = [] + self._functionwrappers = [] + self._duckpunchers = [] + + def final_uisetup(self, ui): + """Method to be used as a the extension uisetup + + The following operations belong here: + + - Changes to ui.__class__ . The ui object that will be used to run the + command has not yet been created. Changes made here will affect ui + objects created after this, and in particular the ui that will be + passed to runcommand + - Command wraps (extensions.wrapcommand) + - Changes that need to be visible by other extensions: because + initialization occurs in phases (all extensions run uisetup, then all + run extsetup), a change made here will be visible by other extensions + during extsetup + - Monkeypatches or function wraps (extensions.wrapfunction) of dispatch + module members + - Setup of pre-* and post-* hooks + - pushkey setup + """ + for cont, funcname, func in self._duckpunchers: + setattr(cont, funcname, func) + for command, wrapper in self._commandwrappers: + extensions.wrapcommand(commands.table, command, wrapper) + for cont, funcname, wrapper in self._functionwrappers: + extensions.wrapfunction(cont, funcname, wrapper) + for c in self._uicallables: + c(ui) + + def final_extsetup(self, ui): + """Method to be used as a the extension extsetup + + The following operations belong here: + + - Changes depending on the status of other extensions. (if extensions.find('mq')) + - Add a global option to all commands + - Extend revsets + """ + knownexts = {} + for name, symbol in self._revsetsymbols: + revset.symbols[name] = symbol + for ext, command, wrapper in self._extcommandwrappers: + if ext not in knownexts: + e = extensions.find('rebase') + if e is None: + raise util.Abort('extension %s not found' %e) + knownexts[ext] = e.cmdtable + extensions.wrapcommand(knownexts[ext], commands, wrapper) + for c in self._extcallables: + c(ui) + + def final_reposetup(self, ui, repo): + """Method to be used as a the extension reposetup + + The following operations belong here: + + - All hooks but pre-* and post-* + - Modify configuration variables + - Changes to repo.__class__, repo.dirstate.__class__ + """ + for c in self._repocallables: + c(ui, repo) + + def uisetup(self, call): + """Decorated function will be executed during uisetup + + example:: + + @eh.uisetup + def setupbabar(ui): + print 'this is uisetup!' + """ + self._uicallables.append(call) + return call + + def extsetup(self, call): + """Decorated function will be executed during extsetup + + example:: + + @eh.extsetup + def setupcelestine(ui): + print 'this is extsetup!' + """ + self._uicallables.append(call) + return call + + def reposetup(self, call): + """Decorated function will be executed during reposetup + + example:: + + @eh.reposetup + def setupzephir(ui, repo): + print 'this is reposetup!' + """ + self._repocallables.append(call) + return call + + def revset(self, symbolname): + """Decorated function is a revset symbol + + The name of the symbol must be given as the decorator argument. + The symbol is added during `extsetup`. + + example:: + + @eh.revset('hidden') + def revsetbabar(repo, subset, x): + args = revset.getargs(x, 0, 0, 'babar accept no argument') + return [r for r in subset if 'babar' in repo[r].description()] + """ + def dec(symbol): + self._revsetsymbols.append((symbolname, symbol)) + return symbol + return dec + + def wrapcommand(self, command, extension=None): + """Decorated function is a command wrapper + + The name of the command must be given as the decorator argument. + The wrapping is installed during `uisetup`. + + If the second option `extension` argument is provided, the wrapping + will be applied in the extension commandtable. This argument must be a + string that will be searched using `extension.find` if not found and + Abort error is raised. If the wrapping apply to an extension, it is + installed during `extsetup` + + example:: + + @eh.wrapcommand('summary') + def wrapsummary(orig, ui, repo, *args, **kwargs): + ui.note('Barry!') + return orig(ui, repo, *args, **kwargs) + + """ + def dec(wrapper): + if extension is None: + self._commandwrappers.append((command, wrapper)) + else: + self._extcommandwrappers.append((extension, command, wrapper)) + return wrapper + return dec + + def wrapfunction(self, container, funcname): + """Decorated function is a function wrapper + + This function take two argument, the container and the name of the + function to wrap. The wrapping is performed during `uisetup`. + (there is don't support extension) + + example:: + + @eh.function(discovery, 'checkheads') + def wrapfunction(orig, *args, **kwargs): + ui.note('His head smashed in and his heart cut out') + return orig(*args, **kwargs) + """ + def dec(wrapper): + self._functionwrappers.append((container, funcname, wrapper)) + return wrapper + return dec + + def addattr(self, container, funcname): + """Decorated function is to be added to the container + + This function take two argument, the container and the name of the + function to wrap. The wrapping is performed during `uisetup`. + + example:: + + @eh.function(context.changectx, 'babar') + def babar(ctx): + return 'babar' in ctx.description + """ + def dec(func): + self._duckpunchers.append((container, funcname, func)) + return func + return dec + +eh = exthelper() +uisetup = eh.final_uisetup +extsetup = eh.final_extsetup +reposetup = eh.final_reposetup + ### Patch changectx ############################# +@eh.addattr(context.changectx, 'unstable') 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 +@eh.addattr(context.changectx, 'extinct') 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 - +@eh.addattr(context.changectx, 'latecomer') 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 - +@eh.addattr(context.changectx, 'conflicting') 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 ############################# +@eh.revset('hidden') def revsethidden(repo, subset, x): """``hidden()`` Changeset is hidden. @@ -123,6 +330,7 @@ args = revset.getargs(x, 0, 0, 'hidden takes no argument') return [r for r in subset if r in repo.hiddenrevs] +@eh.revset('obsolete') def revsetobsolete(repo, subset, x): """``obsolete()`` Changeset is obsolete. @@ -130,6 +338,7 @@ 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] +@eh.revset('unstable') def revsetunstable(repo, subset, x): """``unstable()`` Unstable changesets are non-obsolete with obsolete ancestors. @@ -137,6 +346,7 @@ args = revset.getargs(x, 0, 0, 'unstable takes no arguments') return [r for r in subset if r in repo._unstableset] +@eh.revset('suspended') def revsetsuspended(repo, subset, x): """``suspended()`` Obsolete changesets with non-obsolete descendants. @@ -144,6 +354,7 @@ args = revset.getargs(x, 0, 0, 'suspended takes no arguments') return [r for r in subset if r in repo._suspendedset] +@eh.revset('extinct') def revsetextinct(repo, subset, x): """``extinct()`` Obsolete changesets with obsolete descendants only. @@ -151,6 +362,7 @@ args = revset.getargs(x, 0, 0, 'extinct takes no arguments') return [r for r in subset if r in repo._extinctset] +@eh.revset('latecomer') def revsetlatecomer(repo, subset, x): """``latecomer()`` Changesets marked as successors of public changesets. @@ -158,6 +370,7 @@ args = revset.getargs(x, 0, 0, 'latecomer takes no arguments') return [r for r in subset if r in repo._latecomerset] +@eh.revset('conflicting') def revsetconflicting(repo, subset, x): """``conflicting()`` Changesets marked as successors of a same changeset. @@ -177,6 +390,8 @@ cs.add(pr) return cs +@eh.revset('obsparents') +@eh.revset('precursors') def revsetprecursors(repo, subset, x): """``precursors(set)`` Immediate precursors of changesets in set. @@ -205,6 +420,8 @@ cs.add(pr) return cs +@eh.revset('obsancestors') +@eh.revset('allprecursors') def revsetallprecursors(repo, subset, x): """``allprecursors(set)`` Transitive precursors of changesets in set. @@ -226,6 +443,8 @@ cs.add(sr) return cs +@eh.revset('obschildrend') +@eh.revset('successors') def revsetsuccessors(repo, subset, x): """``successors(set)`` Immediate successors of changesets in set. @@ -254,6 +473,8 @@ cs.add(sr) return cs +@eh.revset('obsdescendants') +@eh.revset('allsuccessors') def revsetallsuccessors(repo, subset, x): """``allsuccessors(set)`` Transitive successors of changesets in set. @@ -279,6 +500,10 @@ return 'unstable' return 'stable' +@eh.extsetup +def addkeyword(ui): + templatekw.keywords['obsolete'] = obsoletekw + ### Other Extension compat ############################ @@ -362,28 +587,9 @@ delattr(repo, '_rebasestate') delattr(repo, '_rebasetarget') - -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 - +@eh.extsetup +def _rebasewrapping(ui): # 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: @@ -398,7 +604,7 @@ ### Discovery wrapping ############################# - +@eh.wrapfunction(discovery, 'checkheads') def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs): """wrap mercurial.discovery.checkheads @@ -418,6 +624,7 @@ % ctx) return orig(repo, remote, outgoing, *args, **kwargs) +@eh.wrapfunction(phases, 'advanceboundary') def wrapclearcache(orig, repo, *args, **kwargs): try: return orig(repo, *args, **kwargs) @@ -432,6 +639,331 @@ command = cmdutil.command(cmdtable) + +@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 +############################# + +@eh.wrapcommand("update") +@eh.wrapcommand("pull") +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) + +@eh.extsetup +def _coreobserrorwrapping(ui): + # warning about more obsolete + for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']: + entry = extensions.wrapcommand(commands.table, cmd, warnobserrors) + +@eh.wrapfunction(cmdutil, 'amend') +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: + tr = repo.transaction('post-amend-obst') + try: + meta = { + 'date': '%i %i' % util.makedate(), + 'user': ui.username(), + } + repo.obsstore.create(tr, oldnode, [new], 0, meta) + tr.close() + repo._clearobsoletecache() + finally: + tr.release() + finally: + lock.release() + return new + + +### 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) + +### repo subclassing +############################# + +@eh.reposetup +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)') + opush = repo.push + 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 + # XXX Kill me + 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 + + # XXX Kill me + 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, [])) + + # XXX move me on obssotre + @util.propertycache + def _obsoleteset(self): + """the set of obsolete revision""" + obs = set() + nm = self.changelog.nodemap + for prec in self.obsstore.precursors: + rev = nm.get(prec) + if rev is not None: + obs.add(rev) + return obs + + # XXX move me on obssotre + @util.propertycache + def _unstableset(self): + """the set of non obsolete revision with obsolete parent""" + return set(self.revs('(obsolete()::) - obsolete()')) + + # XXX move me on obssotre + @util.propertycache + def _suspendedset(self): + """the set of obsolete parent with non obsolete descendant""" + return set(self.revs('obsolete() and obsolete()::unstable()')) + + # XXX move me on obssotre + @util.propertycache + def _extinctset(self): + """the set of obsolete parent without non obsolete descendant""" + return set(self.revs('obsolete() - obsolete()::unstable()')) + + # XXX move me on obssotre + @util.propertycache + def _latecomerset(self): + """the set of rev trying to obsolete public revision""" + query = 'allsuccessors(public()) - obsolete() - public()' + return set(self.revs(query)) + + # XXX move me on obssotre + @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 + + # XXX kill me + 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: + tr = self.transaction('add-obsolete') + try: + meta = { + 'date': '%i %i' % util.makedate(), + 'user': ui.username(), + } + subs = (sub == nullid) and [] or [sub] + mid = self.obsstore.create(tr, obj, subs, 0, meta) + tr.close() + self._clearobsoletecache() + return mid + finally: + tr.release() + finally: + lock.release() + + # XXX kill me + 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) + + ### pull // push support + + def push(self, remote, *args, **opts): + """wrapper around pull that pull obsolete relation""" + try: + result = opush(remote, *args, **opts) + except util.Abort, ex: + hint = _("use 'hg stabilize' to get a stable history " + "or --force to ignore warnings") + if (len(ex.args) >= 1 + and ex.args[0].startswith('push includes ') + and ex.hint is None): + ex.hint = hint + raise + return result + + + repo.__class__ = obsoletingrepo + +@eh.reposetup +def _checkoldobsolete(ui, repo): + 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: + raise util.Abort('old format of obsolete marker detected!\n' + 'run `hg debugconvertobsolete` once.') + +### serialisation +############################# + +def _obsdeserialise(flike): + """read a file like object serialised with _obsserialise + + this desierialize into a {subject -> objects} mapping""" + rels = {} + for line in flike: + subhex, objhex = line.split() + subnode = bin(subhex) + if subnode == nullid: + subnode = None + rels.setdefault( subnode, set()).add(bin(objhex)) + return rels + @command('debugconvertobsolete', [], '') def cmddebugconvertobsolete(ui, repo): """import markers from an .hg/obsolete-relations file""" @@ -519,322 +1051,3 @@ 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 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: - tr = repo.transaction('post-amend-obst') - try: - meta = { - 'date': '%i %i' % util.makedate(), - 'user': ui.username(), - } - repo.obsstore.create(tr, oldnode, [new], 0, meta) - tr.close() - repo._clearobsoletecache() - finally: - tr.release() - finally: - lock.release() - return new - -def uisetup(ui): - extensions.wrapcommand(commands.table, "update", wrapmayobsoletewc) - extensions.wrapcommand(commands.table, "pull", wrapmayobsoletewc) - extensions.wrapfunction(cmdutil, 'amend', wrapcmdutilamend) - extensions.wrapfunction(discovery, 'checkheads', wrapcheckheads) - extensions.wrapfunction(phases, 'advanceboundary', wrapclearcache) - -### 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))) - -def _obsdeserialise(flike): - """read a file like object serialised with _obsserialise - - this desierialize into a {subject -> objects} mapping""" - rels = {} - for line in flike: - subhex, objhex = line.split() - subnode = bin(subhex) - if subnode == nullid: - subnode = None - 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) - -### 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)') - opush = repo.push - 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, [])) - - @util.propertycache - def _obsoleteset(self): - """the set of obsolete revision""" - obs = set() - nm = self.changelog.nodemap - for prec in self.obsstore.precursors: - rev = nm.get(prec) - 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: - tr = self.transaction('add-obsolete') - try: - meta = { - 'date': '%i %i' % util.makedate(), - 'user': ui.username(), - } - subs = (sub == nullid) and [] or [sub] - mid = self.obsstore.create(tr, obj, subs, 0, meta) - tr.close() - self._clearobsoletecache() - return mid - finally: - tr.release() - 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) - - ### pull // push support - - def push(self, remote, *args, **opts): - """wrapper around pull that pull obsolete relation""" - try: - result = opush(remote, *args, **opts) - except util.Abort, ex: - hint = _("use 'hg stabilize' to get a stable history " - "or --force to ignore warnings") - if (len(ex.args) >= 1 - and ex.args[0].startswith('push includes ') - and ex.hint is None): - ex.hint = hint - raise - return result - - - repo.__class__ = obsoletingrepo - 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: - raise util.Abort('old format of obsolete marker detected!\n' - 'run `hg debugconvertobsolete` once.')