##// END OF EJS Templates
rebase: reorder parent check and state storage...
Matt Mackall -
r19477:e9351f0d stable
parent child Browse files
Show More
@@ -1,800 +1,800 b''
1 # rebase.py - rebasing feature for mercurial
1 # rebase.py - rebasing feature for mercurial
2 #
2 #
3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to move sets of revisions to a different ancestor
8 '''command to move sets of revisions to a different ancestor
9
9
10 This extension lets you rebase changesets in an existing Mercurial
10 This extension lets you rebase changesets in an existing Mercurial
11 repository.
11 repository.
12
12
13 For more information:
13 For more information:
14 http://mercurial.selenic.com/wiki/RebaseExtension
14 http://mercurial.selenic.com/wiki/RebaseExtension
15 '''
15 '''
16
16
17 from mercurial import hg, util, repair, merge, cmdutil, commands, bookmarks
17 from mercurial import hg, util, repair, merge, cmdutil, commands, bookmarks
18 from mercurial import extensions, patch, scmutil, phases, 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.bailifchanged(repo)
177 cmdutil.bailifchanged(repo)
178
178
179 if not destf:
179 if not destf:
180 # Destination defaults to the latest revision in the
180 # Destination defaults to the latest revision in the
181 # current branch
181 # current branch
182 branch = repo[None].branch()
182 branch = repo[None].branch()
183 dest = repo[branch]
183 dest = repo[branch]
184 else:
184 else:
185 dest = scmutil.revsingle(repo, destf)
185 dest = scmutil.revsingle(repo, destf)
186
186
187 if revf:
187 if revf:
188 rebaseset = repo.revs('%lr', revf)
188 rebaseset = repo.revs('%lr', revf)
189 elif srcf:
189 elif srcf:
190 src = scmutil.revrange(repo, [srcf])
190 src = scmutil.revrange(repo, [srcf])
191 rebaseset = repo.revs('(%ld)::', src)
191 rebaseset = repo.revs('(%ld)::', src)
192 else:
192 else:
193 base = scmutil.revrange(repo, [basef or '.'])
193 base = scmutil.revrange(repo, [basef or '.'])
194 rebaseset = repo.revs(
194 rebaseset = repo.revs(
195 '(children(ancestor(%ld, %d)) and ::(%ld))::',
195 '(children(ancestor(%ld, %d)) and ::(%ld))::',
196 base, dest, base)
196 base, dest, base)
197 if rebaseset:
197 if rebaseset:
198 root = min(rebaseset)
198 root = min(rebaseset)
199 else:
199 else:
200 root = None
200 root = None
201
201
202 if not rebaseset:
202 if not rebaseset:
203 repo.ui.debug('base is ancestor of destination\n')
203 repo.ui.debug('base is ancestor of destination\n')
204 result = None
204 result = None
205 elif (not (keepf or obsolete._enabled)
205 elif (not (keepf or obsolete._enabled)
206 and repo.revs('first(children(%ld) - %ld)',
206 and repo.revs('first(children(%ld) - %ld)',
207 rebaseset, rebaseset)):
207 rebaseset, rebaseset)):
208 raise util.Abort(
208 raise util.Abort(
209 _("can't remove original changesets with"
209 _("can't remove original changesets with"
210 " unrebased descendants"),
210 " unrebased descendants"),
211 hint=_('use --keep to keep original changesets'))
211 hint=_('use --keep to keep original changesets'))
212 else:
212 else:
213 result = buildstate(repo, dest, rebaseset, collapsef)
213 result = buildstate(repo, dest, rebaseset, collapsef)
214
214
215 if not result:
215 if not result:
216 # Empty state built, nothing to rebase
216 # Empty state built, nothing to rebase
217 ui.status(_('nothing to rebase\n'))
217 ui.status(_('nothing to rebase\n'))
218 return 1
218 return 1
219 elif not keepf and not repo[root].mutable():
219 elif not keepf and not repo[root].mutable():
220 raise util.Abort(_("can't rebase immutable changeset %s")
220 raise util.Abort(_("can't rebase immutable changeset %s")
221 % repo[root],
221 % repo[root],
222 hint=_('see hg help phases for details'))
222 hint=_('see hg help phases for details'))
223 else:
223 else:
224 originalwd, target, state = result
224 originalwd, target, state = result
225 if collapsef:
225 if collapsef:
226 targetancestors = repo.changelog.ancestors([target],
226 targetancestors = repo.changelog.ancestors([target],
227 inclusive=True)
227 inclusive=True)
228 external = checkexternal(repo, state, targetancestors)
228 external = checkexternal(repo, state, targetancestors)
229
229
230 if keepbranchesf:
230 if keepbranchesf:
231 assert not extrafn, 'cannot use both keepbranches and extrafn'
231 assert not extrafn, 'cannot use both keepbranches and extrafn'
232 def extrafn(ctx, extra):
232 def extrafn(ctx, extra):
233 extra['branch'] = ctx.branch()
233 extra['branch'] = ctx.branch()
234 if collapsef:
234 if collapsef:
235 branches = set()
235 branches = set()
236 for rev in state:
236 for rev in state:
237 branches.add(repo[rev].branch())
237 branches.add(repo[rev].branch())
238 if len(branches) > 1:
238 if len(branches) > 1:
239 raise util.Abort(_('cannot collapse multiple named '
239 raise util.Abort(_('cannot collapse multiple named '
240 'branches'))
240 'branches'))
241
241
242
242
243 # Rebase
243 # Rebase
244 if not targetancestors:
244 if not targetancestors:
245 targetancestors = repo.changelog.ancestors([target], inclusive=True)
245 targetancestors = repo.changelog.ancestors([target], inclusive=True)
246
246
247 # Keep track of the current bookmarks in order to reset them later
247 # Keep track of the current bookmarks in order to reset them later
248 currentbookmarks = repo._bookmarks.copy()
248 currentbookmarks = repo._bookmarks.copy()
249 activebookmark = activebookmark or repo._bookmarkcurrent
249 activebookmark = activebookmark or repo._bookmarkcurrent
250 if activebookmark:
250 if activebookmark:
251 bookmarks.unsetcurrent(repo)
251 bookmarks.unsetcurrent(repo)
252
252
253 sortedstate = sorted(state)
253 sortedstate = sorted(state)
254 total = len(sortedstate)
254 total = len(sortedstate)
255 pos = 0
255 pos = 0
256 for rev in sortedstate:
256 for rev in sortedstate:
257 pos += 1
257 pos += 1
258 if state[rev] == -1:
258 if state[rev] == -1:
259 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
259 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
260 _('changesets'), total)
260 _('changesets'), total)
261 p1, p2 = defineparents(repo, rev, target, state,
262 targetancestors)
261 storestatus(repo, originalwd, target, state, collapsef, keepf,
263 storestatus(repo, originalwd, target, state, collapsef, keepf,
262 keepbranchesf, external, activebookmark)
264 keepbranchesf, external, activebookmark)
263 p1, p2 = defineparents(repo, rev, target, state,
264 targetancestors)
265 if len(repo.parents()) == 2:
265 if len(repo.parents()) == 2:
266 repo.ui.debug('resuming interrupted rebase\n')
266 repo.ui.debug('resuming interrupted rebase\n')
267 else:
267 else:
268 try:
268 try:
269 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
269 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
270 stats = rebasenode(repo, rev, p1, state, collapsef)
270 stats = rebasenode(repo, rev, p1, state, collapsef)
271 if stats and stats[3] > 0:
271 if stats and stats[3] > 0:
272 raise error.InterventionRequired(
272 raise error.InterventionRequired(
273 _('unresolved conflicts (see hg '
273 _('unresolved conflicts (see hg '
274 'resolve, then hg rebase --continue)'))
274 'resolve, then hg rebase --continue)'))
275 finally:
275 finally:
276 ui.setconfig('ui', 'forcemerge', '')
276 ui.setconfig('ui', 'forcemerge', '')
277 cmdutil.duplicatecopies(repo, rev, target)
277 cmdutil.duplicatecopies(repo, rev, target)
278 if not collapsef:
278 if not collapsef:
279 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
279 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
280 editor=editor)
280 editor=editor)
281 else:
281 else:
282 # Skip commit if we are collapsing
282 # Skip commit if we are collapsing
283 repo.setparents(repo[p1].node())
283 repo.setparents(repo[p1].node())
284 newrev = None
284 newrev = None
285 # Update the state
285 # Update the state
286 if newrev is not None:
286 if newrev is not None:
287 state[rev] = repo[newrev].rev()
287 state[rev] = repo[newrev].rev()
288 else:
288 else:
289 if not collapsef:
289 if not collapsef:
290 ui.note(_('no changes, revision %d skipped\n') % rev)
290 ui.note(_('no changes, revision %d skipped\n') % rev)
291 ui.debug('next revision set to %s\n' % p1)
291 ui.debug('next revision set to %s\n' % p1)
292 skipped.add(rev)
292 skipped.add(rev)
293 state[rev] = p1
293 state[rev] = p1
294
294
295 ui.progress(_('rebasing'), None)
295 ui.progress(_('rebasing'), None)
296 ui.note(_('rebase merging completed\n'))
296 ui.note(_('rebase merging completed\n'))
297
297
298 if collapsef and not keepopen:
298 if collapsef and not keepopen:
299 p1, p2 = defineparents(repo, min(state), target,
299 p1, p2 = defineparents(repo, min(state), target,
300 state, targetancestors)
300 state, targetancestors)
301 if collapsemsg:
301 if collapsemsg:
302 commitmsg = collapsemsg
302 commitmsg = collapsemsg
303 else:
303 else:
304 commitmsg = 'Collapsed revision'
304 commitmsg = 'Collapsed revision'
305 for rebased in state:
305 for rebased in state:
306 if rebased not in skipped and state[rebased] > nullmerge:
306 if rebased not in skipped and state[rebased] > nullmerge:
307 commitmsg += '\n* %s' % repo[rebased].description()
307 commitmsg += '\n* %s' % repo[rebased].description()
308 commitmsg = ui.edit(commitmsg, repo.ui.username())
308 commitmsg = ui.edit(commitmsg, repo.ui.username())
309 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
309 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
310 extrafn=extrafn, editor=editor)
310 extrafn=extrafn, editor=editor)
311
311
312 if 'qtip' in repo.tags():
312 if 'qtip' in repo.tags():
313 updatemq(repo, state, skipped, **opts)
313 updatemq(repo, state, skipped, **opts)
314
314
315 if currentbookmarks:
315 if currentbookmarks:
316 # Nodeids are needed to reset bookmarks
316 # Nodeids are needed to reset bookmarks
317 nstate = {}
317 nstate = {}
318 for k, v in state.iteritems():
318 for k, v in state.iteritems():
319 if v > nullmerge:
319 if v > nullmerge:
320 nstate[repo[k].node()] = repo[v].node()
320 nstate[repo[k].node()] = repo[v].node()
321 # XXX this is the same as dest.node() for the non-continue path --
321 # XXX this is the same as dest.node() for the non-continue path --
322 # this should probably be cleaned up
322 # this should probably be cleaned up
323 targetnode = repo[target].node()
323 targetnode = repo[target].node()
324
324
325 if not keepf:
325 if not keepf:
326 collapsedas = None
326 collapsedas = None
327 if collapsef:
327 if collapsef:
328 collapsedas = newrev
328 collapsedas = newrev
329 clearrebased(ui, repo, state, skipped, collapsedas)
329 clearrebased(ui, repo, state, skipped, collapsedas)
330
330
331 if currentbookmarks:
331 if currentbookmarks:
332 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
332 updatebookmarks(repo, targetnode, nstate, currentbookmarks)
333
333
334 clearstatus(repo)
334 clearstatus(repo)
335 ui.note(_("rebase completed\n"))
335 ui.note(_("rebase completed\n"))
336 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
336 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
337 if skipped:
337 if skipped:
338 ui.note(_("%d revisions have been skipped\n") % len(skipped))
338 ui.note(_("%d revisions have been skipped\n") % len(skipped))
339
339
340 if (activebookmark and
340 if (activebookmark and
341 repo['tip'].node() == repo._bookmarks[activebookmark]):
341 repo['tip'].node() == repo._bookmarks[activebookmark]):
342 bookmarks.setcurrent(repo, activebookmark)
342 bookmarks.setcurrent(repo, activebookmark)
343
343
344 finally:
344 finally:
345 release(lock, wlock)
345 release(lock, wlock)
346
346
347 def checkexternal(repo, state, targetancestors):
347 def checkexternal(repo, state, targetancestors):
348 """Check whether one or more external revisions need to be taken in
348 """Check whether one or more external revisions need to be taken in
349 consideration. In the latter case, abort.
349 consideration. In the latter case, abort.
350 """
350 """
351 external = nullrev
351 external = nullrev
352 source = min(state)
352 source = min(state)
353 for rev in state:
353 for rev in state:
354 if rev == source:
354 if rev == source:
355 continue
355 continue
356 # Check externals and fail if there are more than one
356 # Check externals and fail if there are more than one
357 for p in repo[rev].parents():
357 for p in repo[rev].parents():
358 if (p.rev() not in state
358 if (p.rev() not in state
359 and p.rev() not in targetancestors):
359 and p.rev() not in targetancestors):
360 if external != nullrev:
360 if external != nullrev:
361 raise util.Abort(_('unable to collapse, there is more '
361 raise util.Abort(_('unable to collapse, there is more '
362 'than one external parent'))
362 'than one external parent'))
363 external = p.rev()
363 external = p.rev()
364 return external
364 return external
365
365
366 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
366 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
367 'Commit the changes and store useful information in extra'
367 'Commit the changes and store useful information in extra'
368 try:
368 try:
369 repo.setparents(repo[p1].node(), repo[p2].node())
369 repo.setparents(repo[p1].node(), repo[p2].node())
370 ctx = repo[rev]
370 ctx = repo[rev]
371 if commitmsg is None:
371 if commitmsg is None:
372 commitmsg = ctx.description()
372 commitmsg = ctx.description()
373 extra = {'rebase_source': ctx.hex()}
373 extra = {'rebase_source': ctx.hex()}
374 if extrafn:
374 if extrafn:
375 extrafn(ctx, extra)
375 extrafn(ctx, extra)
376 # Commit might fail if unresolved files exist
376 # Commit might fail if unresolved files exist
377 newrev = repo.commit(text=commitmsg, user=ctx.user(),
377 newrev = repo.commit(text=commitmsg, user=ctx.user(),
378 date=ctx.date(), extra=extra, editor=editor)
378 date=ctx.date(), extra=extra, editor=editor)
379 repo.dirstate.setbranch(repo[newrev].branch())
379 repo.dirstate.setbranch(repo[newrev].branch())
380 targetphase = max(ctx.phase(), phases.draft)
380 targetphase = max(ctx.phase(), phases.draft)
381 # retractboundary doesn't overwrite upper phase inherited from parent
381 # retractboundary doesn't overwrite upper phase inherited from parent
382 newnode = repo[newrev].node()
382 newnode = repo[newrev].node()
383 if newnode:
383 if newnode:
384 phases.retractboundary(repo, targetphase, [newnode])
384 phases.retractboundary(repo, targetphase, [newnode])
385 return newrev
385 return newrev
386 except util.Abort:
386 except util.Abort:
387 # Invalidate the previous setparents
387 # Invalidate the previous setparents
388 repo.dirstate.invalidate()
388 repo.dirstate.invalidate()
389 raise
389 raise
390
390
391 def rebasenode(repo, rev, p1, state, collapse):
391 def rebasenode(repo, rev, p1, state, collapse):
392 'Rebase a single revision'
392 'Rebase a single revision'
393 # Merge phase
393 # Merge phase
394 # Update to target and merge it with local
394 # Update to target and merge it with local
395 if repo['.'].rev() != repo[p1].rev():
395 if repo['.'].rev() != repo[p1].rev():
396 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
396 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
397 merge.update(repo, p1, False, True, False)
397 merge.update(repo, p1, False, True, False)
398 else:
398 else:
399 repo.ui.debug(" already in target\n")
399 repo.ui.debug(" already in target\n")
400 repo.dirstate.write()
400 repo.dirstate.write()
401 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
401 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
402 base = None
402 base = None
403 if repo[rev].rev() != repo[min(state)].rev():
403 if repo[rev].rev() != repo[min(state)].rev():
404 base = repo[rev].p1().node()
404 base = repo[rev].p1().node()
405 # When collapsing in-place, the parent is the common ancestor, we
405 # When collapsing in-place, the parent is the common ancestor, we
406 # have to allow merging with it.
406 # have to allow merging with it.
407 return merge.update(repo, rev, True, True, False, base, collapse)
407 return merge.update(repo, rev, True, True, False, base, collapse)
408
408
409 def nearestrebased(repo, rev, state):
409 def nearestrebased(repo, rev, state):
410 """return the nearest ancestors of rev in the rebase result"""
410 """return the nearest ancestors of rev in the rebase result"""
411 rebased = [r for r in state if state[r] > nullmerge]
411 rebased = [r for r in state if state[r] > nullmerge]
412 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
412 candidates = repo.revs('max(%ld and (::%d))', rebased, rev)
413 if candidates:
413 if candidates:
414 return state[candidates[0]]
414 return state[candidates[0]]
415 else:
415 else:
416 return None
416 return None
417
417
418 def defineparents(repo, rev, target, state, targetancestors):
418 def defineparents(repo, rev, target, state, targetancestors):
419 'Return the new parent relationship of the revision that will be rebased'
419 'Return the new parent relationship of the revision that will be rebased'
420 parents = repo[rev].parents()
420 parents = repo[rev].parents()
421 p1 = p2 = nullrev
421 p1 = p2 = nullrev
422
422
423 P1n = parents[0].rev()
423 P1n = parents[0].rev()
424 if P1n in targetancestors:
424 if P1n in targetancestors:
425 p1 = target
425 p1 = target
426 elif P1n in state:
426 elif P1n in state:
427 if state[P1n] == nullmerge:
427 if state[P1n] == nullmerge:
428 p1 = target
428 p1 = target
429 elif state[P1n] == revignored:
429 elif state[P1n] == revignored:
430 p1 = nearestrebased(repo, P1n, state)
430 p1 = nearestrebased(repo, P1n, state)
431 if p1 is None:
431 if p1 is None:
432 p1 = target
432 p1 = target
433 else:
433 else:
434 p1 = state[P1n]
434 p1 = state[P1n]
435 else: # P1n external
435 else: # P1n external
436 p1 = target
436 p1 = target
437 p2 = P1n
437 p2 = P1n
438
438
439 if len(parents) == 2 and parents[1].rev() not in targetancestors:
439 if len(parents) == 2 and parents[1].rev() not in targetancestors:
440 P2n = parents[1].rev()
440 P2n = parents[1].rev()
441 # interesting second parent
441 # interesting second parent
442 if P2n in state:
442 if P2n in state:
443 if p1 == target: # P1n in targetancestors or external
443 if p1 == target: # P1n in targetancestors or external
444 p1 = state[P2n]
444 p1 = state[P2n]
445 elif state[P2n] == revignored:
445 elif state[P2n] == revignored:
446 p2 = nearestrebased(repo, P2n, state)
446 p2 = nearestrebased(repo, P2n, state)
447 if p2 is None:
447 if p2 is None:
448 # no ancestors rebased yet, detach
448 # no ancestors rebased yet, detach
449 p2 = target
449 p2 = target
450 else:
450 else:
451 p2 = state[P2n]
451 p2 = state[P2n]
452 else: # P2n external
452 else: # P2n external
453 if p2 != nullrev: # P1n external too => rev is a merged revision
453 if p2 != nullrev: # P1n external too => rev is a merged revision
454 raise util.Abort(_('cannot use revision %d as base, result '
454 raise util.Abort(_('cannot use revision %d as base, result '
455 'would have 3 parents') % rev)
455 'would have 3 parents') % rev)
456 p2 = P2n
456 p2 = P2n
457 repo.ui.debug(" future parents are %d and %d\n" %
457 repo.ui.debug(" future parents are %d and %d\n" %
458 (repo[p1].rev(), repo[p2].rev()))
458 (repo[p1].rev(), repo[p2].rev()))
459 return p1, p2
459 return p1, p2
460
460
461 def isagitpatch(repo, patchname):
461 def isagitpatch(repo, patchname):
462 'Return true if the given patch is in git format'
462 'Return true if the given patch is in git format'
463 mqpatch = os.path.join(repo.mq.path, patchname)
463 mqpatch = os.path.join(repo.mq.path, patchname)
464 for line in patch.linereader(file(mqpatch, 'rb')):
464 for line in patch.linereader(file(mqpatch, 'rb')):
465 if line.startswith('diff --git'):
465 if line.startswith('diff --git'):
466 return True
466 return True
467 return False
467 return False
468
468
469 def updatemq(repo, state, skipped, **opts):
469 def updatemq(repo, state, skipped, **opts):
470 'Update rebased mq patches - finalize and then import them'
470 'Update rebased mq patches - finalize and then import them'
471 mqrebase = {}
471 mqrebase = {}
472 mq = repo.mq
472 mq = repo.mq
473 original_series = mq.fullseries[:]
473 original_series = mq.fullseries[:]
474 skippedpatches = set()
474 skippedpatches = set()
475
475
476 for p in mq.applied:
476 for p in mq.applied:
477 rev = repo[p.node].rev()
477 rev = repo[p.node].rev()
478 if rev in state:
478 if rev in state:
479 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
479 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
480 (rev, p.name))
480 (rev, p.name))
481 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
481 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
482 else:
482 else:
483 # Applied but not rebased, not sure this should happen
483 # Applied but not rebased, not sure this should happen
484 skippedpatches.add(p.name)
484 skippedpatches.add(p.name)
485
485
486 if mqrebase:
486 if mqrebase:
487 mq.finish(repo, mqrebase.keys())
487 mq.finish(repo, mqrebase.keys())
488
488
489 # We must start import from the newest revision
489 # We must start import from the newest revision
490 for rev in sorted(mqrebase, reverse=True):
490 for rev in sorted(mqrebase, reverse=True):
491 if rev not in skipped:
491 if rev not in skipped:
492 name, isgit = mqrebase[rev]
492 name, isgit = mqrebase[rev]
493 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
493 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
494 mq.qimport(repo, (), patchname=name, git=isgit,
494 mq.qimport(repo, (), patchname=name, git=isgit,
495 rev=[str(state[rev])])
495 rev=[str(state[rev])])
496 else:
496 else:
497 # Rebased and skipped
497 # Rebased and skipped
498 skippedpatches.add(mqrebase[rev][0])
498 skippedpatches.add(mqrebase[rev][0])
499
499
500 # Patches were either applied and rebased and imported in
500 # Patches were either applied and rebased and imported in
501 # order, applied and removed or unapplied. Discard the removed
501 # order, applied and removed or unapplied. Discard the removed
502 # ones while preserving the original series order and guards.
502 # ones while preserving the original series order and guards.
503 newseries = [s for s in original_series
503 newseries = [s for s in original_series
504 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
504 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
505 mq.fullseries[:] = newseries
505 mq.fullseries[:] = newseries
506 mq.seriesdirty = True
506 mq.seriesdirty = True
507 mq.savedirty()
507 mq.savedirty()
508
508
509 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
509 def updatebookmarks(repo, targetnode, nstate, originalbookmarks):
510 'Move bookmarks to their correct changesets, and delete divergent ones'
510 'Move bookmarks to their correct changesets, and delete divergent ones'
511 marks = repo._bookmarks
511 marks = repo._bookmarks
512 for k, v in originalbookmarks.iteritems():
512 for k, v in originalbookmarks.iteritems():
513 if v in nstate:
513 if v in nstate:
514 # update the bookmarks for revs that have moved
514 # update the bookmarks for revs that have moved
515 marks[k] = nstate[v]
515 marks[k] = nstate[v]
516 bookmarks.deletedivergent(repo, [targetnode], k)
516 bookmarks.deletedivergent(repo, [targetnode], k)
517
517
518 marks.write()
518 marks.write()
519
519
520 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
520 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
521 external, activebookmark):
521 external, activebookmark):
522 'Store the current status to allow recovery'
522 'Store the current status to allow recovery'
523 f = repo.opener("rebasestate", "w")
523 f = repo.opener("rebasestate", "w")
524 f.write(repo[originalwd].hex() + '\n')
524 f.write(repo[originalwd].hex() + '\n')
525 f.write(repo[target].hex() + '\n')
525 f.write(repo[target].hex() + '\n')
526 f.write(repo[external].hex() + '\n')
526 f.write(repo[external].hex() + '\n')
527 f.write('%d\n' % int(collapse))
527 f.write('%d\n' % int(collapse))
528 f.write('%d\n' % int(keep))
528 f.write('%d\n' % int(keep))
529 f.write('%d\n' % int(keepbranches))
529 f.write('%d\n' % int(keepbranches))
530 f.write('%s\n' % (activebookmark or ''))
530 f.write('%s\n' % (activebookmark or ''))
531 for d, v in state.iteritems():
531 for d, v in state.iteritems():
532 oldrev = repo[d].hex()
532 oldrev = repo[d].hex()
533 if v > nullmerge:
533 if v > nullmerge:
534 newrev = repo[v].hex()
534 newrev = repo[v].hex()
535 else:
535 else:
536 newrev = v
536 newrev = v
537 f.write("%s:%s\n" % (oldrev, newrev))
537 f.write("%s:%s\n" % (oldrev, newrev))
538 f.close()
538 f.close()
539 repo.ui.debug('rebase status stored\n')
539 repo.ui.debug('rebase status stored\n')
540
540
541 def clearstatus(repo):
541 def clearstatus(repo):
542 'Remove the status files'
542 'Remove the status files'
543 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
543 util.unlinkpath(repo.join("rebasestate"), ignoremissing=True)
544
544
545 def restorestatus(repo):
545 def restorestatus(repo):
546 'Restore a previously stored status'
546 'Restore a previously stored status'
547 try:
547 try:
548 target = None
548 target = None
549 collapse = False
549 collapse = False
550 external = nullrev
550 external = nullrev
551 activebookmark = None
551 activebookmark = None
552 state = {}
552 state = {}
553 f = repo.opener("rebasestate")
553 f = repo.opener("rebasestate")
554 for i, l in enumerate(f.read().splitlines()):
554 for i, l in enumerate(f.read().splitlines()):
555 if i == 0:
555 if i == 0:
556 originalwd = repo[l].rev()
556 originalwd = repo[l].rev()
557 elif i == 1:
557 elif i == 1:
558 target = repo[l].rev()
558 target = repo[l].rev()
559 elif i == 2:
559 elif i == 2:
560 external = repo[l].rev()
560 external = repo[l].rev()
561 elif i == 3:
561 elif i == 3:
562 collapse = bool(int(l))
562 collapse = bool(int(l))
563 elif i == 4:
563 elif i == 4:
564 keep = bool(int(l))
564 keep = bool(int(l))
565 elif i == 5:
565 elif i == 5:
566 keepbranches = bool(int(l))
566 keepbranches = bool(int(l))
567 elif i == 6 and not (len(l) == 81 and ':' in l):
567 elif i == 6 and not (len(l) == 81 and ':' in l):
568 # line 6 is a recent addition, so for backwards compatibility
568 # line 6 is a recent addition, so for backwards compatibility
569 # check that the line doesn't look like the oldrev:newrev lines
569 # check that the line doesn't look like the oldrev:newrev lines
570 activebookmark = l
570 activebookmark = l
571 else:
571 else:
572 oldrev, newrev = l.split(':')
572 oldrev, newrev = l.split(':')
573 if newrev in (str(nullmerge), str(revignored)):
573 if newrev in (str(nullmerge), str(revignored)):
574 state[repo[oldrev].rev()] = int(newrev)
574 state[repo[oldrev].rev()] = int(newrev)
575 else:
575 else:
576 state[repo[oldrev].rev()] = repo[newrev].rev()
576 state[repo[oldrev].rev()] = repo[newrev].rev()
577 skipped = set()
577 skipped = set()
578 # recompute the set of skipped revs
578 # recompute the set of skipped revs
579 if not collapse:
579 if not collapse:
580 seen = set([target])
580 seen = set([target])
581 for old, new in sorted(state.items()):
581 for old, new in sorted(state.items()):
582 if new != nullrev and new in seen:
582 if new != nullrev and new in seen:
583 skipped.add(old)
583 skipped.add(old)
584 seen.add(new)
584 seen.add(new)
585 repo.ui.debug('computed skipped revs: %s\n' % skipped)
585 repo.ui.debug('computed skipped revs: %s\n' % skipped)
586 repo.ui.debug('rebase status resumed\n')
586 repo.ui.debug('rebase status resumed\n')
587 return (originalwd, target, state, skipped,
587 return (originalwd, target, state, skipped,
588 collapse, keep, keepbranches, external, activebookmark)
588 collapse, keep, keepbranches, external, activebookmark)
589 except IOError, err:
589 except IOError, err:
590 if err.errno != errno.ENOENT:
590 if err.errno != errno.ENOENT:
591 raise
591 raise
592 raise util.Abort(_('no rebase in progress'))
592 raise util.Abort(_('no rebase in progress'))
593
593
594 def abort(repo, originalwd, target, state):
594 def abort(repo, originalwd, target, state):
595 'Restore the repository to its original state'
595 'Restore the repository to its original state'
596 dstates = [s for s in state.values() if s != nullrev]
596 dstates = [s for s in state.values() if s != nullrev]
597 immutable = [d for d in dstates if not repo[d].mutable()]
597 immutable = [d for d in dstates if not repo[d].mutable()]
598 if immutable:
598 if immutable:
599 raise util.Abort(_("can't abort rebase due to immutable changesets %s")
599 raise util.Abort(_("can't abort rebase due to immutable changesets %s")
600 % ', '.join(str(repo[r]) for r in immutable),
600 % ', '.join(str(repo[r]) for r in immutable),
601 hint=_('see hg help phases for details'))
601 hint=_('see hg help phases for details'))
602
602
603 descendants = set()
603 descendants = set()
604 if dstates:
604 if dstates:
605 descendants = set(repo.changelog.descendants(dstates))
605 descendants = set(repo.changelog.descendants(dstates))
606 if descendants - set(dstates):
606 if descendants - set(dstates):
607 repo.ui.warn(_("warning: new changesets detected on target branch, "
607 repo.ui.warn(_("warning: new changesets detected on target branch, "
608 "can't abort\n"))
608 "can't abort\n"))
609 return -1
609 return -1
610 else:
610 else:
611 # Strip from the first rebased revision
611 # Strip from the first rebased revision
612 merge.update(repo, repo[originalwd].rev(), False, True, False)
612 merge.update(repo, repo[originalwd].rev(), False, True, False)
613 rebased = filter(lambda x: x > -1 and x != target, state.values())
613 rebased = filter(lambda x: x > -1 and x != target, state.values())
614 if rebased:
614 if rebased:
615 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
615 strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
616 # no backup of rebased cset versions needed
616 # no backup of rebased cset versions needed
617 repair.strip(repo.ui, repo, strippoints)
617 repair.strip(repo.ui, repo, strippoints)
618 clearstatus(repo)
618 clearstatus(repo)
619 repo.ui.warn(_('rebase aborted\n'))
619 repo.ui.warn(_('rebase aborted\n'))
620 return 0
620 return 0
621
621
622 def buildstate(repo, dest, rebaseset, collapse):
622 def buildstate(repo, dest, rebaseset, collapse):
623 '''Define which revisions are going to be rebased and where
623 '''Define which revisions are going to be rebased and where
624
624
625 repo: repo
625 repo: repo
626 dest: context
626 dest: context
627 rebaseset: set of rev
627 rebaseset: set of rev
628 '''
628 '''
629
629
630 # This check isn't strictly necessary, since mq detects commits over an
630 # This check isn't strictly necessary, since mq detects commits over an
631 # applied patch. But it prevents messing up the working directory when
631 # applied patch. But it prevents messing up the working directory when
632 # a partially completed rebase is blocked by mq.
632 # a partially completed rebase is blocked by mq.
633 if 'qtip' in repo.tags() and (dest.node() in
633 if 'qtip' in repo.tags() and (dest.node() in
634 [s.node for s in repo.mq.applied]):
634 [s.node for s in repo.mq.applied]):
635 raise util.Abort(_('cannot rebase onto an applied mq patch'))
635 raise util.Abort(_('cannot rebase onto an applied mq patch'))
636
636
637 roots = list(repo.set('roots(%ld)', rebaseset))
637 roots = list(repo.set('roots(%ld)', rebaseset))
638 if not roots:
638 if not roots:
639 raise util.Abort(_('no matching revisions'))
639 raise util.Abort(_('no matching revisions'))
640 roots.sort()
640 roots.sort()
641 state = {}
641 state = {}
642 detachset = set()
642 detachset = set()
643 for root in roots:
643 for root in roots:
644 commonbase = root.ancestor(dest)
644 commonbase = root.ancestor(dest)
645 if commonbase == root:
645 if commonbase == root:
646 raise util.Abort(_('source is ancestor of destination'))
646 raise util.Abort(_('source is ancestor of destination'))
647 if commonbase == dest:
647 if commonbase == dest:
648 samebranch = root.branch() == dest.branch()
648 samebranch = root.branch() == dest.branch()
649 if not collapse and samebranch and root in dest.children():
649 if not collapse and samebranch and root in dest.children():
650 repo.ui.debug('source is a child of destination\n')
650 repo.ui.debug('source is a child of destination\n')
651 return None
651 return None
652
652
653 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))
653 repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))
654 state.update(dict.fromkeys(rebaseset, nullrev))
654 state.update(dict.fromkeys(rebaseset, nullrev))
655 # Rebase tries to turn <dest> into a parent of <root> while
655 # Rebase tries to turn <dest> into a parent of <root> while
656 # preserving the number of parents of rebased changesets:
656 # preserving the number of parents of rebased changesets:
657 #
657 #
658 # - A changeset with a single parent will always be rebased as a
658 # - A changeset with a single parent will always be rebased as a
659 # changeset with a single parent.
659 # changeset with a single parent.
660 #
660 #
661 # - A merge will be rebased as merge unless its parents are both
661 # - A merge will be rebased as merge unless its parents are both
662 # ancestors of <dest> or are themselves in the rebased set and
662 # ancestors of <dest> or are themselves in the rebased set and
663 # pruned while rebased.
663 # pruned while rebased.
664 #
664 #
665 # If one parent of <root> is an ancestor of <dest>, the rebased
665 # If one parent of <root> is an ancestor of <dest>, the rebased
666 # version of this parent will be <dest>. This is always true with
666 # version of this parent will be <dest>. This is always true with
667 # --base option.
667 # --base option.
668 #
668 #
669 # Otherwise, we need to *replace* the original parents with
669 # Otherwise, we need to *replace* the original parents with
670 # <dest>. This "detaches" the rebased set from its former location
670 # <dest>. This "detaches" the rebased set from its former location
671 # and rebases it onto <dest>. Changes introduced by ancestors of
671 # and rebases it onto <dest>. Changes introduced by ancestors of
672 # <root> not common with <dest> (the detachset, marked as
672 # <root> not common with <dest> (the detachset, marked as
673 # nullmerge) are "removed" from the rebased changesets.
673 # nullmerge) are "removed" from the rebased changesets.
674 #
674 #
675 # - If <root> has a single parent, set it to <dest>.
675 # - If <root> has a single parent, set it to <dest>.
676 #
676 #
677 # - If <root> is a merge, we cannot decide which parent to
677 # - If <root> is a merge, we cannot decide which parent to
678 # replace, the rebase operation is not clearly defined.
678 # replace, the rebase operation is not clearly defined.
679 #
679 #
680 # The table below sums up this behavior:
680 # The table below sums up this behavior:
681 #
681 #
682 # +------------------+----------------------+-------------------------+
682 # +------------------+----------------------+-------------------------+
683 # | | one parent | merge |
683 # | | one parent | merge |
684 # +------------------+----------------------+-------------------------+
684 # +------------------+----------------------+-------------------------+
685 # | parent in | new parent is <dest> | parents in ::<dest> are |
685 # | parent in | new parent is <dest> | parents in ::<dest> are |
686 # | ::<dest> | | remapped to <dest> |
686 # | ::<dest> | | remapped to <dest> |
687 # +------------------+----------------------+-------------------------+
687 # +------------------+----------------------+-------------------------+
688 # | unrelated source | new parent is <dest> | ambiguous, abort |
688 # | unrelated source | new parent is <dest> | ambiguous, abort |
689 # +------------------+----------------------+-------------------------+
689 # +------------------+----------------------+-------------------------+
690 #
690 #
691 # The actual abort is handled by `defineparents`
691 # The actual abort is handled by `defineparents`
692 if len(root.parents()) <= 1:
692 if len(root.parents()) <= 1:
693 # ancestors of <root> not ancestors of <dest>
693 # ancestors of <root> not ancestors of <dest>
694 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
694 detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
695 [root.rev()]))
695 [root.rev()]))
696 for r in detachset:
696 for r in detachset:
697 if r not in state:
697 if r not in state:
698 state[r] = nullmerge
698 state[r] = nullmerge
699 if len(roots) > 1:
699 if len(roots) > 1:
700 # If we have multiple roots, we may have "hole" in the rebase set.
700 # If we have multiple roots, we may have "hole" in the rebase set.
701 # Rebase roots that descend from those "hole" should not be detached as
701 # Rebase roots that descend from those "hole" should not be detached as
702 # other root are. We use the special `revignored` to inform rebase that
702 # other root are. We use the special `revignored` to inform rebase that
703 # the revision should be ignored but that `defineparents` should search
703 # the revision should be ignored but that `defineparents` should search
704 # a rebase destination that make sense regarding rebased topology.
704 # a rebase destination that make sense regarding rebased topology.
705 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
705 rebasedomain = set(repo.revs('%ld::%ld', rebaseset, rebaseset))
706 for ignored in set(rebasedomain) - set(rebaseset):
706 for ignored in set(rebasedomain) - set(rebaseset):
707 state[ignored] = revignored
707 state[ignored] = revignored
708 return repo['.'].rev(), dest.rev(), state
708 return repo['.'].rev(), dest.rev(), state
709
709
710 def clearrebased(ui, repo, state, skipped, collapsedas=None):
710 def clearrebased(ui, repo, state, skipped, collapsedas=None):
711 """dispose of rebased revision at the end of the rebase
711 """dispose of rebased revision at the end of the rebase
712
712
713 If `collapsedas` is not None, the rebase was a collapse whose result if the
713 If `collapsedas` is not None, the rebase was a collapse whose result if the
714 `collapsedas` node."""
714 `collapsedas` node."""
715 if obsolete._enabled:
715 if obsolete._enabled:
716 markers = []
716 markers = []
717 for rev, newrev in sorted(state.items()):
717 for rev, newrev in sorted(state.items()):
718 if newrev >= 0:
718 if newrev >= 0:
719 if rev in skipped:
719 if rev in skipped:
720 succs = ()
720 succs = ()
721 elif collapsedas is not None:
721 elif collapsedas is not None:
722 succs = (repo[collapsedas],)
722 succs = (repo[collapsedas],)
723 else:
723 else:
724 succs = (repo[newrev],)
724 succs = (repo[newrev],)
725 markers.append((repo[rev], succs))
725 markers.append((repo[rev], succs))
726 if markers:
726 if markers:
727 obsolete.createmarkers(repo, markers)
727 obsolete.createmarkers(repo, markers)
728 else:
728 else:
729 rebased = [rev for rev in state if state[rev] > nullmerge]
729 rebased = [rev for rev in state if state[rev] > nullmerge]
730 if rebased:
730 if rebased:
731 stripped = []
731 stripped = []
732 for root in repo.set('roots(%ld)', rebased):
732 for root in repo.set('roots(%ld)', rebased):
733 if set(repo.changelog.descendants([root.rev()])) - set(state):
733 if set(repo.changelog.descendants([root.rev()])) - set(state):
734 ui.warn(_("warning: new changesets detected "
734 ui.warn(_("warning: new changesets detected "
735 "on source branch, not stripping\n"))
735 "on source branch, not stripping\n"))
736 else:
736 else:
737 stripped.append(root.node())
737 stripped.append(root.node())
738 if stripped:
738 if stripped:
739 # backup the old csets by default
739 # backup the old csets by default
740 repair.strip(ui, repo, stripped, "all")
740 repair.strip(ui, repo, stripped, "all")
741
741
742
742
743 def pullrebase(orig, ui, repo, *args, **opts):
743 def pullrebase(orig, ui, repo, *args, **opts):
744 'Call rebase after pull if the latter has been invoked with --rebase'
744 'Call rebase after pull if the latter has been invoked with --rebase'
745 if opts.get('rebase'):
745 if opts.get('rebase'):
746 if opts.get('update'):
746 if opts.get('update'):
747 del opts['update']
747 del opts['update']
748 ui.debug('--update and --rebase are not compatible, ignoring '
748 ui.debug('--update and --rebase are not compatible, ignoring '
749 'the update flag\n')
749 'the update flag\n')
750
750
751 movemarkfrom = repo['.'].node()
751 movemarkfrom = repo['.'].node()
752 cmdutil.bailifchanged(repo)
752 cmdutil.bailifchanged(repo)
753 revsprepull = len(repo)
753 revsprepull = len(repo)
754 origpostincoming = commands.postincoming
754 origpostincoming = commands.postincoming
755 def _dummy(*args, **kwargs):
755 def _dummy(*args, **kwargs):
756 pass
756 pass
757 commands.postincoming = _dummy
757 commands.postincoming = _dummy
758 try:
758 try:
759 orig(ui, repo, *args, **opts)
759 orig(ui, repo, *args, **opts)
760 finally:
760 finally:
761 commands.postincoming = origpostincoming
761 commands.postincoming = origpostincoming
762 revspostpull = len(repo)
762 revspostpull = len(repo)
763 if revspostpull > revsprepull:
763 if revspostpull > revsprepull:
764 # --rev option from pull conflict with rebase own --rev
764 # --rev option from pull conflict with rebase own --rev
765 # dropping it
765 # dropping it
766 if 'rev' in opts:
766 if 'rev' in opts:
767 del opts['rev']
767 del opts['rev']
768 rebase(ui, repo, **opts)
768 rebase(ui, repo, **opts)
769 branch = repo[None].branch()
769 branch = repo[None].branch()
770 dest = repo[branch].rev()
770 dest = repo[branch].rev()
771 if dest != repo['.'].rev():
771 if dest != repo['.'].rev():
772 # there was nothing to rebase we force an update
772 # there was nothing to rebase we force an update
773 hg.update(repo, dest)
773 hg.update(repo, dest)
774 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
774 if bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
775 ui.status(_("updating bookmark %s\n")
775 ui.status(_("updating bookmark %s\n")
776 % repo._bookmarkcurrent)
776 % repo._bookmarkcurrent)
777 else:
777 else:
778 if opts.get('tool'):
778 if opts.get('tool'):
779 raise util.Abort(_('--tool can only be used with --rebase'))
779 raise util.Abort(_('--tool can only be used with --rebase'))
780 orig(ui, repo, *args, **opts)
780 orig(ui, repo, *args, **opts)
781
781
782 def summaryhook(ui, repo):
782 def summaryhook(ui, repo):
783 if not os.path.exists(repo.join('rebasestate')):
783 if not os.path.exists(repo.join('rebasestate')):
784 return
784 return
785 state = restorestatus(repo)[2]
785 state = restorestatus(repo)[2]
786 numrebased = len([i for i in state.itervalues() if i != -1])
786 numrebased = len([i for i in state.itervalues() if i != -1])
787 # i18n: column positioning for "hg summary"
787 # i18n: column positioning for "hg summary"
788 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
788 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
789 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
789 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
790 ui.label(_('%d remaining'), 'rebase.remaining') %
790 ui.label(_('%d remaining'), 'rebase.remaining') %
791 (len(state) - numrebased)))
791 (len(state) - numrebased)))
792
792
793 def uisetup(ui):
793 def uisetup(ui):
794 'Replace pull with a decorator to provide --rebase option'
794 'Replace pull with a decorator to provide --rebase option'
795 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
795 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
796 entry[1].append(('', 'rebase', None,
796 entry[1].append(('', 'rebase', None,
797 _("rebase working directory to branch head")))
797 _("rebase working directory to branch head")))
798 entry[1].append(('t', 'tool', '',
798 entry[1].append(('t', 'tool', '',
799 _("specify merge tool for rebase")))
799 _("specify merge tool for rebase")))
800 cmdutil.summaryhooks.add('rebase', summaryhook)
800 cmdutil.summaryhooks.add('rebase', summaryhook)
General Comments 0
You need to be logged in to leave comments. Login now