Mercurial > hg
comparison hgext/rebase.py @ 6906:808f03f61ebe
Add rebase extension
author | Stefano Tortarolo <stefano.tortarolo@gmail.com> |
---|---|
date | Mon, 18 Aug 2008 21:16:31 +0200 |
parents | |
children | ebf1462f2145 |
comparison
equal
deleted
inserted
replaced
6905:248e54a9456e | 6906:808f03f61ebe |
---|---|
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 | |
6 # of the GNU General Public License, incorporated herein by reference. | |
7 | |
8 ''' Rebasing feature | |
9 | |
10 This extension lets you rebase changesets in an existing Mercurial repository. | |
11 | |
12 For more information: | |
13 http://www.selenic.com/mercurial/wiki/index.cgi/RebaseProject | |
14 ''' | |
15 | |
16 from mercurial import util, repair, merge, cmdutil, dispatch, commands | |
17 from mercurial.commands import templateopts | |
18 from mercurial.node import nullrev | |
19 from mercurial.i18n import _ | |
20 import os, errno | |
21 | |
22 def rebase(ui, repo, **opts): | |
23 """move changeset (and descendants) to a different branch | |
24 | |
25 Rebase uses repeated merging to graft changesets from one part of history | |
26 onto another. This can be useful for linearizing local changes relative to | |
27 a master development tree. | |
28 | |
29 If a rebase is interrupted to manually resolve a merge, it can be continued | |
30 with --continue or aborted with --abort. | |
31 """ | |
32 originalwd = target = source = None | |
33 external = nullrev | |
34 state = skipped = {} | |
35 | |
36 lock = wlock = None | |
37 try: | |
38 lock = repo.lock() | |
39 wlock = repo.wlock() | |
40 | |
41 # Validate input and define rebasing points | |
42 destf = opts.get('dest', None) | |
43 srcf = opts.get('source', None) | |
44 basef = opts.get('base', None) | |
45 contf = opts.get('continue') | |
46 abortf = opts.get('abort') | |
47 collapsef = opts.get('collapse', False) | |
48 if contf or abortf: | |
49 if contf and abortf: | |
50 raise dispatch.ParseError('rebase', | |
51 _('cannot use both abort and continue')) | |
52 if collapsef: | |
53 raise dispatch.ParseError('rebase', | |
54 _('cannot use collapse with continue or abort')) | |
55 | |
56 if (srcf or basef or destf): | |
57 raise dispatch.ParseError('rebase', | |
58 _('abort and continue do not allow specifying revisions')) | |
59 | |
60 originalwd, target, state, collapsef, external = restorestatus(repo) | |
61 if abortf: | |
62 abort(repo, originalwd, target, state) | |
63 return | |
64 else: | |
65 if srcf and basef: | |
66 raise dispatch.ParseError('rebase', _('cannot specify both a ' | |
67 'revision and a base')) | |
68 cmdutil.bail_if_changed(repo) | |
69 result = buildstate(repo, destf, srcf, basef, collapsef) | |
70 if result: | |
71 originalwd, target, state, external = result | |
72 else: # Empty state built, nothing to rebase | |
73 repo.ui.status(_('nothing to rebase\n')) | |
74 return | |
75 | |
76 # Rebase | |
77 targetancestors = list(repo.changelog.ancestors(target)) | |
78 targetancestors.append(target) | |
79 | |
80 for rev in util.sort(state): | |
81 if state[rev] == -1: | |
82 storestatus(repo, originalwd, target, state, collapsef, | |
83 external) | |
84 rebasenode(repo, rev, target, state, skipped, targetancestors, | |
85 collapsef) | |
86 ui.note(_('rebase merging completed\n')) | |
87 | |
88 if collapsef: | |
89 p1, p2 = defineparents(repo, min(state), target, | |
90 state, targetancestors) | |
91 concludenode(repo, rev, p1, external, state, collapsef, | |
92 last=True, skipped=skipped) | |
93 | |
94 if 'qtip' in repo.tags(): | |
95 updatemq(repo, state, skipped, **opts) | |
96 | |
97 if not opts.get('keep'): | |
98 # Remove no more useful revisions | |
99 if (util.set(repo.changelog.descendants(min(state))) | |
100 - util.set(state.keys())): | |
101 ui.warn(_("warning: new changesets detected on source branch, " | |
102 "not stripping\n")) | |
103 else: | |
104 repair.strip(repo.ui, repo, repo[min(state)].node(), "strip") | |
105 | |
106 clearstatus(repo) | |
107 ui.status(_("rebase completed\n")) | |
108 if skipped: | |
109 ui.note(_("%d revisions have been skipped\n") % len(skipped)) | |
110 finally: | |
111 del lock, wlock | |
112 | |
113 def concludenode(repo, rev, p1, p2, state, collapse, last=False, skipped={}): | |
114 """Skip commit if collapsing has been required and rev is not the last | |
115 revision, commit otherwise | |
116 """ | |
117 repo.dirstate.setparents(repo[p1].node(), repo[p2].node()) | |
118 | |
119 if collapse and not last: | |
120 return None | |
121 | |
122 # Commit, record the old nodeid | |
123 m, a, r = repo.status()[:3] | |
124 newrev = nullrev | |
125 try: | |
126 if last: | |
127 commitmsg = 'Collapsed revision' | |
128 for rebased in state: | |
129 if rebased not in skipped: | |
130 commitmsg += '\n* %s' % repo[rebased].description() | |
131 commitmsg = repo.ui.edit(commitmsg, repo.ui.username()) | |
132 else: | |
133 commitmsg = repo[rev].description() | |
134 # Commit might fail if unresolved files exist | |
135 newrev = repo.commit(m+a+r, | |
136 text=commitmsg, | |
137 user=repo[rev].user(), | |
138 date=repo[rev].date(), | |
139 extra={'rebase_source': repo[rev].hex()}) | |
140 return newrev | |
141 except util.Abort: | |
142 # Invalidate the previous setparents | |
143 repo.dirstate.invalidate() | |
144 raise | |
145 | |
146 def rebasenode(repo, rev, target, state, skipped, targetancestors, collapse): | |
147 'Rebase a single revision' | |
148 repo.ui.debug(_("rebasing %d:%s\n") % (rev, repo[rev].node())) | |
149 | |
150 p1, p2 = defineparents(repo, rev, target, state, targetancestors) | |
151 | |
152 # Merge phase | |
153 if len(repo.parents()) != 2: | |
154 # Update to target and merge it with local | |
155 merge.update(repo, p1, False, True, False) | |
156 repo.dirstate.write() | |
157 stats = merge.update(repo, rev, True, False, False) | |
158 | |
159 if stats[3] > 0: | |
160 raise util.Abort(_('fix unresolved conflicts with hg resolve then ' | |
161 'run hg rebase --continue')) | |
162 else: # we have an interrupted rebase | |
163 repo.ui.debug(_('resuming interrupted rebase\n')) | |
164 | |
165 | |
166 newrev = concludenode(repo, rev, p1, p2, state, collapse) | |
167 | |
168 # Update the state | |
169 if newrev is not None: | |
170 state[rev] = repo[newrev].rev() | |
171 else: | |
172 if not collapse: | |
173 repo.ui.note('no changes, revision %d skipped\n' % rev) | |
174 repo.ui.debug('next revision set to %s\n' % p1) | |
175 skipped[rev] = True | |
176 state[rev] = p1 | |
177 | |
178 def defineparents(repo, rev, target, state, targetancestors): | |
179 'Return the new parent relationship of the revision that will be rebased' | |
180 parents = repo[rev].parents() | |
181 p1 = p2 = nullrev | |
182 | |
183 P1n = parents[0].rev() | |
184 if P1n in targetancestors: | |
185 p1 = target | |
186 elif P1n in state: | |
187 p1 = state[P1n] | |
188 else: # P1n external | |
189 p1 = target | |
190 p2 = P1n | |
191 | |
192 if len(parents) == 2 and parents[1].rev() not in targetancestors: | |
193 P2n = parents[1].rev() | |
194 # interesting second parent | |
195 if P2n in state: | |
196 if p1 == target: # P1n in targetancestors or external | |
197 p1 = state[P2n] | |
198 else: | |
199 p2 = state[P2n] | |
200 else: # P2n external | |
201 if p2 != nullrev: # P1n external too => rev is a merged revision | |
202 raise util.Abort(_('cannot use revision %d as base, result ' | |
203 'would have 3 parents') % rev) | |
204 p2 = P2n | |
205 return p1, p2 | |
206 | |
207 def updatemq(repo, state, skipped, **opts): | |
208 'Update rebased mq patches - finalize and then import them' | |
209 mqrebase = {} | |
210 for p in repo.mq.applied: | |
211 if repo[p.rev].rev() in state: | |
212 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' % | |
213 (repo[p.rev].rev(), p.name)) | |
214 mqrebase[repo[p.rev].rev()] = p.name | |
215 | |
216 if mqrebase: | |
217 repo.mq.finish(repo, mqrebase.keys()) | |
218 | |
219 # We must start import from the newest revision | |
220 mq = mqrebase.keys() | |
221 mq.sort() | |
222 mq.reverse() | |
223 for rev in mq: | |
224 if rev not in skipped: | |
225 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], | |
226 mqrebase[rev])) | |
227 repo.mq.qimport(repo, (), patchname=mqrebase[rev], | |
228 git=opts.get('git', False),rev=[str(state[rev])]) | |
229 repo.mq.save_dirty() | |
230 | |
231 def storestatus(repo, originalwd, target, state, collapse, external): | |
232 'Store the current status to allow recovery' | |
233 f = repo.opener("rebasestate", "w") | |
234 f.write(repo[originalwd].hex() + '\n') | |
235 f.write(repo[target].hex() + '\n') | |
236 f.write(repo[external].hex() + '\n') | |
237 f.write('%d\n' % int(collapse)) | |
238 for d, v in state.items(): | |
239 oldrev = repo[d].hex() | |
240 newrev = repo[v].hex() | |
241 f.write("%s:%s\n" % (oldrev, newrev)) | |
242 f.close() | |
243 repo.ui.debug(_('rebase status stored\n')) | |
244 | |
245 def clearstatus(repo): | |
246 'Remove the status files' | |
247 if os.path.exists(repo.join("rebasestate")): | |
248 util.unlink(repo.join("rebasestate")) | |
249 | |
250 def restorestatus(repo): | |
251 'Restore a previously stored status' | |
252 try: | |
253 target = None | |
254 collapse = False | |
255 external = nullrev | |
256 state = {} | |
257 f = repo.opener("rebasestate") | |
258 for i, l in enumerate(f.read().splitlines()): | |
259 if i == 0: | |
260 originalwd = repo[l].rev() | |
261 elif i == 1: | |
262 target = repo[l].rev() | |
263 elif i == 2: | |
264 external = repo[l].rev() | |
265 elif i == 3: | |
266 collapse = bool(int(l)) | |
267 else: | |
268 oldrev, newrev = l.split(':') | |
269 state[repo[oldrev].rev()] = repo[newrev].rev() | |
270 repo.ui.debug(_('rebase status resumed\n')) | |
271 return originalwd, target, state, collapse, external | |
272 except IOError, err: | |
273 if err.errno != errno.ENOENT: | |
274 raise | |
275 raise util.Abort(_('no rebase in progress')) | |
276 | |
277 def abort(repo, originalwd, target, state): | |
278 'Restore the repository to its original state' | |
279 if util.set(repo.changelog.descendants(target)) - util.set(state.values()): | |
280 repo.ui.warn(_("warning: new changesets detected on target branch, " | |
281 "not stripping\n")) | |
282 else: | |
283 # Strip from the first rebased revision | |
284 merge.update(repo, repo[originalwd].rev(), False, True, False) | |
285 rebased = filter(lambda x: x > -1, state.values()) | |
286 if rebased: | |
287 strippoint = min(rebased) | |
288 repair.strip(repo.ui, repo, repo[strippoint].node(), "strip") | |
289 clearstatus(repo) | |
290 repo.ui.status(_('rebase aborted\n')) | |
291 | |
292 def buildstate(repo, dest, src, base, collapse): | |
293 'Define which revisions are going to be rebased and where' | |
294 state = {} | |
295 targetancestors = util.set() | |
296 | |
297 if not dest: | |
298 # Destination defaults to the latest revision in the current branch | |
299 branch = repo[None].branch() | |
300 dest = repo[branch].rev() | |
301 else: | |
302 if 'qtip' in repo.tags() and (repo[dest].hex() in | |
303 [s.rev for s in repo.mq.applied]): | |
304 raise util.Abort(_('cannot rebase onto an applied mq patch')) | |
305 dest = repo[dest].rev() | |
306 | |
307 if src: | |
308 commonbase = repo[src].ancestor(repo[dest]) | |
309 if commonbase == repo[src]: | |
310 raise util.Abort(_('cannot rebase an ancestor')) | |
311 if commonbase == repo[dest]: | |
312 raise util.Abort(_('cannot rebase a descendant')) | |
313 source = repo[src].rev() | |
314 else: | |
315 if base: | |
316 cwd = repo[base].rev() | |
317 else: | |
318 cwd = repo['.'].rev() | |
319 | |
320 if cwd == dest: | |
321 repo.ui.debug(_('already working on current\n')) | |
322 return None | |
323 | |
324 targetancestors = util.set(repo.changelog.ancestors(dest)) | |
325 if cwd in targetancestors: | |
326 repo.ui.debug(_('already working on the current branch\n')) | |
327 return None | |
328 | |
329 cwdancestors = util.set(repo.changelog.ancestors(cwd)) | |
330 cwdancestors.add(cwd) | |
331 rebasingbranch = cwdancestors - targetancestors | |
332 source = min(rebasingbranch) | |
333 | |
334 repo.ui.debug(_('rebase onto %d starting from %d\n') % (dest, source)) | |
335 state = dict.fromkeys(repo.changelog.descendants(source), nullrev) | |
336 external = nullrev | |
337 if collapse: | |
338 if not targetancestors: | |
339 targetancestors = util.set(repo.changelog.ancestors(dest)) | |
340 for rev in state: | |
341 # Check externals and fail if there are more than one | |
342 for p in repo[rev].parents(): | |
343 if (p.rev() not in state and p.rev() != source | |
344 and p.rev() not in targetancestors): | |
345 if external != nullrev: | |
346 raise util.Abort(_('unable to collapse, there is more ' | |
347 'than one external parent')) | |
348 external = p.rev() | |
349 | |
350 state[source] = nullrev | |
351 return repo['.'].rev(), repo[dest].rev(), state, external | |
352 | |
353 def pulldelegate(pullfunction, repo, *args, **opts): | |
354 'Call rebase after pull if the latter has been invoked with --rebase' | |
355 if opts.get('rebase'): | |
356 if opts.get('update'): | |
357 raise util.Abort(_('--update and --rebase are not compatible')) | |
358 | |
359 cmdutil.bail_if_changed(repo) | |
360 revsprepull = len(repo) | |
361 pullfunction(repo.ui, repo, *args, **opts) | |
362 revspostpull = len(repo) | |
363 if revspostpull > revsprepull: | |
364 rebase(repo.ui, repo, **opts) | |
365 else: | |
366 pullfunction(repo.ui, repo, *args, **opts) | |
367 | |
368 def uisetup(ui): | |
369 'Replace pull with a decorator to provide --rebase option' | |
370 # cribbed from color.py | |
371 aliases, entry = cmdutil.findcmd(ui, 'pull', commands.table) | |
372 for candidatekey, candidateentry in commands.table.iteritems(): | |
373 if candidateentry is entry: | |
374 cmdkey, cmdentry = candidatekey, entry | |
375 break | |
376 | |
377 decorator = lambda ui, repo, *args, **opts: \ | |
378 pulldelegate(cmdentry[0], repo, *args, **opts) | |
379 # make sure 'hg help cmd' still works | |
380 decorator.__doc__ = cmdentry[0].__doc__ | |
381 decoratorentry = (decorator,) + cmdentry[1:] | |
382 rebaseopt = ('', 'rebase', None, | |
383 _("rebase working directory to branch head")) | |
384 decoratorentry[1].append(rebaseopt) | |
385 commands.table[cmdkey] = decoratorentry | |
386 | |
387 cmdtable = { | |
388 "rebase": | |
389 (rebase, | |
390 [ | |
391 ('', 'keep', False, _('keep original revisions')), | |
392 ('s', 'source', '', _('rebase from a given revision')), | |
393 ('b', 'base', '', _('rebase from the base of a given revision')), | |
394 ('d', 'dest', '', _('rebase onto a given revision')), | |
395 ('', 'collapse', False, _('collapse the rebased revisions')), | |
396 ('c', 'continue', False, _('continue an interrupted rebase')), | |
397 ('a', 'abort', False, _('abort an interrupted rebase')),] + | |
398 templateopts, | |
399 _('hg rebase [-s rev | -b rev] [-d rev] [--collapse] | [-c] | [-a] | ' | |
400 '[--keep]')), | |
401 } |