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