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.
--- 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
--- 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)
--- 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