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