# HG changeset patch # User Pierre-Yves David # Date 1340793894 -7200 # Node ID 3edc829799d51021fa4cc6b677dd07b735d817bc # Parent f4a00b2d8bfba33d712dd576444b1a51232c6665# Parent 232990fbecb5d2e1d2fab8b1741dadb7b5c11d20 merge with stable doc update diff -r 232990fbecb5 -r 3edc829799d5 docs/obs-implementation.rst --- a/docs/obs-implementation.rst Wed Jun 20 18:04:50 2012 +0200 +++ b/docs/obs-implementation.rst Wed Jun 27 12:44:54 2012 +0200 @@ -200,7 +200,7 @@ * Use secret phase to remove from discovery obsolete and unstable changeset (to be improved soon) -* alter rebase to use obsolete marker instead of stripping. (XXX break --keep for now) +* alter rebase to use obsolete marker instead of stripping. * Have an experimental mq-like extension to rewrite history (more on that later) diff -r 232990fbecb5 -r 3edc829799d5 enable.sh --- a/enable.sh Wed Jun 20 18:04:50 2012 +0200 +++ b/enable.sh Wed Jun 27 12:44:54 2012 +0200 @@ -42,7 +42,7 @@ pstatus=status --rev .^ # diff with the previous amend -odiff=diff --rev 'limit(obsparents(.),1)' --rev . +odiff=diff --rev 'limit(precursors(.),1)' --rev . EOF cat << EOF >&2 diff -r 232990fbecb5 -r 3edc829799d5 hgext/evolve.py --- a/hgext/evolve.py Wed Jun 20 18:04:50 2012 +0200 +++ b/hgext/evolve.py Wed Jun 27 12:44:54 2012 +0200 @@ -27,31 +27,29 @@ ### util function ############################# + def noderange(repo, revsets): """The same as revrange but return node""" return map(repo.changelog.node, scmutil.revrange(repo, revsets)) - - -def warnunstable(orig, ui, repo, *args, **kwargs): +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()')) #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 #print orig, newunstables #print len(repo.revs('secret() - obsolete()')) if newunstables > 0: ui.warn(_('%i new unstables changesets\n') % newunstables) - - -### extension check -############################# - + if newlatecomers > 0: + ui.warn(_('%i new latecomers changesets\n') % newlatecomers) ### changeset rewriting logic ############################# @@ -65,7 +63,7 @@ if len(old.parents()) > 1: #XXX remove this unecessary limitation. raise error.Abort(_('cannot amend merge changesets')) base = old.p1() - bm = bookmarks.readcurrent(repo) + updatebookmarks = _bookmarksupdater(repo, old.node()) wlock = repo.wlock() try: @@ -136,21 +134,10 @@ new = repo[newid] created = len(repo) != revcount if created: - # update the bookmark - if bm: - repo._bookmarks[bm] = newid - bookmarks.write(repo) - + updatebookmarks(newid) # add evolution metadata - repo.addobsolete(new.node(), old.node()) - for u in updates: - repo.addobsolete(u.node(), old.node()) - repo.addobsolete(new.node(), u.node()) - oldbookmarks = repo.nodebookmarks(old.node()) - for book in oldbookmarks: - repo._bookmarks[book] = new.node() - if oldbookmarks: - bookmarks.write(repo) + collapsed = set([u.node() for u in updates] + [old.node()]) + repo.addcollapsedobsolete(collapsed, new.node()) else: # newid is an existing revision. It could make sense to # replace revisions with existing ones but probably not by @@ -179,7 +166,8 @@ else: rebase.rebasenode(repo, orig.node(), dest.node(), {node.nullrev: node.nullrev}) - nodenew = rebase.concludenode(repo, orig.node(), dest.node(), node.nullid) + nodenew = rebase.concludenode(repo, orig.node(), dest.node(), + node.nullid) oldbookmarks = repo.nodebookmarks(nodesrc) if nodenew is not None: phases.retractboundary(repo, destphase, [nodenew]) @@ -200,7 +188,6 @@ repo.dirstate.invalidate() raise - def stabilizableunstable(repo, pctx): """Return a changectx for an unstable changeset which can be stabilized on top of pctx or one of its descendants. None if none @@ -220,17 +207,34 @@ return unstables[0] return None +def _bookmarksupdater(repo, oldid): + """Return a callable update(newid) updating the current bookmark + and bookmarks bound to oldid to newid. + """ + bm = bookmarks.readcurrent(repo) + def updatebookmarks(newid): + dirty = False + if bm: + repo._bookmarks[bm] = newid + dirty = True + oldbookmarks = repo.nodebookmarks(oldid) + if oldbookmarks: + for b in oldbookmarks: + repo._bookmarks[b] = newid + dirty = True + if dirty: + bookmarks.write(repo) + return updatebookmarks + ### new command ############################# cmdtable = {} command = cmdutil.command(cmdtable) @command('^stabilize|evolve', - [ - ('n', 'dry-run', False, 'Do nothing but printing what should be done'), - ('A', 'any', False, 'Stabilize unstable change on any topological branch'), - ], - '') + [('n', 'dry-run', False, 'do not perform actions, print what to be done'), + ('A', 'any', False, 'stabilize any unstable changeset'),], + _('[OPTIONS]...')) def stabilize(ui, repo, **opts): """rebase an unstable changeset to make it stable again @@ -298,10 +302,10 @@ shorttemplate = '[{rev}] {desc|firstline}\n' @command('^gdown', - [], - 'update to working directory parent and display summary lines') + [], + '') def cmdgdown(ui, repo): - """update to working directory parent an display summary lines""" + """update to parent an display summary lines""" wkctx = repo[None] wparents = wkctx.parents() if len(wparents) != 1: @@ -321,10 +325,10 @@ return 1 @command('^gup', - [], - 'update to working directory children and display summary lines') + [], + '') def cmdup(ui, repo): - """update to working directory children an display summary lines""" + """update to child an display summary lines""" wkctx = repo[None] wparents = wkctx.parents() if len(wparents) != 1: @@ -346,12 +350,9 @@ ui.warn(_('Multiple non-obsolete children, explicitly update to one\n')) return 1 - -@command('^kill|obsolete', - [ - ('n', 'new', [], _("New changeset that justify this one to be killed")) - ], - '') +@command('^kill|obsolete|prune', + [('n', 'new', [], _("successor changeset"))], + _('[OPTION] REV...')) def kill(ui, repo, *revs, **opts): """mark a changeset as obsolete @@ -389,15 +390,11 @@ @command('^amend|refresh', [('A', 'addremove', None, _('mark new/missing files as added/removed before committing')), - ('n', 'note', '', - _('use text as commit message for this update')), - ('c', 'change', '', - _('specifies the changeset to amend'), _('REV')), - ('e', 'edit', False, - _('edit commit message.'), _('')), + ('n', 'note', '', _('use text as commit message for this update')), + ('c', 'change', '', _('specifies the changesets to amend'), _('REV')), + ('e', 'edit', False, _('invoke editor on commit messages')), ] + walkopts + commitopts + commitopts2, _('[OPTION]... [FILE]...')) - def amend(ui, repo, *pats, **opts): """combine a changeset with updates and replace it with a new one @@ -408,11 +405,9 @@ If you don't specify -m, the parent's message will be reused. - If you specify --change, amend additionally considers all changesets between - the indicated changeset and the working copy parent as updates to be subsumed. - This allows you to commit updates manually first. As a special shorthand you - can say `--amend .` instead of '--amend p1(p1())', which subsumes your latest - commit as an update of its parent. + If you specify --change, amend additionally considers all + changesets between the indicated changeset and the working copy + parent as updates to be subsumed. Behind the scenes, Mercurial first commits the update as a regular child of the current parent. Then it creates a new commit on the parent's parents @@ -424,17 +419,15 @@ """ # determine updates to subsume - change = opts.get('change', '.') - if change == '.': - change = 'p1(p1())' - old = scmutil.revsingle(repo, change) + old = scmutil.revsingle(repo, opts.get('change') or '.') lock = repo.lock() try: wlock = repo.wlock() try: - if not old.phase(): - raise util.Abort(_("can not rewrite immutable changeset %s") % old) + if old.phase() == phases.public: + raise util.Abort(_("can not rewrite immutable changeset %s") + % old) oldphase = old.phase() # commit current changes as update # code copied from commands.commit to avoid noisy messages @@ -444,8 +437,8 @@ ciopts['message'] = opts.get('note') or ('amends %s' % old.hex()) e = cmdutil.commiteditor def commitfunc(ui, repo, message, match, opts): - return repo.commit(message, opts.get('user'), opts.get('date'), match, - editor=e) + return repo.commit(message, opts.get('user'), opts.get('date'), + match, editor=e) revcount = len(repo) tempid = cmdutil.commit(ui, repo, commitfunc, pats, ciopts) if len(repo) == revcount: @@ -466,8 +459,6 @@ raise error.Abort(_('no updates found')) updates = [repo[n] for n in updatenodes] - - # perform amend if opts.get('edit'): opts['force_editor'] = True @@ -490,8 +481,142 @@ finally: lock.release() +def _commitfiltered(repo, ctx, match): + """Recommit ctx with changed files not in match. Return the new + node identifier, or None if nothing changed. + """ + base = ctx.p1() + m, a, r = repo.status(base, ctx)[:3] + allfiles = set(m + a + r) + files = set(f for f in allfiles if not match(f)) + if files == allfiles: + return None + # Filter copies + copied = copies.pathcopies(base, ctx) + copied = dict((src, dst) for src, dst in copied.iteritems() + if dst in files) + def filectxfn(repo, memctx, path): + if path not in ctx: + raise IOError() + fctx = ctx[path] + flags = fctx.flags() + mctx = context.memfilectx(fctx.path(), fctx.data(), + islink='l' in flags, + isexec='x' in flags, + copied=copied.get(path)) + return mctx + new = context.memctx(repo, + parents=[base.node(), node.nullid], + text=ctx.description(), + files=files, + filectxfn=filectxfn, + user=ctx.user(), + date=ctx.date(), + extra=ctx.extra()) + # commitctx always create a new revision, no need to check + newid = repo.commitctx(new) + return newid + +def _uncommitdirstate(repo, oldctx, match): + """Fix the dirstate after switching the working directory from + oldctx to a copy of oldctx not containing changed files matched by + match. + """ + ctx = repo['.'] + ds = repo.dirstate + copies = dict(ds.copies()) + m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3] + for f in m: + if ds[f] == 'r': + # modified + removed -> removed + continue + ds.normallookup(f) + + for f in a: + if ds[f] == 'r': + # added + removed -> unknown + ds.drop(f) + elif ds[f] != 'a': + ds.add(f) + + for f in r: + if ds[f] == 'a': + # removed + added -> normal + ds.normallookup(f) + elif ds[f] != 'r': + ds.remove(f) + + # Merge old parent and old working dir copies + oldcopies = {} + for f in (m + a): + src = oldctx[f].renamed() + if src: + oldcopies[f] = src[0] + oldcopies.update(copies) + copies = dict((dst, oldcopies.get(src, src)) + for dst, src in oldcopies.iteritems()) + # Adjust the dirstate copies + for dst, src in copies.iteritems(): + if (src not in ctx or dst in ctx or ds[dst] != 'a'): + src = None + ds.copy(src, dst) + +@command('^uncommit', + [('a', 'all', None, _('uncommit all changes when no arguments given')), + ] + commands.walkopts, + _('[OPTION]... [NAME]')) +def uncommit(ui, repo, *pats, **opts): + """move changes from parent revision to working directory + + Changes to selected files in parent revision appear again as + uncommitted changed in the working directory. A new revision + without selected changes is created, becomes the new parent and + obsoletes the previous one. + + The --include option specify pattern to uncommit + The --exclude option specify pattern to keep in the commit + + Return 0 if changed files are uncommitted. + """ + lock = repo.lock() + try: + wlock = repo.wlock() + try: + wctx = repo[None] + if len(wctx.parents()) <= 0: + raise util.Abort(_("cannot uncommit null changeset")) + if len(wctx.parents()) > 1: + raise util.Abort(_("cannot uncommit while merging")) + old = repo['.'] + if old.phase() == phases.public: + raise util.Abort(_("cannot rewrite immutable changeset")) + if len(old.parents()) > 1: + raise util.Abort(_("cannot uncommit merge changeset")) + oldphase = old.phase() + updatebookmarks = _bookmarksupdater(repo, old.node()) + # Recommit the filtered changeset + newid = None + if (pats or opts.get('include') or opts.get('exclude') + or opts.get('all')): + match = scmutil.match(old, pats, opts) + newid = _commitfiltered(repo, old, match) + if newid is None: + raise util.Abort(_('nothing to uncommit')) + # Move local changes on filtered changeset + repo.addobsolete(newid, old.node()) + phases.retractboundary(repo, oldphase, [newid]) + repo.dirstate.setparents(newid, node.nullid) + _uncommitdirstate(repo, old, match) + updatebookmarks(newid) + if not repo[newid].files(): + ui.warn(_("new changeset is empty\n")) + ui.status(_('(use "hg kill ." to remove it)\n')) + finally: + wlock.release() + finally: + lock.release() def commitwrapper(orig, ui, repo, *arg, **kwargs): lock = repo.lock() @@ -547,16 +672,19 @@ raise error.Abort(_('evolution extension require rebase extension.')) entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper) - entry[1].append(('o', 'obsolete', [], _("this commit obsolet this revision"))) + entry[1].append(('o', 'obsolete', [], + _("make commit obsolete this revision"))) entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper) - entry[1].append(('o', 'obsolete', [], _("this graft obsolet this revision"))) - entry[1].append(('O', 'old-obsolete', False, _("graft result obsolete graft source"))) + entry[1].append(('o', 'obsolete', [], + _("make graft obsoletes this revision"))) + entry[1].append(('O', 'old-obsolete', False, + _("make graft obsoletes its source"))) # warning about more obsolete - for cmd in ['commit', 'push', 'pull', 'graft']: - entry = extensions.wrapcommand(commands.table, cmd, warnunstable) - for cmd in ['kill', 'amend']: - entry = extensions.wrapcommand(cmdtable, cmd, warnunstable) + for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']: + entry = extensions.wrapcommand(commands.table, cmd, warnobserrors) + for cmd in ['amend', 'kill', 'uncommit']: + entry = extensions.wrapcommand(cmdtable, cmd, warnobserrors) if rebase is not None: - entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnunstable) + entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) diff -r 232990fbecb5 -r 3edc829799d5 hgext/obsolete.py --- a/hgext/obsolete.py Wed Jun 20 18:04:50 2012 +0200 +++ b/hgext/obsolete.py Wed Jun 27 12:44:54 2012 +0200 @@ -101,6 +101,7 @@ from mercurial.lock import release from mercurial import localrepo from mercurial import cmdutil +from mercurial import templatekw try: from mercurial.localrepo import storecache @@ -137,10 +138,22 @@ 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 + ### 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""" @@ -161,17 +174,21 @@ def revsetsuspended(repo, subset, x): """obsolete changesets with non obsolete descendants""" - args = revset.getargs(x, 0, 0, 'unstable takes no arguments') + 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, 'unstable takes no arguments') + 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 _obsparents(repo, s): - """obsolete parents of a subset""" +def _precursors(repo, s): + """Precursor of a changeset""" cs = set() nm = repo.changelog.nodemap markerbysubj = repo.obsoletestore.subjects @@ -182,14 +199,14 @@ cs.add(pr) return cs -def revsetobsparents(repo, subset, x): - """obsolete parents""" +def revsetprecursors(repo, subset, x): + """precursors of a subset""" s = revset.getset(repo, range(len(repo)), x) - cs = _obsparents(repo, s) + cs = _precursors(repo, s) return [r for r in subset if r in cs] -def _obsancestors(repo, s): - """obsolete ancestors of a subset""" +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.obsoletestore.subjects @@ -208,13 +225,73 @@ cs.add(pr) return cs -def revsetobsancestors(repo, subset, x): +def revsetallprecursors(repo, subset, x): """obsolete parents""" s = revset.getset(repo, range(len(repo)), x) - cs = _obsancestors(repo, s) + cs = _allprecursors(repo, s) + return [r for r in subset if r in cs] + +def _successors(repo, s): + """Successors of a changeset""" + cs = set() + nm = repo.changelog.nodemap + markerbyobj = repo.obsoletestore.objects + for r in s: + for p in markerbyobj.get(repo[r].node(), ()): + for sub in p['subjects']: + 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.obsoletestore.objects + while toproceed: + nc = toproceed.pop() + for mark in allobjects.get(nc, ()): + for sub in mark['subjects']: + 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 ############################ @@ -245,9 +322,8 @@ return newrev def cmdrebase(orig, ui, repo, *args, **kwargs): - if kwargs.get('keep', False): - raise util.Abort(_('rebase --keep option is unsupported with obsolete ' - 'extension'), hint=_("see 'hg help obsolete'")) + + reallykeep = kwargs.get('keep', False) kwargs = dict(kwargs) kwargs['keep'] = True @@ -260,33 +336,41 @@ # added from this state after a successful call. repo._rebasestate = {} repo._rebasetarget = None - maxrev = len(repo) - 1 try: res = orig(ui, repo, *args, **kwargs) - if not res and not kwargs.get('abort') and repo._rebasetarget: - # We have to tell rewritten revisions from removed - # ones. When collapsing, removed revisions are considered - # to be collapsed onto the final one, while in the normal - # case their are marked obsolete without successor. - emptynode = nullid - if kwargs.get('collapse'): - emptynode = repo[max(repo._rebasestate.values())].node() - # 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]) - for rev, newrev in sorted(repo._rebasestate.items()): - if newrev == -2: # nullmerge - continue - oldnode = repo[rev].node() - if newrev not in newrevs and newrev >= 0: - newnode = repo[newrev].node() - newrevs.add(newrev) + 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: - newnode = emptynode - repo.addobsolete(newnode, oldnode) + for oldnode, newnode in replacements.iteritems(): + repo.addobsolete(newnode, oldnode) return res finally: delattr(repo, '_rebasestate') @@ -295,13 +379,20 @@ 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["obsparents"] = revsetobsparents - revset.symbols["obsancestors"] = revsetobsancestors + revset.symbols["latecomer"] = revsetlatecomer + 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 try: rebase = extensions.find('rebase') @@ -311,7 +402,7 @@ extensions.wrapfunction(rebase, 'concludenode', concludenode) extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase) except KeyError: - pass # rebase not found + pass # rebase not found # Pushkey mechanism for mutable ######################################### @@ -382,6 +473,9 @@ if ctx.obsolete(): raise util.Abort(_("Trying to push obsolete changeset: %s!") % ctx, hint=hint) + if ctx.latecomer(): + raise util.Abort(_("Trying to push latecomer changeset: %s!") % ctx, + hint=hint) ### patch remote branch map # do not read it this burn eyes try: @@ -436,6 +530,12 @@ else: return None # break recursion +def wrapclearcache(orig, repo, *args, **kwargs): + try: + return orig(repo, *args, **kwargs) + finally: + repo._clearobsoletecache() + ### New commands ############################# @@ -527,12 +627,35 @@ 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: + newmarker = { + 'subjects': [new], + 'object': oldnode, + 'date': util.makedate(), + 'user': ui.username(), + 'reason': 'commit --amend', + } + repo.obsoletestore.new(newmarker) + 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) @@ -767,6 +890,11 @@ """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""" + return set(self.revs('allsuccessors(public()) - obsolete()')) + def _clearobsoletecache(self): if '_obsoleteset' in vars(self): del self._obsoleteset @@ -783,6 +911,8 @@ del self._suspendedset if '_extinctset' in vars(self): del self._extinctset + if '_latecomerset' in vars(self): + del self._latecomerset def addobsolete(self, sub, obj): """Add a relation marking that node is a new version of """ @@ -812,12 +942,22 @@ 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 - expobs = [c.node() for c in repo.set('extinct() - secret()')] + query = '(obsolete() - obsolete()::(unstable() - secret())) - secret()' + expobs = [c.node() for c in repo.set(query)] phases.retractboundary(repo, 2, expobs) ### Disk IO @@ -954,13 +1094,3 @@ return c repo.__class__ = obsoletingrepo - - if False: - expobs = [c.node() for c in repo.set('extinct() - secret()')] - if expobs: # do not lock in nothing move. locking for peanut make hgview reload on any command - lock = repo.lock() - try: - expobs = [c.node() for c in repo.set('extinct() - secret()')] - phases.retractboundary(repo, 2, expobs) - finally: - lock.release() diff -r 232990fbecb5 -r 3edc829799d5 tests/test-amend.t --- a/tests/test-amend.t Wed Jun 20 18:04:50 2012 +0200 +++ b/tests/test-amend.t Wed Jun 27 12:44:54 2012 +0200 @@ -26,7 +26,6 @@ $ hg amend $ hg debugsuccessors 07f494440405 a34b93d251e4 - 07f494440405 bd19cbe78fbf bd19cbe78fbf a34b93d251e4 $ hg branch foo @@ -65,7 +64,6 @@ [255] $ hg debugsuccessors 07f494440405 a34b93d251e4 - 07f494440405 bd19cbe78fbf bd19cbe78fbf a34b93d251e4 $ hg phase 2 2: draft @@ -92,7 +90,6 @@ [255] $ hg debugsuccessors 07f494440405 a34b93d251e4 - 07f494440405 bd19cbe78fbf 7384bbcba36f 000000000000 bd19cbe78fbf a34b93d251e4 $ glog diff -r 232990fbecb5 -r 3edc829799d5 tests/test-evolve.t --- a/tests/test-evolve.t Wed Jun 20 18:04:50 2012 +0200 +++ b/tests/test-evolve.t Wed Jun 27 12:44:54 2012 +0200 @@ -23,6 +23,10 @@ > hg ci -m "add $1" > } + $ glog() { + > hg glog --template '{rev}:{node|short}@{branch}({phase}) {desc|firstline}\n' "$@" + > } + various init $ hg init local @@ -52,7 +56,9 @@ test simple kill - $ hg kill 5 + $ hg id -n + 5 + $ hg kill . 0 files updated, 0 files merged, 1 files removed, 0 files unresolved working directory now at fbb94e3a0ecf $ hg qlog @@ -71,6 +77,18 @@ 2 - 4538525df7e2 add c (draft) 1 - 7c3bad9141dc add b (public) 0 - 1f0dee641bb7 add a (public) + +test kill with dirty changes + + $ hg up 2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo 4 > g + $ hg add g + $ hg kill . + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory now at 7c3bad9141dc + $ hg st + A g $ cd .. ########################## @@ -209,8 +227,27 @@ 4 feature-B: another feature - test 1 : a nifty feature - test 0 : base - test - $ hg up -q 1 - Working directory parent is obsolete + $ hg up -q 0 + $ glog --hidden + o 6:23409eba69a0@default(draft) a nifty feature + | + | o 5:e416e48b2742@default(secret) french looks better + | | + | | o 4:f8111a076f09@default(draft) another feature + | |/ + | | o 3:524e478d4811@default(secret) fix spelling of Zwei + | | | + | | o 2:7b36850622b2@default(secret) another feature + | |/ + | o 1:568a468b60fc@default(draft) a nifty feature + |/ + @ 0:e55e0562ee93@default(draft) base + + $ hg debugsuccessors + 524e478d4811 f8111a076f09 + 568a468b60fc 23409eba69a0 + 7b36850622b2 f8111a076f09 + e416e48b2742 23409eba69a0 $ hg stabilize move:[4] another feature atop:[6] a nifty feature @@ -234,6 +271,12 @@ 8 feature-B: another feature that rox - test 6 feature-A: a nifty feature - test 0 : base - test + +phase change turning obsolete changeset public issue a latecomer warning + + $ hg phase --public 7 + 1 new latecomers changesets + $ cd .. enable general delta diff -r 232990fbecb5 -r 3edc829799d5 tests/test-obsolete-push.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-obsolete-push.t Wed Jun 27 12:44:54 2012 +0200 @@ -0,0 +1,48 @@ + $ cat >> $HGRCPATH < [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ template='{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' + $ glog() { + > hg glog --template "$template" "$@" + > } + +Test outgoing, common A is suspended, B unstable and C secret, remote +has A and B, neither A or C should be in outgoing. + + $ hg init source + $ cd source + $ echo a > a + $ hg ci -qAm A a + $ echo b > b + $ hg ci -qAm B b + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo c > c + $ hg ci -qAm C c + $ hg phase --secret --force . + $ hg kill 0 1 + 1 new unstables changesets + $ glog --hidden + @ 2:244232c2222a@default(unstable/secret) C + | + | o 1:6c81ed0049f8@default(extinct/secret) B + |/ + o 0:1994f17a630e@default(suspended/secret) A + + $ hg init ../clone + $ cat > ../clone/.hg/hgrc < [phases] + > publish = false + > EOF + $ hg outgoing ../clone --template "$template" + comparing with ../clone + searching for changes + no changes found (ignored 2 secret changesets) + [1] diff -r 232990fbecb5 -r 3edc829799d5 tests/test-obsolete-rebase.t --- a/tests/test-obsolete-rebase.t Wed Jun 20 18:04:50 2012 +0200 +++ b/tests/test-obsolete-rebase.t Wed Jun 27 12:44:54 2012 +0200 @@ -30,11 +30,43 @@ created new head $ echo e > e $ hg ci -Am adde e - $ hg rebase -d 1 -r . --detach --keep - abort: rebase --keep option is unsupported with obsolete extension - (see 'hg help obsolete') - [255] - $ hg rebase -d 1 -r . --detach + $ hg rebase -d 1 -r 3 --detach --keep + $ glog + @ 4:9c5494949763@default(draft) adde + | + | o 3:98e4a024635e@default(draft) adde + | | + | o 2:102a90ea7b4a@default(draft) addb + | | + o | 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + + $ glog --hidden + @ 4:9c5494949763@default(draft) adde + | + | o 3:98e4a024635e@default(draft) adde + | | + | o 2:102a90ea7b4a@default(draft) addb + | | + o | 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + + $ hg debugsuccessors + $ hg --config extensions.hgext.mq= strip tip + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9c5494949763-backup.hg + $ hg rebase -d 1 -r 3 --detach + $ glog + @ 4:9c5494949763@default(draft) adde + | + | o 2:102a90ea7b4a@default(draft) addb + | | + o | 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + $ glog --hidden @ 4:9c5494949763@default(draft) adde | diff -r 232990fbecb5 -r 3edc829799d5 tests/test-obsolete.t --- a/tests/test-obsolete.t Wed Jun 20 18:04:50 2012 +0200 +++ b/tests/test-obsolete.t Wed Jun 27 12:44:54 2012 +0200 @@ -5,7 +5,7 @@ > [phases] > publish=False > [alias] - > odiff=diff --rev 'limit(obsparents(.),1)' --rev . + > odiff=diff --rev 'limit(precursors(.),1)' --rev . > [extensions] > hgext.graphlog= > EOF @@ -54,7 +54,7 @@ Test that obsolete parent a properly computed - $ qlog -r 'obsparents(.)' --hidden + $ qlog -r 'precursors(.)' --hidden 2 - 4538525df7e2 $ qlog -r . @@ -72,6 +72,12 @@ @@ -0,0 +1,1 @@ +obsol_c +Test that obsolete successors a properly computed + + $ qlog -r 'successors(2)' --hidden + 3 + - 0d3f46688ccc + test obsolete changeset with no-obsolete descendant $ hg up 1 -q $ mkcommit "obsol_c'" # 4 (on 1) @@ -89,11 +95,16 @@ - 4538525df7e2 3 - 0d3f46688ccc - $ qlog -r 'obsancestors(4)' --hidden + $ qlog -r 'allprecursors(4)' --hidden 2 - 4538525df7e2 3 - 0d3f46688ccc + $ qlog -r 'allsuccessors(2)' --hidden + 3 + - 0d3f46688ccc + 4 + - 725c380fe99b $ hg up 3 -q Working directory parent is obsolete $ mkcommit d # 5 (on 3) @@ -111,6 +122,23 @@ 5 - a7a6f2b5d8a5 +Test obsolete keyword + + $ hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' \ + > --hidden + @ 5:a7a6f2b5d8a5@default(unstable/secret) add d + | + | o 4:725c380fe99b@default(stable/draft) add obsol_c' + | | + o | 3:0d3f46688ccc@default(suspended/secret) add obsol_c + |/ + | o 2:4538525df7e2@default(extinct/secret) add c + |/ + o 1:7c3bad9141dc@default(stable/draft) add b + | + o 0:1f0dee641bb7@default(stable/public) add a + + Test communication of obsolete relation with a compatible client $ hg init ../other-new @@ -454,3 +482,99 @@ adding file changes added 1 changesets with 1 changes to 1 files (+1 heads) $ cd .. + +check latecomer detection +(make an obsolete changeset public) + + $ cd local + $ hg phase --public 11 + $ hg --config extensions.graphlog=glog glog --template='{rev} - ({phase}) {node|short} {desc}\n' + @ 12 - (draft) 6db5e282cb91 add obsol_d''' + | + | o 11 - (public) 9468a5f5d8b2 add obsol_d'' + |/ + o 10 - (public) 2033b4e49474 add obsol_c + | + o 4 - (public) 725c380fe99b add obsol_c' + | + o 1 - (public) 7c3bad9141dc add b + | + o 0 - (public) 1f0dee641bb7 add a + + $ hg log -r 'latecomer()' + changeset: 12:6db5e282cb91 + tag: tip + parent: 10:2033b4e49474 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add obsol_d''' + + $ hg push ../other-new/ + pushing to ../other-new/ + searching for changes + abort: Trying to push latecomer changeset: 6db5e282cb91! + (use 'hg stabilize' to get a stable history (or --force to proceed)) + [255] + +Check hg commit --amend compat + + $ hg up 'desc(obsol_c)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit f + created new head + $ echo 42 >> f + $ hg commit --amend --traceback + saved backup bundle to $TESTTMP/local/.hg/strip-backup/0b1b6dd009c0-amend-backup.hg + $ hg glog + @ changeset: 13:3734a65252e6 + | tag: tip + | parent: 10:2033b4e49474 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add f + | + | o changeset: 12:6db5e282cb91 + |/ parent: 10:2033b4e49474 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add obsol_d''' + | + | o changeset: 11:9468a5f5d8b2 + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add obsol_d'' + | + o changeset: 10:2033b4e49474 + | parent: 4:725c380fe99b + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add obsol_c + | + o changeset: 4:725c380fe99b + | parent: 1:7c3bad9141dc + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add obsol_c' + | + o changeset: 1:7c3bad9141dc + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add b + | + o changeset: 0:1f0dee641bb7 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add a + + $ hg debugsuccessors + 0b1b6dd009c0 3734a65252e6 + 0d3f46688ccc 2033b4e49474 + 0d3f46688ccc 725c380fe99b + 159dfc9fa5d3 9468a5f5d8b2 + 1f0dee641bb7 83b5778897ad + 4538525df7e2 0d3f46688ccc + 83b5778897ad 000000000000 + 909a0fb57e5d 159dfc9fa5d3 + 9468a5f5d8b2 6db5e282cb91 + 95de7fc6918d 909a0fb57e5d + a7a6f2b5d8a5 95de7fc6918d diff -r 232990fbecb5 -r 3edc829799d5 tests/test-stabilize-order.t --- a/tests/test-stabilize-order.t Wed Jun 20 18:04:50 2012 +0200 +++ b/tests/test-stabilize-order.t Wed Jun 27 12:44:54 2012 +0200 @@ -106,9 +106,9 @@ 3a4a591493f8 f5ff10856e5a 3ca0ded0dc50 ab8cbb6d87ff +7a7552255fb5 5e819fbb0d27 - 93418d2c0979 3a4a591493f8 93418d2c0979 f5ff10856e5a ab8cbb6d87ff 6bf44048e43f + ef23d6ef94d6 ab8cbb6d87ff [1] $ glog @ 9:5e819fbb0d27@default(draft) addc diff -r 232990fbecb5 -r 3edc829799d5 tests/test-stabilize-result.t --- a/tests/test-stabilize-result.t Wed Jun 20 18:04:50 2012 +0200 +++ b/tests/test-stabilize-result.t Wed Jun 27 12:44:54 2012 +0200 @@ -47,6 +47,5 @@ $ hg debugsuccessors 102a90ea7b4a 1447e1c4828d - 102a90ea7b4a 41ad4fe8c795 41ad4fe8c795 1447e1c4828d cce2c55b8965 000000000000 diff -r 232990fbecb5 -r 3edc829799d5 tests/test-uncommit.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-uncommit.t Wed Jun 27 12:44:54 2012 +0200 @@ -0,0 +1,334 @@ + $ cat >> $HGRCPATH < [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ glog() { + > hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' "$@" + > } + + $ hg init repo + $ cd repo + +Cannot uncommit null changeset + + $ hg uncommit + abort: cannot rewrite immutable changeset + [255] + +Cannot uncommit public changeset + + $ echo a > a + $ hg ci -Am adda a + $ hg phase --public . + $ hg uncommit + abort: cannot rewrite immutable changeset + [255] + $ hg phase --force --draft . + +Cannot uncommit merge + + $ hg up -q null + $ echo b > b + $ echo c > c + $ echo d > d + $ echo f > f + $ echo g > g + $ echo j > j + $ echo m > m + $ echo n > n + $ echo o > o + $ hg ci -Am addmore + adding b + adding c + adding d + adding f + adding g + adding j + adding m + adding n + adding o + created new head + $ hg merge + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg uncommit + abort: cannot uncommit while merging + [255] + $ hg ci -m merge + $ hg uncommit + abort: cannot uncommit merge changeset + [255] + +Prepare complicated changeset + + $ hg branch bar + marked working directory as branch bar + (branches are permanent and global, did you want a bookmark?) + $ hg cp a aa + $ echo b >> b + $ hg rm c + $ echo d >> d + $ echo e > e + $ hg mv f ff + $ hg mv g h + $ echo j >> j + $ echo k > k + $ echo l > l + $ hg rm m + $ hg rm n + $ echo o >> o + $ hg ci -Am touncommit + adding e + adding k + adding l + $ hg st --copies --change . + M b + M d + M j + M o + A aa + a + A e + A ff + f + A h + g + A k + A l + R c + R f + R g + R m + R n + $ hg man -r . + a + aa + b + d + e + ff + h + j + k + l + o + +Add a couple of bookmarks + + $ glog --hidden + @ 3:5eb72dbe0cb4@bar(stable/draft) touncommit + | + o 2:f63b90038565@default(stable/draft) merge + |\ + | o 1:f15c744d48e8@default(stable/draft) addmore + | + o 0:07f494440405@default(stable/draft) adda + + $ hg bookmark -r 2 unrelated + $ hg bookmark touncommit-bm + $ hg bookmark --inactive touncommit-bm-inactive + $ hg bookmarks + * touncommit-bm 3:5eb72dbe0cb4 + touncommit-bm-inactive 3:5eb72dbe0cb4 + unrelated 2:f63b90038565 + +Prepare complicated working directory + + $ hg branch foo + marked working directory as branch foo + (branches are permanent and global, did you want a bookmark?) + $ hg mv ff f + $ hg mv h i + $ hg rm j + $ hg rm k + $ echo l >> l + $ echo m > m + $ echo o > o + +Test uncommit without argument, should be a no-op + + $ hg uncommit + abort: nothing to uncommit + [255] + $ hg bookmarks + * touncommit-bm 3:5eb72dbe0cb4 + touncommit-bm-inactive 3:5eb72dbe0cb4 + unrelated 2:f63b90038565 + +Test no matches + + $ hg uncommit --include nothere + abort: nothing to uncommit + [255] + +Enjoy uncommit + + $ hg uncommit aa b c f ff g h j k l m o + $ hg branch + foo + $ hg st --copies + M b + A aa + a + A i + g + A l + R c + R g + R j + R m + $ cat aa + a + $ cat b + b + b + $ cat l + l + l + $ cat m + m + $ test -f c && echo 'error: c was removed!' + [1] + $ test -f j && echo 'error: j was removed!' + [1] + $ test -f k && echo 'error: k was removed!' + [1] + $ hg st --copies --change . + M d + A e + R n + $ hg man -r . + a + b + c + d + e + f + g + j + m + o + $ hg cat -r . d + d + d + $ hg cat -r . e + e + $ glog --hidden + @ 4:e8db4aa611f6@bar(stable/draft) touncommit + | + | o 3:5eb72dbe0cb4@bar(extinct/secret) touncommit + |/ + o 2:f63b90038565@default(stable/draft) merge + |\ + | o 1:f15c744d48e8@default(stable/draft) addmore + | + o 0:07f494440405@default(stable/draft) adda + + $ hg bookmarks + * touncommit-bm 4:e8db4aa611f6 + touncommit-bm-inactive 4:e8db4aa611f6 + unrelated 2:f63b90038565 + $ hg debugsuccessors + 5eb72dbe0cb4 e8db4aa611f6 + +Test phase is preserved, no local changes + + $ hg up -C 3 + 8 files updated, 0 files merged, 1 files removed, 0 files unresolved + Working directory parent is obsolete + $ hg --config extensions.purge= purge + $ hg uncommit -I 'set:added() and e' + $ hg st --copies + A e + $ hg st --copies --change . + M b + M d + M j + M o + A aa + A ff + f + A h + g + A k + A l + R c + R f + R g + R m + R n + $ glog --hidden + @ 5:c706fe2c12f8@bar(stable/secret) touncommit + | + | o 4:e8db4aa611f6@bar(stable/draft) touncommit + |/ + | o 3:5eb72dbe0cb4@bar(extinct/secret) touncommit + |/ + o 2:f63b90038565@default(stable/draft) merge + |\ + | o 1:f15c744d48e8@default(stable/draft) addmore + | + o 0:07f494440405@default(stable/draft) adda + + $ hg debugsuccessors + 5eb72dbe0cb4 c706fe2c12f8 + 5eb72dbe0cb4 e8db4aa611f6 + +Test --all + + $ hg up -C 3 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + Working directory parent is obsolete + $ hg --config extensions.purge= purge + $ hg uncommit --all -X e + $ hg st --copies + M b + M d + M j + M o + A aa + a + A ff + f + A h + g + A k + A l + R c + R f + R g + R m + R n + $ hg st --copies --change . + A e + + $ hg debugsuccessors + 5eb72dbe0cb4 c4cbebac3751 + 5eb72dbe0cb4 c706fe2c12f8 + 5eb72dbe0cb4 e8db4aa611f6 + +Display a warning if nothing left + + $ hg uncommit e + new changeset is empty + (use "hg kill ." to remove it) + $ hg debugsuccessors + 5eb72dbe0cb4 c4cbebac3751 + 5eb72dbe0cb4 c706fe2c12f8 + 5eb72dbe0cb4 e8db4aa611f6 + c4cbebac3751 4f1c269eab68 + +Test instability warning + + $ hg ci -m touncommit + $ echo unrelated > unrelated + $ hg ci -Am addunrelated unrelated + $ hg gdown + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + [8] touncommit + $ hg uncommit aa + 1 new unstables changesets