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