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