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