1 # rebase.py - rebasing feature for mercurial
3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
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.
8 '''command to move sets of revisions to a different ancestor
10 This extension lets you rebase changesets in an existing Mercurial
14 http://mercurial.selenic.com/wiki/RebaseExtension
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 _
25 def rebasemerge(repo, rev, first=False):
26 'return the correct ancestor'
27 oldancestor = ancestor.ancestor
29 def newancestor(a, b, pfunc):
30 ancestor.ancestor = oldancestor
32 return repo[rev].parents()[0].rev()
33 return ancestor.ancestor(a, b, pfunc)
36 ancestor.ancestor = newancestor
38 repo.ui.debug(_("first revision, do not change ancestor\n"))
39 stats = merge.update(repo, rev, True, True, False)
42 def rebase(ui, repo, **opts):
43 """move changeset (and descendants) to a different branch
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.
49 If a rebase is interrupted to manually resolve a merge, it can be
50 continued with --continue/-c or aborted with --abort/-a.
52 originalwd = target = None
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)
75 raise error.ParseError('rebase',
76 _('cannot use both abort and continue'))
78 raise error.ParseError(
79 'rebase', _('cannot use collapse with continue or abort'))
81 if srcf or basef or destf:
82 raise error.ParseError('rebase',
83 _('abort and continue do not allow specifying revisions'))
85 (originalwd, target, state, collapsef, keepf,
86 keepbranchesf, external) = restorestatus(repo)
88 abort(repo, originalwd, target, state)
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)
97 originalwd, target, state, external = result
98 else: # Empty state built, nothing to rebase
99 ui.status(_('nothing to rebase\n'))
104 raise error.ParseError(
105 'rebase', _('cannot use both keepbranches and extrafn'))
106 def extrafn(ctx, extra):
107 extra['branch'] = ctx.branch()
110 targetancestors = list(repo.changelog.ancestors(target))
111 targetancestors.append(target)
113 for rev in sorted(state):
115 storestatus(repo, originalwd, target, state, collapsef, keepf,
116 keepbranchesf, external)
117 rebasenode(repo, rev, target, state, skipped, targetancestors,
119 ui.note(_('rebase merging completed\n'))
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)
127 if 'qtip' in repo.tags():
128 updatemq(repo, state, skipped, **opts)
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, "
136 repair.strip(ui, repo, repo[min(state)].node(), "strip")
139 ui.status(_("rebase completed\n"))
140 if os.path.exists(repo.sjoin('undo')):
141 util.unlink(repo.sjoin('undo'))
143 ui.note(_("%d revisions have been skipped\n") % len(skipped))
147 def concludenode(repo, rev, p1, p2, state, collapse, last=False, skipped=None,
149 """Skip commit if collapsing has been required and rev is not the last
150 revision, commit otherwise
152 repo.ui.debug(_(" set parents\n"))
153 if collapse and not last:
154 repo.dirstate.setparents(repo[p1].node())
157 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
162 # Commit, record the old nodeid
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())
173 commitmsg = repo[rev].description()
174 # Commit might fail if unresolved files exist
175 extra = {'rebase_source': repo[rev].hex()}
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())
183 # Invalidate the previous setparents
184 repo.dirstate.invalidate()
187 def rebasenode(repo, rev, target, state, skipped, targetancestors, collapse,
189 'Rebase a single revision'
190 repo.ui.debug(_("rebasing %d:%s\n") % (rev, repo[rev]))
192 p1, p2 = defineparents(repo, rev, target, state, targetancestors)
194 repo.ui.debug(_(" future parents are %d and %d\n") % (repo[p1].rev(),
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)
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)
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'))
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():
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)
228 newrev = concludenode(repo, rev, p1, p2, state, collapse,
232 if newrev is not None:
233 state[rev] = repo[newrev].rev()
236 repo.ui.note(_('no changes, revision %d skipped\n') % rev)
237 repo.ui.debug(_('next revision set to %s\n') % p1)
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()
246 P1n = parents[0].rev()
247 if P1n in targetancestors:
255 if len(parents) == 2 and parents[1].rev() not in targetancestors:
256 P2n = parents[1].rev()
257 # interesting second parent
259 if p1 == target: # P1n in targetancestors or 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)
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'):
278 def updatemq(repo, state, skipped, **opts):
279 'Update rebased mq patches - finalize and then import them'
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))
288 repo.mq.finish(repo, mqrebase.keys())
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])])
299 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
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))
314 repo.ui.debug(_('rebase status stored\n'))
316 def clearstatus(repo):
317 'Remove the status files'
318 if os.path.exists(repo.join("rebasestate")):
319 util.unlink(repo.join("rebasestate"))
321 def restorestatus(repo):
322 'Restore a previously stored status'
328 f = repo.opener("rebasestate")
329 for i, l in enumerate(f.read().splitlines()):
331 originalwd = repo[l].rev()
333 target = repo[l].rev()
335 external = repo[l].rev()
337 collapse = bool(int(l))
341 keepbranches = bool(int(l))
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
348 if err.errno != errno.ENOENT:
350 raise util.Abort(_('no rebase in progress'))
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, "
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())
362 strippoint = min(rebased)
363 repair.strip(repo.ui, repo, repo[strippoint].node(), "strip")
365 repo.ui.status(_('rebase aborted\n'))
367 def buildstate(repo, dest, src, base, collapse):
368 'Define which revisions are going to be rebased and where'
369 targetancestors = set()
372 # Destination defaults to the latest revision in the current branch
373 branch = repo[None].branch()
374 dest = repo[branch].rev()
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()
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()
390 cwd = repo[base].rev()
392 cwd = repo['.'].rev()
395 repo.ui.debug(_('already working on current\n'))
398 targetancestors = set(repo.changelog.ancestors(dest))
399 if cwd in targetancestors:
400 repo.ui.debug(_('already working on the current branch\n'))
403 cwdancestors = set(repo.changelog.ancestors(cwd))
404 cwdancestors.add(cwd)
405 rebasingbranch = cwdancestors - targetancestors
406 source = min(rebasingbranch)
408 repo.ui.debug(_('rebase onto %d starting from %d\n') % (dest, source))
409 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
412 if not targetancestors:
413 targetancestors = set(repo.changelog.ancestors(dest))
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'))
424 state[source] = nullrev
425 return repo['.'].rev(), repo[dest].rev(), state, external
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'):
432 ui.debug(_('--update and --rebase are not compatible, ignoring '
433 'the update flag\n'))
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)
447 orig(ui, repo, *args, **opts)
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"))
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')),] +
469 _('hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] '
470 '[--keepbranches] | [-c] | [-a]')),