diff -r 232990fbecb5 -r c017ad874dfc hgext/evolve.py --- a/hgext/evolve.py Wed Jun 20 18:04:50 2012 +0200 +++ b/hgext/evolve.py Wed Jun 27 15:12:19 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,14 @@ else: rebase.rebasenode(repo, orig.node(), dest.node(), {node.nullrev: node.nullrev}) - nodenew = rebase.concludenode(repo, orig.node(), dest.node(), node.nullid) + try: + nodenew = rebase.concludenode(repo, orig.node(), dest.node(), + node.nullid) + except util.Abort: + repo.ui.write_err(_('/!\\ stabilize failed /!\\\n')) + repo.ui.write_err(_('/!\\ Their is no "hg stabilize --continue" /!\\\n')) + repo.ui.write_err(_('/!\\ use "hg up -C . ; hg stabilize --dry-run" /!\\\n')) + raise oldbookmarks = repo.nodebookmarks(nodesrc) if nodenew is not None: phases.retractboundary(repo, destphase, [nodenew]) @@ -200,7 +194,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 +213,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 +308,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 +331,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 +356,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 +396,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 +411,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 +425,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 +443,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 +465,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 +487,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 +678,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)