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