##// END OF EJS Templates
rebase: allow creation obsolescence relation instead of stripping...
Pierre-Yves David -
r17612:fc2a6114 default
parent child Browse files
Show More
@@ -0,0 +1,175 b''
1 ==========================
2 Test rebase with obsolete
3 ==========================
4
5 Enable obsolete
6
7 $ cat > ${TESTTMP}/obs.py << EOF
8 > import mercurial.obsolete
9 > mercurial.obsolete._enabled = True
10 > EOF
11 $ cat >> $HGRCPATH << EOF
12 > [ui]
13 > logtemplate= {rev}:{node|short} {desc|firstline}
14 > [phases]
15 > publish=False
16 > [extensions]'
17 > rebase=
18 >
19 > obs=${TESTTMP}/obs.py
20 > EOF
21
22 Setup rebase canonical repo
23
24 $ hg init base
25 $ cd base
26 $ hg unbundle "$TESTDIR/bundles/rebase.hg"
27 adding changesets
28 adding manifests
29 adding file changes
30 added 8 changesets with 7 changes to 7 files (+2 heads)
31 (run 'hg heads' to see heads, 'hg merge' to merge)
32 $ hg up tip
33 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 $ hg log -G
35 @ 7:02de42196ebe H
36 |
37 | o 6:eea13746799a G
38 |/|
39 o | 5:24b6387c8c8c F
40 | |
41 | o 4:9520eea781bc E
42 |/
43 | o 3:32af7686d403 D
44 | |
45 | o 2:5fddd98957c8 C
46 | |
47 | o 1:42ccdea3bb16 B
48 |/
49 o 0:cd010b8cd998 A
50
51 $ cd ..
52
53 simple rebase
54 ---------------------------------
55
56 $ hg clone base simple
57 updating to branch default
58 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
59 $ cd simple
60 $ hg up 32af7686d403
61 3 files updated, 0 files merged, 2 files removed, 0 files unresolved
62 $ hg rebase -d eea13746799a
63 $ hg log -G
64 @ 10:8eeb3c33ad33 D
65 |
66 o 9:2327fea05063 C
67 |
68 o 8:e4e5be0395b2 B
69 |
70 | o 7:02de42196ebe H
71 | |
72 o | 6:eea13746799a G
73 |\|
74 | o 5:24b6387c8c8c F
75 | |
76 o | 4:9520eea781bc E
77 |/
78 o 0:cd010b8cd998 A
79
80 $ hg log --hidden -G
81 @ 10:8eeb3c33ad33 D
82 |
83 o 9:2327fea05063 C
84 |
85 o 8:e4e5be0395b2 B
86 |
87 | o 7:02de42196ebe H
88 | |
89 o | 6:eea13746799a G
90 |\|
91 | o 5:24b6387c8c8c F
92 | |
93 o | 4:9520eea781bc E
94 |/
95 | x 3:32af7686d403 D
96 | |
97 | x 2:5fddd98957c8 C
98 | |
99 | x 1:42ccdea3bb16 B
100 |/
101 o 0:cd010b8cd998 A
102
103 $ hg debugobsolete
104 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 e4e5be0395b2cbd471ed22a26b1b6a1a0658a794 0 {'date': '*', 'user': 'test'} (glob)
105 5fddd98957c8a54a4d436dfe1da9d87f21a1b97b 2327fea05063f39961b14cb69435a9898dc9a245 0 {'date': '*', 'user': 'test'} (glob)
106 32af7686d403cf45b5d95f2d70cebea587ac806a 8eeb3c33ad33d452c89e5dcf611c347f978fb42b 0 {'date': '*', 'user': 'test'} (glob)
107
108
109 $ cd ..
110
111 empty changeset
112 ---------------------------------
113
114 $ hg clone base empty
115 updating to branch default
116 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
117 $ cd empty
118 $ hg up eea13746799a
119 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
120
121 We make a copy of both the first changeset in the rebased and some other in the
122 set.
123
124 $ hg graft 42ccdea3bb16 32af7686d403
125 grafting revision 1
126 grafting revision 3
127 $ hg rebase -s 42ccdea3bb16 -d .
128 $ hg log -G
129 @ 10:5ae4c968c6ac C
130 |
131 o 9:08483444fef9 D
132 |
133 o 8:8877864f1edb B
134 |
135 | o 7:02de42196ebe H
136 | |
137 o | 6:eea13746799a G
138 |\|
139 | o 5:24b6387c8c8c F
140 | |
141 o | 4:9520eea781bc E
142 |/
143 o 0:cd010b8cd998 A
144
145 $ hg log --hidden -G
146 @ 10:5ae4c968c6ac C
147 |
148 o 9:08483444fef9 D
149 |
150 o 8:8877864f1edb B
151 |
152 | o 7:02de42196ebe H
153 | |
154 o | 6:eea13746799a G
155 |\|
156 | o 5:24b6387c8c8c F
157 | |
158 o | 4:9520eea781bc E
159 |/
160 | x 3:32af7686d403 D
161 | |
162 | x 2:5fddd98957c8 C
163 | |
164 | x 1:42ccdea3bb16 B
165 |/
166 o 0:cd010b8cd998 A
167
168 $ hg debugobsolete
169 42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 08483444fef91d6224f6655ee586a65d263ad34c 0 {'date': '*', 'user': 'test'} (glob)
170 5fddd98957c8a54a4d436dfe1da9d87f21a1b97b 5ae4c968c6aca831df823664e706c9d4aa34473d 0 {'date': '*', 'user': 'test'} (glob)
171 32af7686d403cf45b5d95f2d70cebea587ac806a 5ae4c968c6aca831df823664e706c9d4aa34473d 0 {'date': '*', 'user': 'test'} (glob)
172
173
174 $ cd ..
175
@@ -1,712 +1,720 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, bookmarks
18 from mercurial import extensions, patch, scmutil, phases
18 from mercurial import extensions, patch, scmutil, phases, obsolete
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 cmdtable = {}
28 28 command = cmdutil.command(cmdtable)
29 29 testedwith = 'internal'
30 30
31 31 @command('rebase',
32 32 [('s', 'source', '',
33 33 _('rebase from the specified changeset'), _('REV')),
34 34 ('b', 'base', '',
35 35 _('rebase from the base of the specified changeset '
36 36 '(up to greatest common ancestor of base and dest)'),
37 37 _('REV')),
38 38 ('r', 'rev', [],
39 39 _('rebase these revisions'),
40 40 _('REV')),
41 41 ('d', 'dest', '',
42 42 _('rebase onto the specified changeset'), _('REV')),
43 43 ('', 'collapse', False, _('collapse the rebased changesets')),
44 44 ('m', 'message', '',
45 45 _('use text as collapse commit message'), _('TEXT')),
46 46 ('e', 'edit', False, _('invoke editor on commit messages')),
47 47 ('l', 'logfile', '',
48 48 _('read collapse commit message from file'), _('FILE')),
49 49 ('', 'keep', False, _('keep original changesets')),
50 50 ('', 'keepbranches', False, _('keep original branch names')),
51 51 ('D', 'detach', False, _('(DEPRECATED)')),
52 52 ('t', 'tool', '', _('specify merge tool')),
53 53 ('c', 'continue', False, _('continue an interrupted rebase')),
54 54 ('a', 'abort', False, _('abort an interrupted rebase'))] +
55 55 templateopts,
56 56 _('[-s REV | -b REV] [-d REV] [OPTION]'))
57 57 def rebase(ui, repo, **opts):
58 58 """move changeset (and descendants) to a different branch
59 59
60 60 Rebase uses repeated merging to graft changesets from one part of
61 61 history (the source) onto another (the destination). This can be
62 62 useful for linearizing *local* changes relative to a master
63 63 development tree.
64 64
65 65 You should not rebase changesets that have already been shared
66 66 with others. Doing so will force everybody else to perform the
67 67 same rebase or they will end up with duplicated changesets after
68 68 pulling in your rebased changesets.
69 69
70 70 If you don't specify a destination changeset (``-d/--dest``),
71 71 rebase uses the tipmost head of the current named branch as the
72 72 destination. (The destination changeset is not modified by
73 73 rebasing, but new changesets are added as its descendants.)
74 74
75 75 You can specify which changesets to rebase in two ways: as a
76 76 "source" changeset or as a "base" changeset. Both are shorthand
77 77 for a topologically related set of changesets (the "source
78 78 branch"). If you specify source (``-s/--source``), rebase will
79 79 rebase that changeset and all of its descendants onto dest. If you
80 80 specify base (``-b/--base``), rebase will select ancestors of base
81 81 back to but not including the common ancestor with dest. Thus,
82 82 ``-b`` is less precise but more convenient than ``-s``: you can
83 83 specify any changeset in the source branch, and rebase will select
84 84 the whole branch. If you specify neither ``-s`` nor ``-b``, rebase
85 85 uses the parent of the working directory as the base.
86 86
87 87 By default, rebase recreates the changesets in the source branch
88 88 as descendants of dest and then destroys the originals. Use
89 89 ``--keep`` to preserve the original source changesets. Some
90 90 changesets in the source branch (e.g. merges from the destination
91 91 branch) may be dropped if they no longer contribute any change.
92 92
93 93 One result of the rules for selecting the destination changeset
94 94 and source branch is that, unlike ``merge``, rebase will do
95 95 nothing if you are at the latest (tipmost) head of a named branch
96 96 with two heads. You need to explicitly specify source and/or
97 97 destination (or ``update`` to the other head, if it's the head of
98 98 the intended source branch).
99 99
100 100 If a rebase is interrupted to manually resolve a merge, it can be
101 101 continued with --continue/-c or aborted with --abort/-a.
102 102
103 103 Returns 0 on success, 1 if nothing to rebase.
104 104 """
105 105 originalwd = target = None
106 106 external = nullrev
107 107 state = {}
108 108 skipped = set()
109 109 targetancestors = set()
110 110
111 111 editor = None
112 112 if opts.get('edit'):
113 113 editor = cmdutil.commitforceeditor
114 114
115 115 lock = wlock = None
116 116 try:
117 117 wlock = repo.wlock()
118 118 lock = repo.lock()
119 119
120 120 # Validate input and define rebasing points
121 121 destf = opts.get('dest', None)
122 122 srcf = opts.get('source', None)
123 123 basef = opts.get('base', None)
124 124 revf = opts.get('rev', [])
125 125 contf = opts.get('continue')
126 126 abortf = opts.get('abort')
127 127 collapsef = opts.get('collapse', False)
128 128 collapsemsg = cmdutil.logmessage(ui, opts)
129 129 extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
130 130 keepf = opts.get('keep', False)
131 131 keepbranchesf = opts.get('keepbranches', False)
132 132 # keepopen is not meant for use on the command line, but by
133 133 # other extensions
134 134 keepopen = opts.get('keepopen', False)
135 135
136 136 if collapsemsg and not collapsef:
137 137 raise util.Abort(
138 138 _('message can only be specified with collapse'))
139 139
140 140 if contf or abortf:
141 141 if contf and abortf:
142 142 raise util.Abort(_('cannot use both abort and continue'))
143 143 if collapsef:
144 144 raise util.Abort(
145 145 _('cannot use collapse with continue or abort'))
146 146 if srcf or basef or destf:
147 147 raise util.Abort(
148 148 _('abort and continue do not allow specifying revisions'))
149 149 if opts.get('tool', False):
150 150 ui.warn(_('tool option will be ignored\n'))
151 151
152 152 (originalwd, target, state, skipped, collapsef, keepf,
153 153 keepbranchesf, external) = restorestatus(repo)
154 154 if abortf:
155 155 return abort(repo, originalwd, target, state)
156 156 else:
157 157 if srcf and basef:
158 158 raise util.Abort(_('cannot specify both a '
159 159 'source and a base'))
160 160 if revf and basef:
161 161 raise util.Abort(_('cannot specify both a '
162 162 'revision and a base'))
163 163 if revf and srcf:
164 164 raise util.Abort(_('cannot specify both a '
165 165 'revision and a source'))
166 166
167 167 cmdutil.bailifchanged(repo)
168 168
169 169 if not destf:
170 170 # Destination defaults to the latest revision in the
171 171 # current branch
172 172 branch = repo[None].branch()
173 173 dest = repo[branch]
174 174 else:
175 175 dest = scmutil.revsingle(repo, destf)
176 176
177 177 if revf:
178 178 rebaseset = repo.revs('%lr', revf)
179 179 elif srcf:
180 180 src = scmutil.revrange(repo, [srcf])
181 181 rebaseset = repo.revs('(%ld)::', src)
182 182 else:
183 183 base = scmutil.revrange(repo, [basef or '.'])
184 184 rebaseset = repo.revs(
185 185 '(children(ancestor(%ld, %d)) and ::(%ld))::',
186 186 base, dest, base)
187 187
188 188 if rebaseset:
189 189 root = min(rebaseset)
190 190 else:
191 191 root = None
192 192
193 193 if not rebaseset:
194 194 repo.ui.debug('base is ancestor of destination\n')
195 195 result = None
196 196 elif not keepf and list(repo.revs('first(children(%ld) - %ld)',
197 197 rebaseset, rebaseset)):
198 198 raise util.Abort(
199 199 _("can't remove original changesets with"
200 200 " unrebased descendants"),
201 201 hint=_('use --keep to keep original changesets'))
202 202 elif not keepf and not repo[root].mutable():
203 203 raise util.Abort(_("can't rebase immutable changeset %s")
204 204 % repo[root],
205 205 hint=_('see hg help phases for details'))
206 206 else:
207 207 result = buildstate(repo, dest, rebaseset, collapsef)
208 208
209 209 if not result:
210 210 # Empty state built, nothing to rebase
211 211 ui.status(_('nothing to rebase\n'))
212 212 return 1
213 213 else:
214 214 originalwd, target, state = result
215 215 if collapsef:
216 216 targetancestors = set(repo.changelog.ancestors([target]))
217 217 targetancestors.add(target)
218 218 external = checkexternal(repo, state, targetancestors)
219 219
220 220 if keepbranchesf:
221 221 assert not extrafn, 'cannot use both keepbranches and extrafn'
222 222 def extrafn(ctx, extra):
223 223 extra['branch'] = ctx.branch()
224 224 if collapsef:
225 225 branches = set()
226 226 for rev in state:
227 227 branches.add(repo[rev].branch())
228 228 if len(branches) > 1:
229 229 raise util.Abort(_('cannot collapse multiple named '
230 230 'branches'))
231 231
232 232
233 233 # Rebase
234 234 if not targetancestors:
235 235 targetancestors = set(repo.changelog.ancestors([target]))
236 236 targetancestors.add(target)
237 237
238 238 # Keep track of the current bookmarks in order to reset them later
239 239 currentbookmarks = repo._bookmarks.copy()
240 240 activebookmark = repo._bookmarkcurrent
241 241 if activebookmark:
242 242 bookmarks.unsetcurrent(repo)
243 243
244 244 sortedstate = sorted(state)
245 245 total = len(sortedstate)
246 246 pos = 0
247 247 for rev in sortedstate:
248 248 pos += 1
249 249 if state[rev] == -1:
250 250 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
251 251 _('changesets'), total)
252 252 storestatus(repo, originalwd, target, state, collapsef, keepf,
253 253 keepbranchesf, external)
254 254 p1, p2 = defineparents(repo, rev, target, state,
255 255 targetancestors)
256 256 if len(repo.parents()) == 2:
257 257 repo.ui.debug('resuming interrupted rebase\n')
258 258 else:
259 259 try:
260 260 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
261 261 stats = rebasenode(repo, rev, p1, state, collapsef)
262 262 if stats and stats[3] > 0:
263 263 raise util.Abort(_('unresolved conflicts (see hg '
264 264 'resolve, then hg rebase --continue)'))
265 265 finally:
266 266 ui.setconfig('ui', 'forcemerge', '')
267 267 cmdutil.duplicatecopies(repo, rev, target)
268 268 if not collapsef:
269 269 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
270 270 editor=editor)
271 271 else:
272 272 # Skip commit if we are collapsing
273 273 repo.setparents(repo[p1].node())
274 274 newrev = None
275 275 # Update the state
276 276 if newrev is not None:
277 277 state[rev] = repo[newrev].rev()
278 278 else:
279 279 if not collapsef:
280 280 ui.note(_('no changes, revision %d skipped\n') % rev)
281 281 ui.debug('next revision set to %s\n' % p1)
282 282 skipped.add(rev)
283 283 state[rev] = p1
284 284
285 285 ui.progress(_('rebasing'), None)
286 286 ui.note(_('rebase merging completed\n'))
287 287
288 288 if collapsef and not keepopen:
289 289 p1, p2 = defineparents(repo, min(state), target,
290 290 state, targetancestors)
291 291 if collapsemsg:
292 292 commitmsg = collapsemsg
293 293 else:
294 294 commitmsg = 'Collapsed revision'
295 295 for rebased in state:
296 296 if rebased not in skipped and state[rebased] != nullmerge:
297 297 commitmsg += '\n* %s' % repo[rebased].description()
298 298 commitmsg = ui.edit(commitmsg, repo.ui.username())
299 299 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
300 300 extrafn=extrafn, editor=editor)
301 301
302 302 if 'qtip' in repo.tags():
303 303 updatemq(repo, state, skipped, **opts)
304 304
305 305 if currentbookmarks:
306 306 # Nodeids are needed to reset bookmarks
307 307 nstate = {}
308 308 for k, v in state.iteritems():
309 309 if v != nullmerge:
310 310 nstate[repo[k].node()] = repo[v].node()
311 311
312 312 if not keepf:
313 313 clearrebased(ui, repo, state)
314 314
315 315 if currentbookmarks:
316 316 updatebookmarks(repo, nstate, currentbookmarks, **opts)
317 317
318 318 clearstatus(repo)
319 319 ui.note(_("rebase completed\n"))
320 320 if os.path.exists(repo.sjoin('undo')):
321 321 util.unlinkpath(repo.sjoin('undo'))
322 322 if skipped:
323 323 ui.note(_("%d revisions have been skipped\n") % len(skipped))
324 324
325 325 if (activebookmark and
326 326 repo['tip'].node() == repo._bookmarks[activebookmark]):
327 327 bookmarks.setcurrent(repo, activebookmark)
328 328
329 329 finally:
330 330 release(lock, wlock)
331 331
332 332 def checkexternal(repo, state, targetancestors):
333 333 """Check whether one or more external revisions need to be taken in
334 334 consideration. In the latter case, abort.
335 335 """
336 336 external = nullrev
337 337 source = min(state)
338 338 for rev in state:
339 339 if rev == source:
340 340 continue
341 341 # Check externals and fail if there are more than one
342 342 for p in repo[rev].parents():
343 343 if (p.rev() not in state
344 344 and p.rev() not in targetancestors):
345 345 if external != nullrev:
346 346 raise util.Abort(_('unable to collapse, there is more '
347 347 'than one external parent'))
348 348 external = p.rev()
349 349 return external
350 350
351 351 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
352 352 'Commit the changes and store useful information in extra'
353 353 try:
354 354 repo.setparents(repo[p1].node(), repo[p2].node())
355 355 ctx = repo[rev]
356 356 if commitmsg is None:
357 357 commitmsg = ctx.description()
358 358 extra = {'rebase_source': ctx.hex()}
359 359 if extrafn:
360 360 extrafn(ctx, extra)
361 361 # Commit might fail if unresolved files exist
362 362 newrev = repo.commit(text=commitmsg, user=ctx.user(),
363 363 date=ctx.date(), extra=extra, editor=editor)
364 364 repo.dirstate.setbranch(repo[newrev].branch())
365 365 targetphase = max(ctx.phase(), phases.draft)
366 366 # retractboundary doesn't overwrite upper phase inherited from parent
367 367 newnode = repo[newrev].node()
368 368 if newnode:
369 369 phases.retractboundary(repo, targetphase, [newnode])
370 370 return newrev
371 371 except util.Abort:
372 372 # Invalidate the previous setparents
373 373 repo.dirstate.invalidate()
374 374 raise
375 375
376 376 def rebasenode(repo, rev, p1, state, collapse):
377 377 'Rebase a single revision'
378 378 # Merge phase
379 379 # Update to target and merge it with local
380 380 if repo['.'].rev() != repo[p1].rev():
381 381 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
382 382 merge.update(repo, p1, False, True, False)
383 383 else:
384 384 repo.ui.debug(" already in target\n")
385 385 repo.dirstate.write()
386 386 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
387 387 base = None
388 388 if repo[rev].rev() != repo[min(state)].rev():
389 389 base = repo[rev].p1().node()
390 390 # When collapsing in-place, the parent is the common ancestor, we
391 391 # have to allow merging with it.
392 392 return merge.update(repo, rev, True, True, False, base, collapse)
393 393
394 394 def defineparents(repo, rev, target, state, targetancestors):
395 395 'Return the new parent relationship of the revision that will be rebased'
396 396 parents = repo[rev].parents()
397 397 p1 = p2 = nullrev
398 398
399 399 P1n = parents[0].rev()
400 400 if P1n in targetancestors:
401 401 p1 = target
402 402 elif P1n in state:
403 403 if state[P1n] == nullmerge:
404 404 p1 = target
405 405 else:
406 406 p1 = state[P1n]
407 407 else: # P1n external
408 408 p1 = target
409 409 p2 = P1n
410 410
411 411 if len(parents) == 2 and parents[1].rev() not in targetancestors:
412 412 P2n = parents[1].rev()
413 413 # interesting second parent
414 414 if P2n in state:
415 415 if p1 == target: # P1n in targetancestors or external
416 416 p1 = state[P2n]
417 417 else:
418 418 p2 = state[P2n]
419 419 else: # P2n external
420 420 if p2 != nullrev: # P1n external too => rev is a merged revision
421 421 raise util.Abort(_('cannot use revision %d as base, result '
422 422 'would have 3 parents') % rev)
423 423 p2 = P2n
424 424 repo.ui.debug(" future parents are %d and %d\n" %
425 425 (repo[p1].rev(), repo[p2].rev()))
426 426 return p1, p2
427 427
428 428 def isagitpatch(repo, patchname):
429 429 'Return true if the given patch is in git format'
430 430 mqpatch = os.path.join(repo.mq.path, patchname)
431 431 for line in patch.linereader(file(mqpatch, 'rb')):
432 432 if line.startswith('diff --git'):
433 433 return True
434 434 return False
435 435
436 436 def updatemq(repo, state, skipped, **opts):
437 437 'Update rebased mq patches - finalize and then import them'
438 438 mqrebase = {}
439 439 mq = repo.mq
440 440 original_series = mq.fullseries[:]
441 441 skippedpatches = set()
442 442
443 443 for p in mq.applied:
444 444 rev = repo[p.node].rev()
445 445 if rev in state:
446 446 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
447 447 (rev, p.name))
448 448 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
449 449 else:
450 450 # Applied but not rebased, not sure this should happen
451 451 skippedpatches.add(p.name)
452 452
453 453 if mqrebase:
454 454 mq.finish(repo, mqrebase.keys())
455 455
456 456 # We must start import from the newest revision
457 457 for rev in sorted(mqrebase, reverse=True):
458 458 if rev not in skipped:
459 459 name, isgit = mqrebase[rev]
460 460 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
461 461 mq.qimport(repo, (), patchname=name, git=isgit,
462 462 rev=[str(state[rev])])
463 463 else:
464 464 # Rebased and skipped
465 465 skippedpatches.add(mqrebase[rev][0])
466 466
467 467 # Patches were either applied and rebased and imported in
468 468 # order, applied and removed or unapplied. Discard the removed
469 469 # ones while preserving the original series order and guards.
470 470 newseries = [s for s in original_series
471 471 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
472 472 mq.fullseries[:] = newseries
473 473 mq.seriesdirty = True
474 474 mq.savedirty()
475 475
476 476 def updatebookmarks(repo, nstate, originalbookmarks, **opts):
477 477 'Move bookmarks to their correct changesets'
478 478 for k, v in originalbookmarks.iteritems():
479 479 if v in nstate:
480 480 if nstate[v] != nullmerge:
481 481 # update the bookmarks for revs that have moved
482 482 repo._bookmarks[k] = nstate[v]
483 483
484 484 bookmarks.write(repo)
485 485
486 486 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
487 487 external):
488 488 'Store the current status to allow recovery'
489 489 f = repo.opener("rebasestate", "w")
490 490 f.write(repo[originalwd].hex() + '\n')
491 491 f.write(repo[target].hex() + '\n')
492 492 f.write(repo[external].hex() + '\n')
493 493 f.write('%d\n' % int(collapse))
494 494 f.write('%d\n' % int(keep))
495 495 f.write('%d\n' % int(keepbranches))
496 496 for d, v in state.iteritems():
497 497 oldrev = repo[d].hex()
498 498 if v != nullmerge:
499 499 newrev = repo[v].hex()
500 500 else:
501 501 newrev = v
502 502 f.write("%s:%s\n" % (oldrev, newrev))
503 503 f.close()
504 504 repo.ui.debug('rebase status stored\n')
505 505
506 506 def clearstatus(repo):
507 507 'Remove the status files'
508 508 if os.path.exists(repo.join("rebasestate")):
509 509 util.unlinkpath(repo.join("rebasestate"))
510 510
511 511 def restorestatus(repo):
512 512 'Restore a previously stored status'
513 513 try:
514 514 target = None
515 515 collapse = False
516 516 external = nullrev
517 517 state = {}
518 518 f = repo.opener("rebasestate")
519 519 for i, l in enumerate(f.read().splitlines()):
520 520 if i == 0:
521 521 originalwd = repo[l].rev()
522 522 elif i == 1:
523 523 target = repo[l].rev()
524 524 elif i == 2:
525 525 external = repo[l].rev()
526 526 elif i == 3:
527 527 collapse = bool(int(l))
528 528 elif i == 4:
529 529 keep = bool(int(l))
530 530 elif i == 5:
531 531 keepbranches = bool(int(l))
532 532 else:
533 533 oldrev, newrev = l.split(':')
534 534 if newrev != str(nullmerge):
535 535 state[repo[oldrev].rev()] = repo[newrev].rev()
536 536 else:
537 537 state[repo[oldrev].rev()] = int(newrev)
538 538 skipped = set()
539 539 # recompute the set of skipped revs
540 540 if not collapse:
541 541 seen = set([target])
542 542 for old, new in sorted(state.items()):
543 543 if new != nullrev and new in seen:
544 544 skipped.add(old)
545 545 seen.add(new)
546 546 repo.ui.debug('computed skipped revs: %s\n' % skipped)
547 547 repo.ui.debug('rebase status resumed\n')
548 548 return (originalwd, target, state, skipped,
549 549 collapse, keep, keepbranches, external)
550 550 except IOError, err:
551 551 if err.errno != errno.ENOENT:
552 552 raise
553 553 raise util.Abort(_('no rebase in progress'))
554 554
555 555 def abort(repo, originalwd, target, state):
556 556 'Restore the repository to its original state'
557 557 dstates = [s for s in state.values() if s != nullrev]
558 558 immutable = [d for d in dstates if not repo[d].mutable()]
559 559 if immutable:
560 560 raise util.Abort(_("can't abort rebase due to immutable changesets %s")
561 561 % ', '.join(str(repo[r]) for r in immutable),
562 562 hint=_('see hg help phases for details'))
563 563
564 564 descendants = set()
565 565 if dstates:
566 566 descendants = set(repo.changelog.descendants(dstates))
567 567 if descendants - set(dstates):
568 568 repo.ui.warn(_("warning: new changesets detected on target branch, "
569 569 "can't abort\n"))
570 570 return -1
571 571 else:
572 572 # Strip from the first rebased revision
573 573 merge.update(repo, repo[originalwd].rev(), False, True, False)
574 574 rebased = filter(lambda x: x > -1 and x != target, state.values())
575 575 if rebased:
576 576 strippoint = min(rebased)
577 577 # no backup of rebased cset versions needed
578 578 repair.strip(repo.ui, repo, repo[strippoint].node())
579 579 clearstatus(repo)
580 580 repo.ui.warn(_('rebase aborted\n'))
581 581 return 0
582 582
583 583 def buildstate(repo, dest, rebaseset, collapse):
584 584 '''Define which revisions are going to be rebased and where
585 585
586 586 repo: repo
587 587 dest: context
588 588 rebaseset: set of rev
589 589 '''
590 590
591 591 # This check isn't strictly necessary, since mq detects commits over an
592 592 # applied patch. But it prevents messing up the working directory when
593 593 # a partially completed rebase is blocked by mq.
594 594 if 'qtip' in repo.tags() and (dest.node() in
595 595 [s.node for s in repo.mq.applied]):
596 596 raise util.Abort(_('cannot rebase onto an applied mq patch'))
597 597
598 598 roots = list(repo.set('roots(%ld)', rebaseset))
599 599 if not roots:
600 600 raise util.Abort(_('no matching revisions'))
601 601 if len(roots) > 1:
602 602 raise util.Abort(_("can't rebase multiple roots"))
603 603 root = roots[0]
604 604
605 605 commonbase = root.ancestor(dest)
606 606 if commonbase == root:
607 607 raise util.Abort(_('source is ancestor of destination'))
608 608 if commonbase == dest:
609 609 samebranch = root.branch() == dest.branch()
610 610 if not collapse and samebranch and root in dest.children():
611 611 repo.ui.debug('source is a child of destination\n')
612 612 return None
613 613
614 614 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
615 615 state = dict.fromkeys(rebaseset, nullrev)
616 616 # Rebase tries to turn <dest> into a parent of <root> while
617 617 # preserving the number of parents of rebased changesets:
618 618 #
619 619 # - A changeset with a single parent will always be rebased as a
620 620 # changeset with a single parent.
621 621 #
622 622 # - A merge will be rebased as merge unless its parents are both
623 623 # ancestors of <dest> or are themselves in the rebased set and
624 624 # pruned while rebased.
625 625 #
626 626 # If one parent of <root> is an ancestor of <dest>, the rebased
627 627 # version of this parent will be <dest>. This is always true with
628 628 # --base option.
629 629 #
630 630 # Otherwise, we need to *replace* the original parents with
631 631 # <dest>. This "detaches" the rebased set from its former location
632 632 # and rebases it onto <dest>. Changes introduced by ancestors of
633 633 # <root> not common with <dest> (the detachset, marked as
634 634 # nullmerge) are "removed" from the rebased changesets.
635 635 #
636 636 # - If <root> has a single parent, set it to <dest>.
637 637 #
638 638 # - If <root> is a merge, we cannot decide which parent to
639 639 # replace, the rebase operation is not clearly defined.
640 640 #
641 641 # The table below sums up this behavior:
642 642 #
643 643 # +--------------------+----------------------+-------------------------+
644 644 # | | one parent | merge |
645 645 # +--------------------+----------------------+-------------------------+
646 646 # | parent in ::<dest> | new parent is <dest> | parents in ::<dest> are |
647 647 # | | | remapped to <dest> |
648 648 # +--------------------+----------------------+-------------------------+
649 649 # | unrelated source | new parent is <dest> | ambiguous, abort |
650 650 # +--------------------+----------------------+-------------------------+
651 651 #
652 652 # The actual abort is handled by `defineparents`
653 653 if len(root.parents()) <= 1:
654 654 # (strict) ancestors of <root> not ancestors of <dest>
655 655 detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root)
656 656 state.update(dict.fromkeys(detachset, nullmerge))
657 657 return repo['.'].rev(), dest.rev(), state
658 658
659 659 def clearrebased(ui, repo, state):
660 660 """dispose of rebased revision at the end of the rebase"""
661 rebased = [rev for rev in state if state[rev] != nullmerge]
662 if rebased:
663 if set(repo.changelog.descendants([min(rebased)])) - set(state):
664 ui.warn(_("warning: new changesets detected "
665 "on source branch, not stripping\n"))
666 else:
667 # backup the old csets by default
668 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
661 if obsolete._enabled:
662 markers = []
663 for rev, newrev in sorted(state.items()):
664 if newrev >= 0:
665 markers.append((repo[rev], (repo[newrev],)))
666 if markers:
667 obsolete.createmarkers(repo, markers)
668 else:
669 rebased = [rev for rev in state if state[rev] != nullmerge]
670 if rebased:
671 if set(repo.changelog.descendants([min(rebased)])) - set(state):
672 ui.warn(_("warning: new changesets detected "
673 "on source branch, not stripping\n"))
674 else:
675 # backup the old csets by default
676 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
669 677
670 678
671 679 def pullrebase(orig, ui, repo, *args, **opts):
672 680 'Call rebase after pull if the latter has been invoked with --rebase'
673 681 if opts.get('rebase'):
674 682 if opts.get('update'):
675 683 del opts['update']
676 684 ui.debug('--update and --rebase are not compatible, ignoring '
677 685 'the update flag\n')
678 686
679 687 movemarkfrom = repo['.'].node()
680 688 cmdutil.bailifchanged(repo)
681 689 revsprepull = len(repo)
682 690 origpostincoming = commands.postincoming
683 691 def _dummy(*args, **kwargs):
684 692 pass
685 693 commands.postincoming = _dummy
686 694 try:
687 695 orig(ui, repo, *args, **opts)
688 696 finally:
689 697 commands.postincoming = origpostincoming
690 698 revspostpull = len(repo)
691 699 if revspostpull > revsprepull:
692 700 rebase(ui, repo, **opts)
693 701 branch = repo[None].branch()
694 702 dest = repo[branch].rev()
695 703 if dest != repo['.'].rev():
696 704 # there was nothing to rebase we force an update
697 705 hg.update(repo, dest)
698 706 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
699 707 ui.status(_("updating bookmark %s\n")
700 708 % repo._bookmarkcurrent)
701 709 else:
702 710 if opts.get('tool'):
703 711 raise util.Abort(_('--tool can only be used with --rebase'))
704 712 orig(ui, repo, *args, **opts)
705 713
706 714 def uisetup(ui):
707 715 'Replace pull with a decorator to provide --rebase option'
708 716 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
709 717 entry[1].append(('', 'rebase', None,
710 718 _("rebase working directory to branch head")))
711 719 entry[1].append(('t', 'tool', '',
712 720 _("specify merge tool for rebase")))
General Comments 0
You need to be logged in to leave comments. Login now