# HG changeset patch # User Pierre-Yves David # Date 1499763306 -7200 # Node ID 4eb90eace7f92091aaaed6606baa25df45c349ef # Parent e6bc6eaa17c56a00a4e8f589ea1072e902363daa uncommit: move to the 'evocommands' module We are about to play around with a UI mixing amend and uncommit, we take advantage of this to split the __init__.py module a bit more. diff -r e6bc6eaa17c5 -r 4eb90eace7f9 hgext3rd/evolve/__init__.py --- a/hgext3rd/evolve/__init__.py Tue Jul 11 10:38:01 2017 +0200 +++ b/hgext3rd/evolve/__init__.py Tue Jul 11 10:55:06 2017 +0200 @@ -316,6 +316,7 @@ aliases, entry = cmdutil.findcmd('commit', commands.table) commitopts3 = evocommands.commitopts3 interactiveopt = evocommands.commitopts3 +_bookmarksupdater = evocommands._bookmarksupdater # This extension contains the following code # @@ -959,21 +960,6 @@ _finalizerelocate(repo, orig, dest, nodenew, tr) return nodenew -def _bookmarksupdater(repo, oldid, tr): - """Return a callable update(newid) updating the current bookmark - and bookmarks bound to oldid to newid. - """ - def updatebookmarks(newid): - dirty = False - oldbookmarks = repo.nodebookmarks(oldid) - if oldbookmarks: - for b in oldbookmarks: - repo._bookmarks[b] = newid - dirty = True - if dirty: - repo._bookmarks.recordchange(tr) - return updatebookmarks - ### new command ############################# metadataopts = [ @@ -2406,186 +2392,6 @@ finally: lockmod.release(tr, lock, wlock) -def _touchedbetween(repo, source, dest, match=None): - touched = set() - for files in repo.status(source, dest, match=match)[:3]: - touched.update(files) - return touched - -def _commitfiltered(repo, ctx, match, target=None): - """Recommit ctx with changed files not in match. Return the new - node identifier, or None if nothing changed. - """ - base = ctx.p1() - if target is None: - target = base - # ctx - initialfiles = _touchedbetween(repo, base, ctx) - if base == target: - affected = set(f for f in initialfiles if match(f)) - newcontent = set() - else: - affected = _touchedbetween(repo, target, ctx, match=match) - newcontent = _touchedbetween(repo, target, base, match=match) - # The commit touchs all existing files - # + all file that needs a new content - # - the file affected bny uncommit with the same content than base. - files = (initialfiles - affected) | newcontent - if not newcontent and files == initialfiles: - return None - - # Filter copies - copied = copies.pathcopies(target, ctx) - copied = dict((dst, src) for dst, src in copied.iteritems() - if dst in files) - - def filectxfn(repo, memctx, path, contentctx=ctx, redirect=newcontent): - if path in redirect: - return filectxfn(repo, memctx, path, contentctx=target, redirect=()) - if path not in contentctx: - return None - fctx = contentctx[path] - flags = fctx.flags() - mctx = context.memfilectx(repo, 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) - -@eh.command( - '^uncommit', - [('a', 'all', None, _('uncommit all changes when no arguments given')), - ('r', 'rev', '', _('revert commit content to REV instead')), - ] + commands.walkopts, - _('[OPTION]... [NAME]')) -def uncommit(ui, repo, *pats, **opts): - """move changes from parent revision to working directory - - Changes to selected files in the checked out revision appear again as - uncommitted changed in the working directory. A new revision - without the selected changes is created, becomes the checked out - revision, and obsoletes the previous one. - - The --include option specifies patterns to uncommit. - The --exclude option specifies patterns to keep in the commit. - - The --rev argument let you change the commit file to a content of another - revision. It still does not change the content of your file in the working - directory. - - Return 0 if changed files are uncommitted. - """ - - wlock = lock = tr = None - try: - wlock = repo.wlock() - lock = repo.lock() - wctx = repo[None] - if len(wctx.parents()) <= 0: - raise error.Abort(_("cannot uncommit null changeset")) - if len(wctx.parents()) > 1: - raise error.Abort(_("cannot uncommit while merging")) - old = repo['.'] - if old.phase() == phases.public: - raise error.Abort(_("cannot rewrite immutable changeset")) - if len(old.parents()) > 1: - raise error.Abort(_("cannot uncommit merge changeset")) - oldphase = old.phase() - - rev = None - if opts.get('rev'): - rev = scmutil.revsingle(repo, opts.get('rev')) - ctx = repo[None] - if ctx.p1() == rev or ctx.p2() == rev: - raise error.Abort(_("cannot uncommit to parent changeset")) - - onahead = old.rev() in repo.changelog.headrevs() - disallowunstable = not obsolete.isenabled(repo, - obsolete.allowunstableopt) - if disallowunstable and not onahead: - raise error.Abort(_("cannot uncommit in the middle of a stack")) - - # Recommit the filtered changeset - tr = repo.transaction('uncommit') - updatebookmarks = _bookmarksupdater(repo, old.node(), tr) - newid = None - includeorexclude = opts.get('include') or opts.get('exclude') - if (pats or includeorexclude or opts.get('all')): - match = scmutil.match(old, pats, opts) - newid = _commitfiltered(repo, old, match, target=rev) - if newid is None: - raise error.Abort(_('nothing to uncommit'), - hint=_("use --all to uncommit all files")) - # Move local changes on filtered changeset - obsolete.createmarkers(repo, [(old, (repo[newid],))]) - phases.retractboundary(repo, tr, oldphase, [newid]) - with repo.dirstate.parentchange(): - 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 prune .' to remove it)\n")) - tr.close() - finally: - lockmod.release(tr, lock, wlock) - @eh.wrapcommand('commit') def commitwrapper(orig, ui, repo, *arg, **kwargs): tr = None diff -r e6bc6eaa17c5 -r 4eb90eace7f9 hgext3rd/evolve/evocommands.py --- a/hgext3rd/evolve/evocommands.py Tue Jul 11 10:38:01 2017 +0200 +++ b/hgext3rd/evolve/evocommands.py Tue Jul 11 10:55:06 2017 +0200 @@ -16,6 +16,14 @@ from mercurial import ( cmdutil, commands, + context, + copies, + error, + lock as lockmod, + node, + obsolete, + phases, + scmutil, util, ) @@ -55,6 +63,21 @@ interactiveopt = [['i', 'interactive', None, _('use interactive mode')]] +def _bookmarksupdater(repo, oldid, tr): + """Return a callable update(newid) updating the current bookmark + and bookmarks bound to oldid to newid. + """ + def updatebookmarks(newid): + dirty = False + oldbookmarks = repo.nodebookmarks(oldid) + if oldbookmarks: + for b in oldbookmarks: + repo._bookmarks[b] = newid + dirty = True + if dirty: + repo._bookmarks.recordchange(tr) + return updatebookmarks + @eh.command( 'amend|refresh', [('A', 'addremove', None, @@ -92,3 +115,183 @@ _resolveoptions(ui, opts) _alias, commitcmd = cmdutil.findcmd('commit', commands.table) return commitcmd[0](ui, repo, *pats, **opts) + +def _touchedbetween(repo, source, dest, match=None): + touched = set() + for files in repo.status(source, dest, match=match)[:3]: + touched.update(files) + return touched + +def _commitfiltered(repo, ctx, match, target=None): + """Recommit ctx with changed files not in match. Return the new + node identifier, or None if nothing changed. + """ + base = ctx.p1() + if target is None: + target = base + # ctx + initialfiles = _touchedbetween(repo, base, ctx) + if base == target: + affected = set(f for f in initialfiles if match(f)) + newcontent = set() + else: + affected = _touchedbetween(repo, target, ctx, match=match) + newcontent = _touchedbetween(repo, target, base, match=match) + # The commit touchs all existing files + # + all file that needs a new content + # - the file affected bny uncommit with the same content than base. + files = (initialfiles - affected) | newcontent + if not newcontent and files == initialfiles: + return None + + # Filter copies + copied = copies.pathcopies(target, ctx) + copied = dict((dst, src) for dst, src in copied.iteritems() + if dst in files) + + def filectxfn(repo, memctx, path, contentctx=ctx, redirect=newcontent): + if path in redirect: + return filectxfn(repo, memctx, path, contentctx=target, redirect=()) + if path not in contentctx: + return None + fctx = contentctx[path] + flags = fctx.flags() + mctx = context.memfilectx(repo, 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) + +@eh.command( + '^uncommit', + [('a', 'all', None, _('uncommit all changes when no arguments given')), + ('r', 'rev', '', _('revert commit content to REV instead')), + ] + commands.walkopts, + _('[OPTION]... [NAME]')) +def uncommit(ui, repo, *pats, **opts): + """move changes from parent revision to working directory + + Changes to selected files in the checked out revision appear again as + uncommitted changed in the working directory. A new revision + without the selected changes is created, becomes the checked out + revision, and obsoletes the previous one. + + The --include option specifies patterns to uncommit. + The --exclude option specifies patterns to keep in the commit. + + The --rev argument let you change the commit file to a content of another + revision. It still does not change the content of your file in the working + directory. + + Return 0 if changed files are uncommitted. + """ + + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + wctx = repo[None] + if len(wctx.parents()) <= 0: + raise error.Abort(_("cannot uncommit null changeset")) + if len(wctx.parents()) > 1: + raise error.Abort(_("cannot uncommit while merging")) + old = repo['.'] + if old.phase() == phases.public: + raise error.Abort(_("cannot rewrite immutable changeset")) + if len(old.parents()) > 1: + raise error.Abort(_("cannot uncommit merge changeset")) + oldphase = old.phase() + + rev = None + if opts.get('rev'): + rev = scmutil.revsingle(repo, opts.get('rev')) + ctx = repo[None] + if ctx.p1() == rev or ctx.p2() == rev: + raise error.Abort(_("cannot uncommit to parent changeset")) + + onahead = old.rev() in repo.changelog.headrevs() + disallowunstable = not obsolete.isenabled(repo, + obsolete.allowunstableopt) + if disallowunstable and not onahead: + raise error.Abort(_("cannot uncommit in the middle of a stack")) + + # Recommit the filtered changeset + tr = repo.transaction('uncommit') + updatebookmarks = _bookmarksupdater(repo, old.node(), tr) + newid = None + includeorexclude = opts.get('include') or opts.get('exclude') + if (pats or includeorexclude or opts.get('all')): + match = scmutil.match(old, pats, opts) + newid = _commitfiltered(repo, old, match, target=rev) + if newid is None: + raise error.Abort(_('nothing to uncommit'), + hint=_("use --all to uncommit all files")) + # Move local changes on filtered changeset + obsolete.createmarkers(repo, [(old, (repo[newid],))]) + phases.retractboundary(repo, tr, oldphase, [newid]) + with repo.dirstate.parentchange(): + 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 prune .' to remove it)\n")) + tr.close() + finally: + lockmod.release(tr, lock, wlock) diff -r e6bc6eaa17c5 -r 4eb90eace7f9 tests/test-uncommit.t --- a/tests/test-uncommit.t Tue Jul 11 10:38:01 2017 +0200 +++ b/tests/test-uncommit.t Tue Jul 11 10:55:06 2017 +0200 @@ -358,7 +358,7 @@ Test uncommiting precursors - $ hg uncommit --hidden --rev 'precursors(.)' b + $ hg uncommit --hidden --rev 'precursors(.)' b --traceback $ hg cat b --rev . b b