]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/hgext/rebase.py
added factotum support for python and hg
[plan9front.git] / sys / lib / python / hgext / rebase.py
1 # rebase.py - rebasing feature for mercurial
2 #
3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
7
8 '''command to move sets of revisions to a different ancestor
9
10 This extension lets you rebase changesets in an existing Mercurial
11 repository.
12
13 For more information:
14 http://mercurial.selenic.com/wiki/RebaseExtension
15 '''
16
17 from mercurial import util, repair, merge, cmdutil, commands, error
18 from mercurial import extensions, ancestor, copies, patch
19 from mercurial.commands import templateopts
20 from mercurial.node import nullrev
21 from mercurial.lock import release
22 from mercurial.i18n import _
23 import os, errno
24
25 def rebasemerge(repo, rev, first=False):
26     'return the correct ancestor'
27     oldancestor = ancestor.ancestor
28
29     def newancestor(a, b, pfunc):
30         ancestor.ancestor = oldancestor
31         if b == rev:
32             return repo[rev].parents()[0].rev()
33         return ancestor.ancestor(a, b, pfunc)
34
35     if not first:
36         ancestor.ancestor = newancestor
37     else:
38         repo.ui.debug(_("first revision, do not change ancestor\n"))
39     stats = merge.update(repo, rev, True, True, False)
40     return stats
41
42 def rebase(ui, repo, **opts):
43     """move changeset (and descendants) to a different branch
44
45     Rebase uses repeated merging to graft changesets from one part of
46     history onto another. This can be useful for linearizing local
47     changes relative to a master development tree.
48
49     If a rebase is interrupted to manually resolve a merge, it can be
50     continued with --continue/-c or aborted with --abort/-a.
51     """
52     originalwd = target = None
53     external = nullrev
54     state = {}
55     skipped = set()
56
57     lock = wlock = None
58     try:
59         lock = repo.lock()
60         wlock = repo.wlock()
61
62         # Validate input and define rebasing points
63         destf = opts.get('dest', None)
64         srcf = opts.get('source', None)
65         basef = opts.get('base', None)
66         contf = opts.get('continue')
67         abortf = opts.get('abort')
68         collapsef = opts.get('collapse', False)
69         extrafn = opts.get('extrafn')
70         keepf = opts.get('keep', False)
71         keepbranchesf = opts.get('keepbranches', False)
72
73         if contf or abortf:
74             if contf and abortf:
75                 raise error.ParseError('rebase',
76                                        _('cannot use both abort and continue'))
77             if collapsef:
78                 raise error.ParseError(
79                     'rebase', _('cannot use collapse with continue or abort'))
80
81             if srcf or basef or destf:
82                 raise error.ParseError('rebase',
83                     _('abort and continue do not allow specifying revisions'))
84
85             (originalwd, target, state, collapsef, keepf,
86                                 keepbranchesf, external) = restorestatus(repo)
87             if abortf:
88                 abort(repo, originalwd, target, state)
89                 return
90         else:
91             if srcf and basef:
92                 raise error.ParseError('rebase', _('cannot specify both a '
93                                                    'revision and a base'))
94             cmdutil.bail_if_changed(repo)
95             result = buildstate(repo, destf, srcf, basef, collapsef)
96             if result:
97                 originalwd, target, state, external = result
98             else: # Empty state built, nothing to rebase
99                 ui.status(_('nothing to rebase\n'))
100                 return
101
102         if keepbranchesf:
103             if extrafn:
104                 raise error.ParseError(
105                     'rebase', _('cannot use both keepbranches and extrafn'))
106             def extrafn(ctx, extra):
107                 extra['branch'] = ctx.branch()
108
109         # Rebase
110         targetancestors = list(repo.changelog.ancestors(target))
111         targetancestors.append(target)
112
113         for rev in sorted(state):
114             if state[rev] == -1:
115                 storestatus(repo, originalwd, target, state, collapsef, keepf,
116                                                     keepbranchesf, external)
117                 rebasenode(repo, rev, target, state, skipped, targetancestors,
118                                                        collapsef, extrafn)
119         ui.note(_('rebase merging completed\n'))
120
121         if collapsef:
122             p1, p2 = defineparents(repo, min(state), target,
123                                                         state, targetancestors)
124             concludenode(repo, rev, p1, external, state, collapsef,
125                          last=True, skipped=skipped, extrafn=extrafn)
126
127         if 'qtip' in repo.tags():
128             updatemq(repo, state, skipped, **opts)
129
130         if not keepf:
131             # Remove no more useful revisions
132             if set(repo.changelog.descendants(min(state))) - set(state):
133                 ui.warn(_("warning: new changesets detected on source branch, "
134                                                         "not stripping\n"))
135             else:
136                 repair.strip(ui, repo, repo[min(state)].node(), "strip")
137
138         clearstatus(repo)
139         ui.status(_("rebase completed\n"))
140         if os.path.exists(repo.sjoin('undo')):
141             util.unlink(repo.sjoin('undo'))
142         if skipped:
143             ui.note(_("%d revisions have been skipped\n") % len(skipped))
144     finally:
145         release(lock, wlock)
146
147 def concludenode(repo, rev, p1, p2, state, collapse, last=False, skipped=None,
148                  extrafn=None):
149     """Skip commit if collapsing has been required and rev is not the last
150     revision, commit otherwise
151     """
152     repo.ui.debug(_(" set parents\n"))
153     if collapse and not last:
154         repo.dirstate.setparents(repo[p1].node())
155         return None
156
157     repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
158
159     if skipped is None:
160         skipped = set()
161
162     # Commit, record the old nodeid
163     newrev = nullrev
164     try:
165         if last:
166             # we don't translate commit messages
167             commitmsg = 'Collapsed revision'
168             for rebased in state:
169                 if rebased not in skipped:
170                     commitmsg += '\n* %s' % repo[rebased].description()
171             commitmsg = repo.ui.edit(commitmsg, repo.ui.username())
172         else:
173             commitmsg = repo[rev].description()
174         # Commit might fail if unresolved files exist
175         extra = {'rebase_source': repo[rev].hex()}
176         if extrafn:
177             extrafn(repo[rev], extra)
178         newrev = repo.commit(text=commitmsg, user=repo[rev].user(),
179                              date=repo[rev].date(), extra=extra)
180         repo.dirstate.setbranch(repo[newrev].branch())
181         return newrev
182     except util.Abort:
183         # Invalidate the previous setparents
184         repo.dirstate.invalidate()
185         raise
186
187 def rebasenode(repo, rev, target, state, skipped, targetancestors, collapse,
188                extrafn):
189     'Rebase a single revision'
190     repo.ui.debug(_("rebasing %d:%s\n") % (rev, repo[rev]))
191
192     p1, p2 = defineparents(repo, rev, target, state, targetancestors)
193
194     repo.ui.debug(_(" future parents are %d and %d\n") % (repo[p1].rev(),
195                                                             repo[p2].rev()))
196
197     # Merge phase
198     if len(repo.parents()) != 2:
199         # Update to target and merge it with local
200         if repo['.'].rev() != repo[p1].rev():
201             repo.ui.debug(_(" update to %d:%s\n") % (repo[p1].rev(), repo[p1]))
202             merge.update(repo, p1, False, True, False)
203         else:
204             repo.ui.debug(_(" already in target\n"))
205         repo.dirstate.write()
206         repo.ui.debug(_(" merge against %d:%s\n") % (repo[rev].rev(), repo[rev]))
207         first = repo[rev].rev() == repo[min(state)].rev()
208         stats = rebasemerge(repo, rev, first)
209
210         if stats[3] > 0:
211             raise util.Abort(_('fix unresolved conflicts with hg resolve then '
212                                                 'run hg rebase --continue'))
213     else: # we have an interrupted rebase
214         repo.ui.debug(_('resuming interrupted rebase\n'))
215
216     # Keep track of renamed files in the revision that is going to be rebased
217     # Here we simulate the copies and renames in the source changeset
218     cop, diver = copies.copies(repo, repo[rev], repo[target], repo[p2], True)
219     m1 = repo[rev].manifest()
220     m2 = repo[target].manifest()
221     for k, v in cop.iteritems():
222         if k in m1:
223             if v in m1 or v in m2:
224                 repo.dirstate.copy(v, k)
225                 if v in m2 and v not in m1:
226                     repo.dirstate.remove(v)
227
228     newrev = concludenode(repo, rev, p1, p2, state, collapse,
229                           extrafn=extrafn)
230
231     # Update the state
232     if newrev is not None:
233         state[rev] = repo[newrev].rev()
234     else:
235         if not collapse:
236             repo.ui.note(_('no changes, revision %d skipped\n') % rev)
237             repo.ui.debug(_('next revision set to %s\n') % p1)
238             skipped.add(rev)
239         state[rev] = p1
240
241 def defineparents(repo, rev, target, state, targetancestors):
242     'Return the new parent relationship of the revision that will be rebased'
243     parents = repo[rev].parents()
244     p1 = p2 = nullrev
245
246     P1n = parents[0].rev()
247     if P1n in targetancestors:
248         p1 = target
249     elif P1n in state:
250         p1 = state[P1n]
251     else: # P1n external
252         p1 = target
253         p2 = P1n
254
255     if len(parents) == 2 and parents[1].rev() not in targetancestors:
256         P2n = parents[1].rev()
257         # interesting second parent
258         if P2n in state:
259             if p1 == target: # P1n in targetancestors or external
260                 p1 = state[P2n]
261             else:
262                 p2 = state[P2n]
263         else: # P2n external
264             if p2 != nullrev: # P1n external too => rev is a merged revision
265                 raise util.Abort(_('cannot use revision %d as base, result '
266                         'would have 3 parents') % rev)
267             p2 = P2n
268     return p1, p2
269
270 def isagitpatch(repo, patchname):
271     'Return true if the given patch is in git format'
272     mqpatch = os.path.join(repo.mq.path, patchname)
273     for line in patch.linereader(file(mqpatch, 'rb')):
274         if line.startswith('diff --git'):
275             return True
276     return False
277
278 def updatemq(repo, state, skipped, **opts):
279     'Update rebased mq patches - finalize and then import them'
280     mqrebase = {}
281     for p in repo.mq.applied:
282         if repo[p.rev].rev() in state:
283             repo.ui.debug(_('revision %d is an mq patch (%s), finalize it.\n') %
284                                         (repo[p.rev].rev(), p.name))
285             mqrebase[repo[p.rev].rev()] = (p.name, isagitpatch(repo, p.name))
286
287     if mqrebase:
288         repo.mq.finish(repo, mqrebase.keys())
289
290         # We must start import from the newest revision
291         for rev in sorted(mqrebase, reverse=True):
292             if rev not in skipped:
293                 repo.ui.debug(_('import mq patch %d (%s)\n')
294                               % (state[rev], mqrebase[rev][0]))
295                 repo.mq.qimport(repo, (), patchname=mqrebase[rev][0],
296                             git=mqrebase[rev][1],rev=[str(state[rev])])
297         repo.mq.save_dirty()
298
299 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
300                                                                 external):
301     'Store the current status to allow recovery'
302     f = repo.opener("rebasestate", "w")
303     f.write(repo[originalwd].hex() + '\n')
304     f.write(repo[target].hex() + '\n')
305     f.write(repo[external].hex() + '\n')
306     f.write('%d\n' % int(collapse))
307     f.write('%d\n' % int(keep))
308     f.write('%d\n' % int(keepbranches))
309     for d, v in state.iteritems():
310         oldrev = repo[d].hex()
311         newrev = repo[v].hex()
312         f.write("%s:%s\n" % (oldrev, newrev))
313     f.close()
314     repo.ui.debug(_('rebase status stored\n'))
315
316 def clearstatus(repo):
317     'Remove the status files'
318     if os.path.exists(repo.join("rebasestate")):
319         util.unlink(repo.join("rebasestate"))
320
321 def restorestatus(repo):
322     'Restore a previously stored status'
323     try:
324         target = None
325         collapse = False
326         external = nullrev
327         state = {}
328         f = repo.opener("rebasestate")
329         for i, l in enumerate(f.read().splitlines()):
330             if i == 0:
331                 originalwd = repo[l].rev()
332             elif i == 1:
333                 target = repo[l].rev()
334             elif i == 2:
335                 external = repo[l].rev()
336             elif i == 3:
337                 collapse = bool(int(l))
338             elif i == 4:
339                 keep = bool(int(l))
340             elif i == 5:
341                 keepbranches = bool(int(l))
342             else:
343                 oldrev, newrev = l.split(':')
344                 state[repo[oldrev].rev()] = repo[newrev].rev()
345         repo.ui.debug(_('rebase status resumed\n'))
346         return originalwd, target, state, collapse, keep, keepbranches, external
347     except IOError, err:
348         if err.errno != errno.ENOENT:
349             raise
350         raise util.Abort(_('no rebase in progress'))
351
352 def abort(repo, originalwd, target, state):
353     'Restore the repository to its original state'
354     if set(repo.changelog.descendants(target)) - set(state.values()):
355         repo.ui.warn(_("warning: new changesets detected on target branch, "
356                                                     "not stripping\n"))
357     else:
358         # Strip from the first rebased revision
359         merge.update(repo, repo[originalwd].rev(), False, True, False)
360         rebased = filter(lambda x: x > -1, state.values())
361         if rebased:
362             strippoint = min(rebased)
363             repair.strip(repo.ui, repo, repo[strippoint].node(), "strip")
364         clearstatus(repo)
365         repo.ui.status(_('rebase aborted\n'))
366
367 def buildstate(repo, dest, src, base, collapse):
368     'Define which revisions are going to be rebased and where'
369     targetancestors = set()
370
371     if not dest:
372         # Destination defaults to the latest revision in the current branch
373         branch = repo[None].branch()
374         dest = repo[branch].rev()
375     else:
376         if 'qtip' in repo.tags() and (repo[dest].hex() in
377                                 [s.rev for s in repo.mq.applied]):
378             raise util.Abort(_('cannot rebase onto an applied mq patch'))
379         dest = repo[dest].rev()
380
381     if src:
382         commonbase = repo[src].ancestor(repo[dest])
383         if commonbase == repo[src]:
384             raise util.Abort(_('cannot rebase an ancestor'))
385         if commonbase == repo[dest]:
386             raise util.Abort(_('cannot rebase a descendant'))
387         source = repo[src].rev()
388     else:
389         if base:
390             cwd = repo[base].rev()
391         else:
392             cwd = repo['.'].rev()
393
394         if cwd == dest:
395             repo.ui.debug(_('already working on current\n'))
396             return None
397
398         targetancestors = set(repo.changelog.ancestors(dest))
399         if cwd in targetancestors:
400             repo.ui.debug(_('already working on the current branch\n'))
401             return None
402
403         cwdancestors = set(repo.changelog.ancestors(cwd))
404         cwdancestors.add(cwd)
405         rebasingbranch = cwdancestors - targetancestors
406         source = min(rebasingbranch)
407
408     repo.ui.debug(_('rebase onto %d starting from %d\n') % (dest, source))
409     state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
410     external = nullrev
411     if collapse:
412         if not targetancestors:
413             targetancestors = set(repo.changelog.ancestors(dest))
414         for rev in state:
415             # Check externals and fail if there are more than one
416             for p in repo[rev].parents():
417                 if (p.rev() not in state and p.rev() != source
418                             and p.rev() not in targetancestors):
419                     if external != nullrev:
420                         raise util.Abort(_('unable to collapse, there is more '
421                                 'than one external parent'))
422                     external = p.rev()
423
424     state[source] = nullrev
425     return repo['.'].rev(), repo[dest].rev(), state, external
426
427 def pullrebase(orig, ui, repo, *args, **opts):
428     'Call rebase after pull if the latter has been invoked with --rebase'
429     if opts.get('rebase'):
430         if opts.get('update'):
431             del opts['update']
432             ui.debug(_('--update and --rebase are not compatible, ignoring '
433                                         'the update flag\n'))
434
435         cmdutil.bail_if_changed(repo)
436         revsprepull = len(repo)
437         orig(ui, repo, *args, **opts)
438         revspostpull = len(repo)
439         if revspostpull > revsprepull:
440             rebase(ui, repo, **opts)
441             branch = repo[None].branch()
442             dest = repo[branch].rev()
443             if dest != repo['.'].rev():
444                 # there was nothing to rebase we force an update
445                 merge.update(repo, dest, False, False, False)
446     else:
447         orig(ui, repo, *args, **opts)
448
449 def uisetup(ui):
450     'Replace pull with a decorator to provide --rebase option'
451     entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
452     entry[1].append(('', 'rebase', None,
453                      _("rebase working directory to branch head"))
454 )
455
456 cmdtable = {
457 "rebase":
458         (rebase,
459         [
460         ('s', 'source', '', _('rebase from a given revision')),
461         ('b', 'base', '', _('rebase from the base of a given revision')),
462         ('d', 'dest', '', _('rebase onto a given revision')),
463         ('', 'collapse', False, _('collapse the rebased revisions')),
464         ('', 'keep', False, _('keep original revisions')),
465         ('', 'keepbranches', False, _('keep original branches')),
466         ('c', 'continue', False, _('continue an interrupted rebase')),
467         ('a', 'abort', False, _('abort an interrupted rebase')),] +
468          templateopts,
469         _('hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] '
470                             '[--keepbranches] | [-c] | [-a]')),
471 }