--- 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"))
- ],
- '<revs>')
+@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)