hgext/evolve.py
branchstable
changeset 327 c017ad874dfc
parent 326 52c53e2d413b
child 355 72642a6970e0
--- 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)