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