##// END OF EJS Templates
rebase: allow for rebasing descendants onto ancestors on different named branches...
Stefano Tortarolo -
r13733:4e2690a7 default
parent child Browse files
Show More
@@ -0,0 +1,171 b''
1 $ cat >> $HGRCPATH <<EOF
2 > [extensions]
3 > graphlog=
4 > rebase=
5 >
6 > [alias]
7 > tglog = log -G --template "{rev}: '{desc}' {branches}\n"
8 > EOF
9
10
11 $ hg init a
12 $ cd a
13
14 $ echo A > A
15 $ hg ci -Am A
16 adding A
17
18 $ echo B > B
19 $ hg ci -Am B
20 adding B
21
22 $ hg up -q -C 0
23
24 $ echo C > C
25 $ hg ci -Am C
26 adding C
27 created new head
28
29 $ hg up -q -C 0
30
31 $ echo D > D
32 $ hg ci -Am D
33 adding D
34 created new head
35
36 $ hg merge -r 2
37 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
38 (branch merge, don't forget to commit)
39
40 $ hg ci -m E
41
42 $ hg up -q -C 3
43
44 $ echo F > F
45 $ hg ci -Am F
46 adding F
47 created new head
48
49 $ cd ..
50
51
52 Rebasing descendant onto ancestor across different named branches
53
54 $ hg clone -q -u . a a1
55
56 $ cd a1
57
58 $ hg branch dev
59 marked working directory as branch dev
60
61 $ echo x > x
62
63 $ hg add x
64
65 $ hg ci -m 'extra named branch'
66
67 $ hg tglog
68 @ 6: 'extra named branch' dev
69 |
70 o 5: 'F'
71 |
72 | o 4: 'E'
73 |/|
74 o | 3: 'D'
75 | |
76 | o 2: 'C'
77 |/
78 | o 1: 'B'
79 |/
80 o 0: 'A'
81
82 $ hg rebase -s 6 -d 5
83 saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
84
85 $ hg tglog
86 @ 6: 'extra named branch'
87 |
88 o 5: 'F'
89 |
90 | o 4: 'E'
91 |/|
92 o | 3: 'D'
93 | |
94 | o 2: 'C'
95 |/
96 | o 1: 'B'
97 |/
98 o 0: 'A'
99
100 $ cd ..
101
102 Rebasing descendant onto ancestor across the same named branches
103
104 $ hg clone -q -u . a a2
105
106 $ cd a2
107
108 $ echo x > x
109
110 $ hg add x
111
112 $ hg ci -m 'G'
113
114 $ hg tglog
115 @ 6: 'G'
116 |
117 o 5: 'F'
118 |
119 | o 4: 'E'
120 |/|
121 o | 3: 'D'
122 | |
123 | o 2: 'C'
124 |/
125 | o 1: 'B'
126 |/
127 o 0: 'A'
128
129 $ hg rebase -s 6 -d 5
130 abort: source is descendant of destination
131 [255]
132
133 $ cd ..
134
135 Rebasing ancestor onto descendant across different named branches
136
137 $ hg clone -q -u . a a3
138
139 $ cd a3
140
141 $ hg branch dev
142 marked working directory as branch dev
143
144 $ echo x > x
145
146 $ hg add x
147
148 $ hg ci -m 'extra named branch'
149
150 $ hg tglog
151 @ 6: 'extra named branch' dev
152 |
153 o 5: 'F'
154 |
155 | o 4: 'E'
156 |/|
157 o | 3: 'D'
158 | |
159 | o 2: 'C'
160 |/
161 | o 1: 'B'
162 |/
163 o 0: 'A'
164
165 $ hg rebase -s 5 -d 6
166 abort: source is ancestor of destination
167 [255]
168
169 $ cd ..
170
171
@@ -1,588 +1,589 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 hg, util, repair, merge, cmdutil, commands
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 (the source) onto another (the destination). This can be
32 32 useful for linearizing *local* changes relative to a master
33 33 development tree.
34 34
35 35 You should not rebase changesets that have already been shared
36 36 with others. Doing so will force everybody else to perform the
37 37 same rebase or they will end up with duplicated changesets after
38 38 pulling in your rebased changesets.
39 39
40 40 If you don't specify a destination changeset (``-d/--dest``),
41 41 rebase uses the tipmost head of the current named branch as the
42 42 destination. (The destination changeset is not modified by
43 43 rebasing, but new changesets are added as its descendants.)
44 44
45 45 You can specify which changesets to rebase in two ways: as a
46 46 "source" changeset or as a "base" changeset. Both are shorthand
47 47 for a topologically related set of changesets (the "source
48 48 branch"). If you specify source (``-s/--source``), rebase will
49 49 rebase that changeset and all of its descendants onto dest. If you
50 50 specify base (``-b/--base``), rebase will select ancestors of base
51 51 back to but not including the common ancestor with dest. Thus,
52 52 ``-b`` is less precise but more convenient than ``-s``: you can
53 53 specify any changeset in the source branch, and rebase will select
54 54 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
55 55 uses the parent of the working directory as the base.
56 56
57 57 By default, rebase recreates the changesets in the source branch
58 58 as descendants of dest and then destroys the originals. Use
59 59 ``--keep`` to preserve the original source changesets. Some
60 60 changesets in the source branch (e.g. merges from the destination
61 61 branch) may be dropped if they no longer contribute any change.
62 62
63 63 One result of the rules for selecting the destination changeset
64 64 and source branch is that, unlike ``merge``, rebase will do
65 65 nothing if you are at the latest (tipmost) head of a named branch
66 66 with two heads. You need to explicitly specify source and/or
67 67 destination (or ``update`` to the other head, if it's the head of
68 68 the intended source branch).
69 69
70 70 If a rebase is interrupted to manually resolve a merge, it can be
71 71 continued with --continue/-c or aborted with --abort/-a.
72 72
73 73 Returns 0 on success, 1 if nothing to rebase.
74 74 """
75 75 originalwd = target = None
76 76 external = nullrev
77 77 state = {}
78 78 skipped = set()
79 79 targetancestors = set()
80 80
81 81 lock = wlock = None
82 82 try:
83 83 lock = repo.lock()
84 84 wlock = repo.wlock()
85 85
86 86 # Validate input and define rebasing points
87 87 destf = opts.get('dest', None)
88 88 srcf = opts.get('source', None)
89 89 basef = opts.get('base', None)
90 90 contf = opts.get('continue')
91 91 abortf = opts.get('abort')
92 92 collapsef = opts.get('collapse', False)
93 93 collapsemsg = cmdutil.logmessage(opts)
94 94 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
95 95 keepf = opts.get('keep', False)
96 96 keepbranchesf = opts.get('keepbranches', False)
97 97 detachf = opts.get('detach', False)
98 98 # keepopen is not meant for use on the command line, but by
99 99 # other extensions
100 100 keepopen = opts.get('keepopen', False)
101 101
102 102 if collapsemsg and not collapsef:
103 103 raise util.Abort(
104 104 _('message can only be specified with collapse'))
105 105
106 106 if contf or abortf:
107 107 if contf and abortf:
108 108 raise util.Abort(_('cannot use both abort and continue'))
109 109 if collapsef:
110 110 raise util.Abort(
111 111 _('cannot use collapse with continue or abort'))
112 112 if detachf:
113 113 raise util.Abort(_('cannot use detach with continue or abort'))
114 114 if srcf or basef or destf:
115 115 raise util.Abort(
116 116 _('abort and continue do not allow specifying revisions'))
117 117
118 118 (originalwd, target, state, skipped, collapsef, keepf,
119 119 keepbranchesf, external) = restorestatus(repo)
120 120 if abortf:
121 121 return abort(repo, originalwd, target, state)
122 122 else:
123 123 if srcf and basef:
124 124 raise util.Abort(_('cannot specify both a '
125 125 'revision and a base'))
126 126 if detachf:
127 127 if not srcf:
128 128 raise util.Abort(
129 129 _('detach requires a revision to be specified'))
130 130 if basef:
131 131 raise util.Abort(_('cannot specify a base with detach'))
132 132
133 133 cmdutil.bail_if_changed(repo)
134 134 result = buildstate(repo, destf, srcf, basef, detachf)
135 135 if not result:
136 136 # Empty state built, nothing to rebase
137 137 ui.status(_('nothing to rebase\n'))
138 138 return 1
139 139 else:
140 140 originalwd, target, state = result
141 141 if collapsef:
142 142 targetancestors = set(repo.changelog.ancestors(target))
143 143 external = checkexternal(repo, state, targetancestors)
144 144
145 145 if keepbranchesf:
146 146 assert not extrafn, 'cannot use both keepbranches and extrafn'
147 147 def extrafn(ctx, extra):
148 148 extra['branch'] = ctx.branch()
149 149
150 150 # Rebase
151 151 if not targetancestors:
152 152 targetancestors = set(repo.changelog.ancestors(target))
153 153 targetancestors.add(target)
154 154
155 155 sortedstate = sorted(state)
156 156 total = len(sortedstate)
157 157 pos = 0
158 158 for rev in sortedstate:
159 159 pos += 1
160 160 if state[rev] == -1:
161 161 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
162 162 _('changesets'), total)
163 163 storestatus(repo, originalwd, target, state, collapsef, keepf,
164 164 keepbranchesf, external)
165 165 p1, p2 = defineparents(repo, rev, target, state,
166 166 targetancestors)
167 167 if len(repo.parents()) == 2:
168 168 repo.ui.debug('resuming interrupted rebase\n')
169 169 else:
170 170 stats = rebasenode(repo, rev, p1, p2, state)
171 171 if stats and stats[3] > 0:
172 172 raise util.Abort(_('unresolved conflicts (see hg '
173 173 'resolve, then hg rebase --continue)'))
174 174 updatedirstate(repo, rev, target, p2)
175 175 if not collapsef:
176 176 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn)
177 177 else:
178 178 # Skip commit if we are collapsing
179 179 repo.dirstate.setparents(repo[p1].node())
180 180 newrev = None
181 181 # Update the state
182 182 if newrev is not None:
183 183 state[rev] = repo[newrev].rev()
184 184 else:
185 185 if not collapsef:
186 186 ui.note(_('no changes, revision %d skipped\n') % rev)
187 187 ui.debug('next revision set to %s\n' % p1)
188 188 skipped.add(rev)
189 189 state[rev] = p1
190 190
191 191 ui.progress(_('rebasing'), None)
192 192 ui.note(_('rebase merging completed\n'))
193 193
194 194 if collapsef and not keepopen:
195 195 p1, p2 = defineparents(repo, min(state), target,
196 196 state, targetancestors)
197 197 if collapsemsg:
198 198 commitmsg = collapsemsg
199 199 else:
200 200 commitmsg = 'Collapsed revision'
201 201 for rebased in state:
202 202 if rebased not in skipped and state[rebased] != nullmerge:
203 203 commitmsg += '\n* %s' % repo[rebased].description()
204 204 commitmsg = ui.edit(commitmsg, repo.ui.username())
205 205 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
206 206 extrafn=extrafn)
207 207
208 208 if 'qtip' in repo.tags():
209 209 updatemq(repo, state, skipped, **opts)
210 210
211 211 if not keepf:
212 212 # Remove no more useful revisions
213 213 rebased = [rev for rev in state if state[rev] != nullmerge]
214 214 if rebased:
215 215 if set(repo.changelog.descendants(min(rebased))) - set(state):
216 216 ui.warn(_("warning: new changesets detected "
217 217 "on source branch, not stripping\n"))
218 218 else:
219 219 # backup the old csets by default
220 220 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
221 221
222 222 clearstatus(repo)
223 223 ui.note(_("rebase completed\n"))
224 224 if os.path.exists(repo.sjoin('undo')):
225 225 util.unlinkpath(repo.sjoin('undo'))
226 226 if skipped:
227 227 ui.note(_("%d revisions have been skipped\n") % len(skipped))
228 228 finally:
229 229 release(lock, wlock)
230 230
231 231 def rebasemerge(repo, rev, first=False):
232 232 'return the correct ancestor'
233 233 oldancestor = ancestor.ancestor
234 234
235 235 def newancestor(a, b, pfunc):
236 236 if b == rev:
237 237 return repo[rev].parents()[0].rev()
238 238 return oldancestor(a, b, pfunc)
239 239
240 240 if not first:
241 241 ancestor.ancestor = newancestor
242 242 else:
243 243 repo.ui.debug("first revision, do not change ancestor\n")
244 244 try:
245 245 stats = merge.update(repo, rev, True, True, False)
246 246 return stats
247 247 finally:
248 248 ancestor.ancestor = oldancestor
249 249
250 250 def checkexternal(repo, state, targetancestors):
251 251 """Check whether one or more external revisions need to be taken in
252 252 consideration. In the latter case, abort.
253 253 """
254 254 external = nullrev
255 255 source = min(state)
256 256 for rev in state:
257 257 if rev == source:
258 258 continue
259 259 # Check externals and fail if there are more than one
260 260 for p in repo[rev].parents():
261 261 if (p.rev() not in state
262 262 and p.rev() not in targetancestors):
263 263 if external != nullrev:
264 264 raise util.Abort(_('unable to collapse, there is more '
265 265 'than one external parent'))
266 266 external = p.rev()
267 267 return external
268 268
269 269 def updatedirstate(repo, rev, p1, p2):
270 270 """Keep track of renamed files in the revision that is going to be rebased
271 271 """
272 272 # Here we simulate the copies and renames in the source changeset
273 273 cop, diver = copies.copies(repo, repo[rev], repo[p1], repo[p2], True)
274 274 m1 = repo[rev].manifest()
275 275 m2 = repo[p1].manifest()
276 276 for k, v in cop.iteritems():
277 277 if k in m1:
278 278 if v in m1 or v in m2:
279 279 repo.dirstate.copy(v, k)
280 280 if v in m2 and v not in m1:
281 281 repo.dirstate.remove(v)
282 282
283 283 def concludenode(repo, rev, p1, p2, commitmsg=None, extrafn=None):
284 284 'Commit the changes and store useful information in extra'
285 285 try:
286 286 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
287 287 ctx = repo[rev]
288 288 if commitmsg is None:
289 289 commitmsg = ctx.description()
290 290 extra = {'rebase_source': ctx.hex()}
291 291 if extrafn:
292 292 extrafn(ctx, extra)
293 293 # Commit might fail if unresolved files exist
294 294 newrev = repo.commit(text=commitmsg, user=ctx.user(),
295 295 date=ctx.date(), extra=extra)
296 296 repo.dirstate.setbranch(repo[newrev].branch())
297 297 return newrev
298 298 except util.Abort:
299 299 # Invalidate the previous setparents
300 300 repo.dirstate.invalidate()
301 301 raise
302 302
303 303 def rebasenode(repo, rev, p1, p2, state):
304 304 'Rebase a single revision'
305 305 # Merge phase
306 306 # Update to target and merge it with local
307 307 if repo['.'].rev() != repo[p1].rev():
308 308 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
309 309 merge.update(repo, p1, False, True, False)
310 310 else:
311 311 repo.ui.debug(" already in target\n")
312 312 repo.dirstate.write()
313 313 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
314 314 first = repo[rev].rev() == repo[min(state)].rev()
315 315 stats = rebasemerge(repo, rev, first)
316 316 return stats
317 317
318 318 def defineparents(repo, rev, target, state, targetancestors):
319 319 'Return the new parent relationship of the revision that will be rebased'
320 320 parents = repo[rev].parents()
321 321 p1 = p2 = nullrev
322 322
323 323 P1n = parents[0].rev()
324 324 if P1n in targetancestors:
325 325 p1 = target
326 326 elif P1n in state:
327 327 if state[P1n] == nullmerge:
328 328 p1 = target
329 329 else:
330 330 p1 = state[P1n]
331 331 else: # P1n external
332 332 p1 = target
333 333 p2 = P1n
334 334
335 335 if len(parents) == 2 and parents[1].rev() not in targetancestors:
336 336 P2n = parents[1].rev()
337 337 # interesting second parent
338 338 if P2n in state:
339 339 if p1 == target: # P1n in targetancestors or external
340 340 p1 = state[P2n]
341 341 else:
342 342 p2 = state[P2n]
343 343 else: # P2n external
344 344 if p2 != nullrev: # P1n external too => rev is a merged revision
345 345 raise util.Abort(_('cannot use revision %d as base, result '
346 346 'would have 3 parents') % rev)
347 347 p2 = P2n
348 348 repo.ui.debug(" future parents are %d and %d\n" %
349 349 (repo[p1].rev(), repo[p2].rev()))
350 350 return p1, p2
351 351
352 352 def isagitpatch(repo, patchname):
353 353 'Return true if the given patch is in git format'
354 354 mqpatch = os.path.join(repo.mq.path, patchname)
355 355 for line in patch.linereader(file(mqpatch, 'rb')):
356 356 if line.startswith('diff --git'):
357 357 return True
358 358 return False
359 359
360 360 def updatemq(repo, state, skipped, **opts):
361 361 'Update rebased mq patches - finalize and then import them'
362 362 mqrebase = {}
363 363 mq = repo.mq
364 364 for p in mq.applied:
365 365 rev = repo[p.node].rev()
366 366 if rev in state:
367 367 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
368 368 (rev, p.name))
369 369 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
370 370
371 371 if mqrebase:
372 372 mq.finish(repo, mqrebase.keys())
373 373
374 374 # We must start import from the newest revision
375 375 for rev in sorted(mqrebase, reverse=True):
376 376 if rev not in skipped:
377 377 name, isgit = mqrebase[rev]
378 378 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
379 379 mq.qimport(repo, (), patchname=name, git=isgit,
380 380 rev=[str(state[rev])])
381 381 mq.save_dirty()
382 382
383 383 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
384 384 external):
385 385 'Store the current status to allow recovery'
386 386 f = repo.opener("rebasestate", "w")
387 387 f.write(repo[originalwd].hex() + '\n')
388 388 f.write(repo[target].hex() + '\n')
389 389 f.write(repo[external].hex() + '\n')
390 390 f.write('%d\n' % int(collapse))
391 391 f.write('%d\n' % int(keep))
392 392 f.write('%d\n' % int(keepbranches))
393 393 for d, v in state.iteritems():
394 394 oldrev = repo[d].hex()
395 395 newrev = repo[v].hex()
396 396 f.write("%s:%s\n" % (oldrev, newrev))
397 397 f.close()
398 398 repo.ui.debug('rebase status stored\n')
399 399
400 400 def clearstatus(repo):
401 401 'Remove the status files'
402 402 if os.path.exists(repo.join("rebasestate")):
403 403 util.unlinkpath(repo.join("rebasestate"))
404 404
405 405 def restorestatus(repo):
406 406 'Restore a previously stored status'
407 407 try:
408 408 target = None
409 409 collapse = False
410 410 external = nullrev
411 411 state = {}
412 412 f = repo.opener("rebasestate")
413 413 for i, l in enumerate(f.read().splitlines()):
414 414 if i == 0:
415 415 originalwd = repo[l].rev()
416 416 elif i == 1:
417 417 target = repo[l].rev()
418 418 elif i == 2:
419 419 external = repo[l].rev()
420 420 elif i == 3:
421 421 collapse = bool(int(l))
422 422 elif i == 4:
423 423 keep = bool(int(l))
424 424 elif i == 5:
425 425 keepbranches = bool(int(l))
426 426 else:
427 427 oldrev, newrev = l.split(':')
428 428 state[repo[oldrev].rev()] = repo[newrev].rev()
429 429 skipped = set()
430 430 # recompute the set of skipped revs
431 431 if not collapse:
432 432 seen = set([target])
433 433 for old, new in sorted(state.items()):
434 434 if new != nullrev and new in seen:
435 435 skipped.add(old)
436 436 seen.add(new)
437 437 repo.ui.debug('computed skipped revs: %s\n' % skipped)
438 438 repo.ui.debug('rebase status resumed\n')
439 439 return (originalwd, target, state, skipped,
440 440 collapse, keep, keepbranches, external)
441 441 except IOError, err:
442 442 if err.errno != errno.ENOENT:
443 443 raise
444 444 raise util.Abort(_('no rebase in progress'))
445 445
446 446 def abort(repo, originalwd, target, state):
447 447 'Restore the repository to its original state'
448 448 if set(repo.changelog.descendants(target)) - set(state.values()):
449 449 repo.ui.warn(_("warning: new changesets detected on target branch, "
450 450 "can't abort\n"))
451 451 return -1
452 452 else:
453 453 # Strip from the first rebased revision
454 454 merge.update(repo, repo[originalwd].rev(), False, True, False)
455 455 rebased = filter(lambda x: x > -1 and x != target, state.values())
456 456 if rebased:
457 457 strippoint = min(rebased)
458 458 # no backup of rebased cset versions needed
459 459 repair.strip(repo.ui, repo, repo[strippoint].node())
460 460 clearstatus(repo)
461 461 repo.ui.warn(_('rebase aborted\n'))
462 462 return 0
463 463
464 464 def buildstate(repo, dest, src, base, detach):
465 465 'Define which revisions are going to be rebased and where'
466 466 targetancestors = set()
467 467 detachset = set()
468 468
469 469 if not dest:
470 470 # Destination defaults to the latest revision in the current branch
471 471 branch = repo[None].branch()
472 472 dest = repo[branch].rev()
473 473 else:
474 474 dest = repo[dest].rev()
475 475
476 476 # This check isn't strictly necessary, since mq detects commits over an
477 477 # applied patch. But it prevents messing up the working directory when
478 478 # a partially completed rebase is blocked by mq.
479 479 if 'qtip' in repo.tags() and (repo[dest].node() in
480 480 [s.node for s in repo.mq.applied]):
481 481 raise util.Abort(_('cannot rebase onto an applied mq patch'))
482 482
483 483 if src:
484 484 commonbase = repo[src].ancestor(repo[dest])
485 samebranch = repo[src].branch() == repo[dest].branch()
485 486 if commonbase == repo[src]:
486 487 raise util.Abort(_('source is ancestor of destination'))
487 if commonbase == repo[dest]:
488 if samebranch and commonbase == repo[dest]:
488 489 raise util.Abort(_('source is descendant of destination'))
489 490 source = repo[src].rev()
490 491 if detach:
491 492 # We need to keep track of source's ancestors up to the common base
492 493 srcancestors = set(repo.changelog.ancestors(source))
493 494 baseancestors = set(repo.changelog.ancestors(commonbase.rev()))
494 495 detachset = srcancestors - baseancestors
495 496 detachset.discard(commonbase.rev())
496 497 else:
497 498 if base:
498 499 cwd = repo[base].rev()
499 500 else:
500 501 cwd = repo['.'].rev()
501 502
502 503 if cwd == dest:
503 504 repo.ui.debug('source and destination are the same\n')
504 505 return None
505 506
506 507 targetancestors = set(repo.changelog.ancestors(dest))
507 508 if cwd in targetancestors:
508 509 repo.ui.debug('source is ancestor of destination\n')
509 510 return None
510 511
511 512 cwdancestors = set(repo.changelog.ancestors(cwd))
512 513 if dest in cwdancestors:
513 514 repo.ui.debug('source is descendant of destination\n')
514 515 return None
515 516
516 517 cwdancestors.add(cwd)
517 518 rebasingbranch = cwdancestors - targetancestors
518 519 source = min(rebasingbranch)
519 520
520 521 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
521 522 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
522 523 state.update(dict.fromkeys(detachset, nullmerge))
523 524 state[source] = nullrev
524 525 return repo['.'].rev(), repo[dest].rev(), state
525 526
526 527 def pullrebase(orig, ui, repo, *args, **opts):
527 528 'Call rebase after pull if the latter has been invoked with --rebase'
528 529 if opts.get('rebase'):
529 530 if opts.get('update'):
530 531 del opts['update']
531 532 ui.debug('--update and --rebase are not compatible, ignoring '
532 533 'the update flag\n')
533 534
534 535 cmdutil.bail_if_changed(repo)
535 536 revsprepull = len(repo)
536 537 origpostincoming = commands.postincoming
537 538 def _dummy(*args, **kwargs):
538 539 pass
539 540 commands.postincoming = _dummy
540 541 try:
541 542 orig(ui, repo, *args, **opts)
542 543 finally:
543 544 commands.postincoming = origpostincoming
544 545 revspostpull = len(repo)
545 546 if revspostpull > revsprepull:
546 547 rebase(ui, repo, **opts)
547 548 branch = repo[None].branch()
548 549 dest = repo[branch].rev()
549 550 if dest != repo['.'].rev():
550 551 # there was nothing to rebase we force an update
551 552 hg.update(repo, dest)
552 553 else:
553 554 orig(ui, repo, *args, **opts)
554 555
555 556 def uisetup(ui):
556 557 'Replace pull with a decorator to provide --rebase option'
557 558 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
558 559 entry[1].append(('', 'rebase', None,
559 560 _("rebase working directory to branch head"))
560 561 )
561 562
562 563 cmdtable = {
563 564 "rebase":
564 565 (rebase,
565 566 [
566 567 ('s', 'source', '',
567 568 _('rebase from the specified changeset'), _('REV')),
568 569 ('b', 'base', '',
569 570 _('rebase from the base of the specified changeset '
570 571 '(up to greatest common ancestor of base and dest)'),
571 572 _('REV')),
572 573 ('d', 'dest', '',
573 574 _('rebase onto the specified changeset'), _('REV')),
574 575 ('', 'collapse', False, _('collapse the rebased changesets')),
575 576 ('m', 'message', '',
576 577 _('use text as collapse commit message'), _('TEXT')),
577 578 ('l', 'logfile', '',
578 579 _('read collapse commit message from file'), _('FILE')),
579 580 ('', 'keep', False, _('keep original changesets')),
580 581 ('', 'keepbranches', False, _('keep original branch names')),
581 582 ('', 'detach', False, _('force detaching of source from its original '
582 583 'branch')),
583 584 ('c', 'continue', False, _('continue an interrupted rebase')),
584 585 ('a', 'abort', False, _('abort an interrupted rebase'))] +
585 586 templateopts,
586 587 _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
587 588 'hg rebase {-a|-c}'))
588 589 }
General Comments 0
You need to be logged in to leave comments. Login now