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