hgext/evolve.py
changeset 133 aa182b912d62
parent 131 3124889cad55
child 137 bbc653876876
equal deleted inserted replaced
132:64d16f07d67f 133:aa182b912d62
       
     1 # states.py - introduce the state concept for mercurial changeset
       
     2 #
       
     3 # Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
       
     4 #                Logilab SA        <contact@logilab.fr>
       
     5 #                Pierre-Yves David <pierre-yves.david@ens-lyon.org>
       
     6 #
       
     7 # This software may be used and distributed according to the terms of the
       
     8 # GNU General Public License version 2 or any later version.
       
     9 
       
    10 '''A set of command to make changeset evolve.'''
       
    11 
       
    12 from mercurial import cmdutil
       
    13 from mercurial import scmutil
       
    14 from mercurial import node
       
    15 from mercurial import error
       
    16 from mercurial import extensions
       
    17 from mercurial import commands
       
    18 from mercurial import bookmarks
       
    19 from mercurial import phases
       
    20 from mercurial import context
       
    21 from mercurial import commands
       
    22 from mercurial import util
       
    23 from mercurial.i18n import _
       
    24 from mercurial.commands import walkopts, commitopts, commitopts2, logopt
       
    25 from mercurial import hg
       
    26 
       
    27 ### util function
       
    28 #############################
       
    29 def noderange(repo, revsets):
       
    30     """The same as revrange but return node"""
       
    31     return map(repo.changelog.node,
       
    32                scmutil.revrange(repo, revsets))
       
    33 
       
    34 ### extension check
       
    35 #############################
       
    36 
       
    37 def extsetup(ui):
       
    38     try:
       
    39         obsolete = extensions.find('obsolete')
       
    40     except KeyError:
       
    41         raise error.Abort(_('evolution extension require obsolete extension.'))
       
    42     try:
       
    43         rebase = extensions.find('rebase')
       
    44     except KeyError:
       
    45         raise error.Abort(_('evolution extension require rebase extension.'))
       
    46 
       
    47 ### changeset rewriting logic
       
    48 #############################
       
    49 
       
    50 def rewrite(repo, old, updates, head, newbases, commitopts):
       
    51     if len(old.parents()) > 1: #XXX remove this unecessary limitation.
       
    52         raise error.Abort(_('cannot amend merge changesets'))
       
    53     base = old.p1()
       
    54     bm = bookmarks.readcurrent(repo)
       
    55 
       
    56     wlock = repo.wlock()
       
    57     try:
       
    58 
       
    59         # commit a new version of the old changeset, including the update
       
    60         # collect all files which might be affected
       
    61         files = set(old.files())
       
    62         for u in updates:
       
    63             files.update(u.files())
       
    64         # prune files which were reverted by the updates
       
    65         def samefile(f):
       
    66             if f in head.manifest():
       
    67                 a = head.filectx(f)
       
    68                 if f in base.manifest():
       
    69                     b = base.filectx(f)
       
    70                     return (a.data() == b.data()
       
    71                             and a.flags() == b.flags()
       
    72                             and a.renamed() == b.renamed())
       
    73                 else:
       
    74                     return False
       
    75             else:
       
    76                 return f not in base.manifest()
       
    77         files = [f for f in files if not samefile(f)]
       
    78         # commit version of these files as defined by head
       
    79         headmf = head.manifest()
       
    80         def filectxfn(repo, ctx, path):
       
    81             if path in headmf:
       
    82                 return head.filectx(path)
       
    83             raise IOError()
       
    84         if commitopts.get('message') and commitopts.get('logfile'):
       
    85             raise util.Abort(_('options --message and --logfile are mutually'
       
    86                                ' exclusive'))
       
    87         if commitopts.get('logfile'):
       
    88             message= open(commitopts['logfile']).read()
       
    89         elif commitopts.get('message'):
       
    90             message = commitopts['message']
       
    91         else:
       
    92             message = old.description()
       
    93 
       
    94 
       
    95 
       
    96         new = context.memctx(repo,
       
    97                              parents=newbases,
       
    98                              text=message,
       
    99                              files=files,
       
   100                              filectxfn=filectxfn,
       
   101                              user=commitopts.get('user') or None,
       
   102                              date=commitopts.get('date') or None,
       
   103                              extra=commitopts.get('extra') or None)
       
   104 
       
   105         if commitopts.get('edit'):
       
   106             new._text = cmdutil.commitforceeditor(repo, new, [])
       
   107         newid = repo.commitctx(new)
       
   108         new = repo[newid]
       
   109 
       
   110         # update the bookmark
       
   111         if bm:
       
   112             repo._bookmarks[bm] = newid
       
   113             bookmarks.write(repo)
       
   114 
       
   115         # hide obsolete csets
       
   116         repo.changelog.hiddeninit = False
       
   117 
       
   118         # add evolution metadata
       
   119         repo.addobsolete(new.node(), old.node())
       
   120         for u in updates:
       
   121             repo.addobsolete(u.node(), old.node())
       
   122             repo.addobsolete(new.node(), u.node())
       
   123 
       
   124     finally:
       
   125         wlock.release()
       
   126 
       
   127     return newid
       
   128 
       
   129 def relocate(repo, rev, dest):
       
   130     """rewrite <rev> on dest"""
       
   131     try:
       
   132         rebase = extensions.find('rebase')
       
   133         # dummy state to trick rebase node
       
   134         assert repo[rev].p2().rev() == node.nullrev, 'no support yet'
       
   135         cmdutil.duplicatecopies(repo, rev, repo[dest].node(),
       
   136                                          repo[rev].p2().node())
       
   137         rebase.rebasenode(repo, rev, dest, {node.nullrev: node.nullrev})
       
   138         nodenew = rebase.concludenode(repo, rev, dest, node.nullid)
       
   139         nodesrc = repo.changelog.node(rev)
       
   140         repo.addobsolete(nodenew, nodesrc)
       
   141         phases.retractboundary(repo, repo[nodesrc].phase(), [nodenew])
       
   142         oldbookmarks = repo.nodebookmarks(nodesrc)
       
   143         for book in oldbookmarks:
       
   144             repo._bookmarks[book] = nodenew
       
   145         if oldbookmarks:
       
   146             bookmarks.write(repo)
       
   147     except util.Abort:
       
   148         # Invalidate the previous setparents
       
   149         repo.dirstate.invalidate()
       
   150         raise
       
   151 
       
   152 
       
   153 
       
   154 ### new command
       
   155 #############################
       
   156 cmdtable = {}
       
   157 command = cmdutil.command(cmdtable)
       
   158 
       
   159 @command('^evolve',
       
   160     [],
       
   161     '')
       
   162 def evolve(ui, repo):
       
   163     """suggest the next evolution step"""
       
   164     obsolete = extensions.find('obsolete')
       
   165     next = min(obsolete.unstables(repo))
       
   166     obs = repo[next].parents()[0]
       
   167     if not obs.obsolete():
       
   168         obs = next.parents()[1]
       
   169     assert obs.obsolete()
       
   170     newer = obsolete.newerversion(repo, obs.node())
       
   171     target = newer[-1]
       
   172     repo.ui.status('hg relocate --rev %s %s\n' % (repo[next], repo[target]))
       
   173 
       
   174 shorttemplate = '[{rev}] {desc|firstline}\n'
       
   175 
       
   176 @command('^gdown',
       
   177     [],
       
   178     'update to working directory parent an display summary lines')
       
   179 def cmdgdown(ui, repo):
       
   180     wkctx = repo[None]
       
   181     wparents = wkctx.parents()
       
   182     if len(wparents) != 1:
       
   183         raise util.Abort('merge in progress')
       
   184 
       
   185     parents = wparents[0].parents()
       
   186     displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
       
   187     if len(parents) == 1:
       
   188         p = parents[0]
       
   189         hg.update(repo, p.rev())
       
   190         displayer.show(p)
       
   191         return 0
       
   192     else:
       
   193         for p in parents:
       
   194             displayer.show(p)
       
   195         ui.warn(_('multiple parents, explicitly update to one\n'))
       
   196         return 1
       
   197 
       
   198 @command('^gup',
       
   199     [],
       
   200     'update to working directory children an display summary lines')
       
   201 def cmdup(ui, repo):
       
   202     wkctx = repo[None]
       
   203     wparents = wkctx.parents()
       
   204     if len(wparents) != 1:
       
   205         raise util.Abort('merge in progress')
       
   206 
       
   207     children = [ctx for ctx in wparents[0].children() if not ctx.obsolete()]
       
   208     displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
       
   209     if not children:
       
   210         ui.warn(_('No non-obsolete children\n'))
       
   211         return 1
       
   212     if len(children) == 1:
       
   213         c = children[0]
       
   214         hg.update(repo, c.rev())
       
   215         displayer.show(c)
       
   216         return 0
       
   217     else:
       
   218         for c in children:
       
   219             displayer.show(c)
       
   220         ui.warn(_('Multiple non-obsolete children, explicitly update to one\n'))
       
   221         return 1
       
   222 
       
   223 
       
   224 @command('^kill',
       
   225     [
       
   226     ('n', 'new', [], _("New changeset that justify this one to be killed"))
       
   227     ],
       
   228     '<revs>')
       
   229 def kill(ui, repo, *revs, **opts):
       
   230     """mark a changeset as obsolete
       
   231 
       
   232     This update the parent directory to a not-killed parent if the current
       
   233     working directory parent are killed.
       
   234 
       
   235     XXX bookmark support
       
   236     XXX handle merge
       
   237     XXX check immutable first
       
   238     """
       
   239     wlock = repo.wlock()
       
   240     try:
       
   241         new = opts['new']
       
   242         targetnodes = set(noderange(repo, revs))
       
   243         if not new:
       
   244             new = [node.nullid]
       
   245         for n in targetnodes:
       
   246             if not repo[n].mutable():
       
   247                 ui.warn(_("Can't kill immutable changeset %s") % repo[n])
       
   248             else:
       
   249                 for ne in new:
       
   250                     repo.addobsolete(ne, n)
       
   251         # update to an unkilled parent
       
   252         wdp = repo['.']
       
   253         newnode = wdp
       
   254         while newnode.obsolete():
       
   255             newnode = newnode.parents()[0]
       
   256         if newnode.node() != wdp.node():
       
   257             commands.update(ui, repo, newnode.rev())
       
   258             ui.status(_('working directory now at %s\n') % newnode)
       
   259 
       
   260     finally:
       
   261         wlock.release()
       
   262 
       
   263 @command('^amend',
       
   264     [('A', 'addremove', None,
       
   265      _('mark new/missing files as added/removed before committing')),
       
   266     ('n', 'note', '',
       
   267      _('use text as commit message for this update')),
       
   268     ('c', 'change', '',
       
   269      _('specifies the changeset to amend'), _('REV')),
       
   270     ('b', 'branch', '',
       
   271      _('specifies a branch for the new.'), _('REV')),
       
   272     ('e', 'edit', False,
       
   273      _('edit commit message.'), _('')),
       
   274     ] + walkopts + commitopts + commitopts2,
       
   275     _('[OPTION]... [FILE]...'))
       
   276 
       
   277 def amend(ui, repo, *pats, **opts):
       
   278     """combine a changeset with updates and replace it with a new one
       
   279 
       
   280     Commits a new changeset incorporating both the changes to the given files
       
   281     and all the changes from the current parent changeset into the repository.
       
   282 
       
   283     See :hg:`commit` for details about committing changes.
       
   284 
       
   285     If you don't specify -m, the parent's message will be reused.
       
   286 
       
   287     If you specify --change, amend additionally considers all changesets between
       
   288     the indicated changeset and the working copy parent as updates to be subsumed.
       
   289     This allows you to commit updates manually first. As a special shorthand you
       
   290     can say `--amend .` instead of '--amend p1(p1())', which subsumes your latest
       
   291     commit as an update of its parent.
       
   292 
       
   293     Behind the scenes, Mercurial first commits the update as a regular child
       
   294     of the current parent. Then it creates a new commit on the parent's parents
       
   295     with the updated contents. Then it changes the working copy parent to this
       
   296     new combined changeset. Finally, the old changeset and its update are hidden
       
   297     from :hg:`log` (unless you use --hidden with log).
       
   298 
       
   299     Returns 0 on success, 1 if nothing changed.
       
   300     """
       
   301 
       
   302     # determine updates to subsume
       
   303     change = opts.get('change')
       
   304     if change == '.':
       
   305         change = 'p1(p1())'
       
   306     old = scmutil.revsingle(repo, change)
       
   307     branch = opts.get('branch')
       
   308     if branch:
       
   309         opts.setdefault('extra', {})['branch'] = branch
       
   310     else:
       
   311         if old.branch() != 'default':
       
   312             opts.setdefault('extra', {})['branch'] = old.branch()
       
   313 
       
   314     lock = repo.lock()
       
   315     try:
       
   316         wlock = repo.wlock()
       
   317         try:
       
   318             if not old.phase():
       
   319                 raise util.Abort(_("can not rewrite immutable changeset %s") % old)
       
   320 
       
   321             # commit current changes as update
       
   322             # code copied from commands.commit to avoid noisy messages
       
   323             ciopts = dict(opts)
       
   324             ciopts.pop('message', None)
       
   325             ciopts.pop('logfile', None)
       
   326             ciopts['message'] = opts.get('note') or ('amends %s' % old.hex())
       
   327             e = cmdutil.commiteditor
       
   328             def commitfunc(ui, repo, message, match, opts):
       
   329                 return repo.commit(message, opts.get('user'), opts.get('date'), match,
       
   330                                    editor=e)
       
   331             cmdutil.commit(ui, repo, commitfunc, pats, ciopts)
       
   332 
       
   333             # find all changesets to be considered updates
       
   334             cl = repo.changelog
       
   335             head = repo['.']
       
   336             updatenodes = set(cl.nodesbetween(roots=[old.node()],
       
   337                                               heads=[head.node()])[0])
       
   338             updatenodes.remove(old.node())
       
   339             if not updatenodes and not (opts.get('message') or opts.get('logfile') or opts.get('edit')):
       
   340                 raise error.Abort(_('no updates found'))
       
   341             updates = [repo[n] for n in updatenodes]
       
   342 
       
   343             # perform amend
       
   344             if opts.get('edit'):
       
   345                 opts['force_editor'] = True
       
   346             newid = rewrite(repo, old, updates, head,
       
   347                             [old.p1().node(), old.p2().node()], opts)
       
   348 
       
   349             # reroute the working copy parent to the new changeset
       
   350             phases.retractboundary(repo, old.phase(), [newid])
       
   351             repo.dirstate.setparents(newid, node.nullid)
       
   352         finally:
       
   353             wlock.release()
       
   354     finally:
       
   355         lock.release()
       
   356 
       
   357 def commitwrapper(orig, ui, repo, *arg, **kwargs):
       
   358     obsoleted = kwargs.get('obsolete', [])
       
   359     if obsoleted:
       
   360         obsoleted = repo.set('%lr', obsoleted)
       
   361     result = orig(ui, repo, *arg, **kwargs)
       
   362     if not result: # commit successed
       
   363         new = repo['-1']
       
   364         for old in obsoleted:
       
   365             repo.addobsolete(new.node(), old.node())
       
   366     return result
       
   367 
       
   368 def graftwrapper(orig, ui, repo, *revs, **kwargs):
       
   369     lock = repo.lock()
       
   370     try:
       
   371         if kwargs.get('old_obsolete'):
       
   372             obsoleted = kwargs.setdefault('obsolete', [])
       
   373             if kwargs['continue']:
       
   374                 obsoleted.extend(repo.opener.read('graftstate').splitlines())
       
   375             else:
       
   376                 obsoleted.extend(revs)
       
   377         return commitwrapper(orig, ui, repo,*revs, **kwargs)
       
   378     finally:
       
   379         lock.release()
       
   380 
       
   381 def extsetup(ui):
       
   382     entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper)
       
   383     entry[1].append(('o', 'obsolete', [], _("this commit obsolet this revision")))
       
   384     entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper)
       
   385     entry[1].append(('o', 'obsolete', [], _("this graft obsolet this revision")))
       
   386     entry[1].append(('O', 'old-obsolete', False, _("graft result obsolete graft source")))