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