##// END OF EJS Templates
rebase: add a "D" short option for detach...
Pierre-Yves David -
r15737:8edd9f2c default
parent child Browse files
Show More
@@ -1,634 +1,634 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
18 from mercurial import extensions, patch
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
26
27 cmdtable = {}
27 cmdtable = {}
28 command = cmdutil.command(cmdtable)
28 command = cmdutil.command(cmdtable)
29
29
30 @command('rebase',
30 @command('rebase',
31 [('s', 'source', '',
31 [('s', 'source', '',
32 _('rebase from the specified changeset'), _('REV')),
32 _('rebase from the specified changeset'), _('REV')),
33 ('b', 'base', '',
33 ('b', 'base', '',
34 _('rebase from the base of the specified changeset '
34 _('rebase from the base of the specified changeset '
35 '(up to greatest common ancestor of base and dest)'),
35 '(up to greatest common ancestor of base and dest)'),
36 _('REV')),
36 _('REV')),
37 ('r', 'rev', [],
37 ('r', 'rev', [],
38 _('rebase these revisions'),
38 _('rebase these revisions'),
39 _('REV')),
39 _('REV')),
40 ('d', 'dest', '',
40 ('d', 'dest', '',
41 _('rebase onto the specified changeset'), _('REV')),
41 _('rebase onto the specified changeset'), _('REV')),
42 ('', 'collapse', False, _('collapse the rebased changesets')),
42 ('', 'collapse', False, _('collapse the rebased changesets')),
43 ('m', 'message', '',
43 ('m', 'message', '',
44 _('use text as collapse commit message'), _('TEXT')),
44 _('use text as collapse commit message'), _('TEXT')),
45 ('e', 'edit', False, _('invoke editor on commit messages')),
45 ('e', 'edit', False, _('invoke editor on commit messages')),
46 ('l', 'logfile', '',
46 ('l', 'logfile', '',
47 _('read collapse commit message from file'), _('FILE')),
47 _('read collapse commit message from file'), _('FILE')),
48 ('', 'keep', False, _('keep original changesets')),
48 ('', 'keep', False, _('keep original changesets')),
49 ('', 'keepbranches', False, _('keep original branch names')),
49 ('', 'keepbranches', False, _('keep original branch names')),
50 ('', 'detach', False, _('force detaching of source from its original '
50 ('D', 'detach', False, _('force detaching of source from its original '
51 'branch')),
51 'branch')),
52 ('t', 'tool', '', _('specify merge tool')),
52 ('t', 'tool', '', _('specify merge tool')),
53 ('c', 'continue', False, _('continue an interrupted rebase')),
53 ('c', 'continue', False, _('continue an interrupted rebase')),
54 ('a', 'abort', False, _('abort an interrupted rebase'))] +
54 ('a', 'abort', False, _('abort an interrupted rebase'))] +
55 templateopts,
55 templateopts,
56 _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
56 _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
57 'hg rebase {-a|-c}'))
57 'hg rebase {-a|-c}'))
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 lock = repo.lock()
118 lock = repo.lock()
119 wlock = repo.wlock()
119 wlock = repo.wlock()
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 detachf = opts.get('detach', False)
133 detachf = opts.get('detach', False)
134 # keepopen is not meant for use on the command line, but by
134 # keepopen is not meant for use on the command line, but by
135 # other extensions
135 # other extensions
136 keepopen = opts.get('keepopen', False)
136 keepopen = opts.get('keepopen', False)
137
137
138 if collapsemsg and not collapsef:
138 if collapsemsg and not collapsef:
139 raise util.Abort(
139 raise util.Abort(
140 _('message can only be specified with collapse'))
140 _('message can only be specified with collapse'))
141
141
142 if contf or abortf:
142 if contf or abortf:
143 if contf and abortf:
143 if contf and abortf:
144 raise util.Abort(_('cannot use both abort and continue'))
144 raise util.Abort(_('cannot use both abort and continue'))
145 if collapsef:
145 if collapsef:
146 raise util.Abort(
146 raise util.Abort(
147 _('cannot use collapse with continue or abort'))
147 _('cannot use collapse with continue or abort'))
148 if detachf:
148 if detachf:
149 raise util.Abort(_('cannot use detach with continue or abort'))
149 raise util.Abort(_('cannot use detach with continue or abort'))
150 if srcf or basef or destf:
150 if srcf or basef or destf:
151 raise util.Abort(
151 raise util.Abort(
152 _('abort and continue do not allow specifying revisions'))
152 _('abort and continue do not allow specifying revisions'))
153 if opts.get('tool', False):
153 if opts.get('tool', False):
154 ui.warn(_('tool option will be ignored\n'))
154 ui.warn(_('tool option will be ignored\n'))
155
155
156 (originalwd, target, state, skipped, collapsef, keepf,
156 (originalwd, target, state, skipped, collapsef, keepf,
157 keepbranchesf, external) = restorestatus(repo)
157 keepbranchesf, external) = restorestatus(repo)
158 if abortf:
158 if abortf:
159 return abort(repo, originalwd, target, state)
159 return abort(repo, originalwd, target, state)
160 else:
160 else:
161 if srcf and basef:
161 if srcf and basef:
162 raise util.Abort(_('cannot specify both a '
162 raise util.Abort(_('cannot specify both a '
163 'source and a base'))
163 'source and a base'))
164 if revf and basef:
164 if revf and basef:
165 raise util.Abort(_('cannot specify both a '
165 raise util.Abort(_('cannot specify both a '
166 'revision and a base'))
166 'revision and a base'))
167 if revf and srcf:
167 if revf and srcf:
168 raise util.Abort(_('cannot specify both a '
168 raise util.Abort(_('cannot specify both a '
169 'revision and a source'))
169 'revision and a source'))
170 if detachf:
170 if detachf:
171 if not (srcf or revf):
171 if not (srcf or revf):
172 raise util.Abort(
172 raise util.Abort(
173 _('detach requires a revision to be specified'))
173 _('detach requires a revision to be specified'))
174 if basef:
174 if basef:
175 raise util.Abort(_('cannot specify a base with detach'))
175 raise util.Abort(_('cannot specify a base with detach'))
176
176
177 cmdutil.bailifchanged(repo)
177 cmdutil.bailifchanged(repo)
178
178
179 if not destf:
179 if not destf:
180 # Destination defaults to the latest revision in the
180 # Destination defaults to the latest revision in the
181 # current branch
181 # current branch
182 branch = repo[None].branch()
182 branch = repo[None].branch()
183 dest = repo[branch]
183 dest = repo[branch]
184 else:
184 else:
185 dest = repo[destf]
185 dest = repo[destf]
186
186
187 if revf:
187 if revf:
188 rebaseset = repo.revs('%lr', revf)
188 rebaseset = repo.revs('%lr', revf)
189 elif srcf:
189 elif srcf:
190 rebaseset = repo.revs('(%r)::', srcf)
190 rebaseset = repo.revs('(%r)::', srcf)
191 else:
191 else:
192 base = basef or '.'
192 base = basef or '.'
193 rebaseset = repo.revs('(children(ancestor(%r, %d)) & ::%r)::',
193 rebaseset = repo.revs('(children(ancestor(%r, %d)) & ::%r)::',
194 base, dest, base)
194 base, dest, base)
195
195
196 if not rebaseset:
196 if not rebaseset:
197 repo.ui.debug('base is ancestor of destination')
197 repo.ui.debug('base is ancestor of destination')
198 result = None
198 result = None
199 elif not keepf and list(repo.revs('first(children(%ld) - %ld)',
199 elif not keepf and list(repo.revs('first(children(%ld) - %ld)',
200 rebaseset, rebaseset)):
200 rebaseset, rebaseset)):
201 raise util.Abort(
201 raise util.Abort(
202 _("can't remove original changesets with"
202 _("can't remove original changesets with"
203 " unrebased descendants"),
203 " unrebased descendants"),
204 hint=_('use --keep to keep original changesets'))
204 hint=_('use --keep to keep original changesets'))
205 else:
205 else:
206 result = buildstate(repo, dest, rebaseset, detachf)
206 result = buildstate(repo, dest, rebaseset, detachf)
207
207
208 if not result:
208 if not result:
209 # Empty state built, nothing to rebase
209 # Empty state built, nothing to rebase
210 ui.status(_('nothing to rebase\n'))
210 ui.status(_('nothing to rebase\n'))
211 return 1
211 return 1
212 else:
212 else:
213 originalwd, target, state = result
213 originalwd, target, state = result
214 if collapsef:
214 if collapsef:
215 targetancestors = set(repo.changelog.ancestors(target))
215 targetancestors = set(repo.changelog.ancestors(target))
216 targetancestors.add(target)
216 targetancestors.add(target)
217 external = checkexternal(repo, state, targetancestors)
217 external = checkexternal(repo, state, targetancestors)
218
218
219 if keepbranchesf:
219 if keepbranchesf:
220 assert not extrafn, 'cannot use both keepbranches and extrafn'
220 assert not extrafn, 'cannot use both keepbranches and extrafn'
221 def extrafn(ctx, extra):
221 def extrafn(ctx, extra):
222 extra['branch'] = ctx.branch()
222 extra['branch'] = ctx.branch()
223 if collapsef:
223 if collapsef:
224 branches = set()
224 branches = set()
225 for rev in state:
225 for rev in state:
226 branches.add(repo[rev].branch())
226 branches.add(repo[rev].branch())
227 if len(branches) > 1:
227 if len(branches) > 1:
228 raise util.Abort(_('cannot collapse multiple named '
228 raise util.Abort(_('cannot collapse multiple named '
229 'branches'))
229 'branches'))
230
230
231
231
232 # Rebase
232 # Rebase
233 if not targetancestors:
233 if not targetancestors:
234 targetancestors = set(repo.changelog.ancestors(target))
234 targetancestors = set(repo.changelog.ancestors(target))
235 targetancestors.add(target)
235 targetancestors.add(target)
236
236
237 # Keep track of the current bookmarks in order to reset them later
237 # Keep track of the current bookmarks in order to reset them later
238 currentbookmarks = repo._bookmarks.copy()
238 currentbookmarks = repo._bookmarks.copy()
239
239
240 sortedstate = sorted(state)
240 sortedstate = sorted(state)
241 total = len(sortedstate)
241 total = len(sortedstate)
242 pos = 0
242 pos = 0
243 for rev in sortedstate:
243 for rev in sortedstate:
244 pos += 1
244 pos += 1
245 if state[rev] == -1:
245 if state[rev] == -1:
246 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
246 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, repo[rev])),
247 _('changesets'), total)
247 _('changesets'), total)
248 storestatus(repo, originalwd, target, state, collapsef, keepf,
248 storestatus(repo, originalwd, target, state, collapsef, keepf,
249 keepbranchesf, external)
249 keepbranchesf, external)
250 p1, p2 = defineparents(repo, rev, target, state,
250 p1, p2 = defineparents(repo, rev, target, state,
251 targetancestors)
251 targetancestors)
252 if len(repo.parents()) == 2:
252 if len(repo.parents()) == 2:
253 repo.ui.debug('resuming interrupted rebase\n')
253 repo.ui.debug('resuming interrupted rebase\n')
254 else:
254 else:
255 try:
255 try:
256 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
256 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
257 stats = rebasenode(repo, rev, p1, state)
257 stats = rebasenode(repo, rev, p1, state)
258 if stats and stats[3] > 0:
258 if stats and stats[3] > 0:
259 raise util.Abort(_('unresolved conflicts (see hg '
259 raise util.Abort(_('unresolved conflicts (see hg '
260 'resolve, then hg rebase --continue)'))
260 'resolve, then hg rebase --continue)'))
261 finally:
261 finally:
262 ui.setconfig('ui', 'forcemerge', '')
262 ui.setconfig('ui', 'forcemerge', '')
263 cmdutil.duplicatecopies(repo, rev, target, p2)
263 cmdutil.duplicatecopies(repo, rev, target, p2)
264 if not collapsef:
264 if not collapsef:
265 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
265 newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn,
266 editor=editor)
266 editor=editor)
267 else:
267 else:
268 # Skip commit if we are collapsing
268 # Skip commit if we are collapsing
269 repo.dirstate.setparents(repo[p1].node())
269 repo.dirstate.setparents(repo[p1].node())
270 newrev = None
270 newrev = None
271 # Update the state
271 # Update the state
272 if newrev is not None:
272 if newrev is not None:
273 state[rev] = repo[newrev].rev()
273 state[rev] = repo[newrev].rev()
274 else:
274 else:
275 if not collapsef:
275 if not collapsef:
276 ui.note(_('no changes, revision %d skipped\n') % rev)
276 ui.note(_('no changes, revision %d skipped\n') % rev)
277 ui.debug('next revision set to %s\n' % p1)
277 ui.debug('next revision set to %s\n' % p1)
278 skipped.add(rev)
278 skipped.add(rev)
279 state[rev] = p1
279 state[rev] = p1
280
280
281 ui.progress(_('rebasing'), None)
281 ui.progress(_('rebasing'), None)
282 ui.note(_('rebase merging completed\n'))
282 ui.note(_('rebase merging completed\n'))
283
283
284 if collapsef and not keepopen:
284 if collapsef and not keepopen:
285 p1, p2 = defineparents(repo, min(state), target,
285 p1, p2 = defineparents(repo, min(state), target,
286 state, targetancestors)
286 state, targetancestors)
287 if collapsemsg:
287 if collapsemsg:
288 commitmsg = collapsemsg
288 commitmsg = collapsemsg
289 else:
289 else:
290 commitmsg = 'Collapsed revision'
290 commitmsg = 'Collapsed revision'
291 for rebased in state:
291 for rebased in state:
292 if rebased not in skipped and state[rebased] != nullmerge:
292 if rebased not in skipped and state[rebased] != nullmerge:
293 commitmsg += '\n* %s' % repo[rebased].description()
293 commitmsg += '\n* %s' % repo[rebased].description()
294 commitmsg = ui.edit(commitmsg, repo.ui.username())
294 commitmsg = ui.edit(commitmsg, repo.ui.username())
295 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
295 newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
296 extrafn=extrafn, editor=editor)
296 extrafn=extrafn, editor=editor)
297
297
298 if 'qtip' in repo.tags():
298 if 'qtip' in repo.tags():
299 updatemq(repo, state, skipped, **opts)
299 updatemq(repo, state, skipped, **opts)
300
300
301 if currentbookmarks:
301 if currentbookmarks:
302 # Nodeids are needed to reset bookmarks
302 # Nodeids are needed to reset bookmarks
303 nstate = {}
303 nstate = {}
304 for k, v in state.iteritems():
304 for k, v in state.iteritems():
305 if v != nullmerge:
305 if v != nullmerge:
306 nstate[repo[k].node()] = repo[v].node()
306 nstate[repo[k].node()] = repo[v].node()
307
307
308 if not keepf:
308 if not keepf:
309 # Remove no more useful revisions
309 # Remove no more useful revisions
310 rebased = [rev for rev in state if state[rev] != nullmerge]
310 rebased = [rev for rev in state if state[rev] != nullmerge]
311 if rebased:
311 if rebased:
312 if set(repo.changelog.descendants(min(rebased))) - set(state):
312 if set(repo.changelog.descendants(min(rebased))) - set(state):
313 ui.warn(_("warning: new changesets detected "
313 ui.warn(_("warning: new changesets detected "
314 "on source branch, not stripping\n"))
314 "on source branch, not stripping\n"))
315 else:
315 else:
316 # backup the old csets by default
316 # backup the old csets by default
317 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
317 repair.strip(ui, repo, repo[min(rebased)].node(), "all")
318
318
319 if currentbookmarks:
319 if currentbookmarks:
320 updatebookmarks(repo, nstate, currentbookmarks, **opts)
320 updatebookmarks(repo, nstate, currentbookmarks, **opts)
321
321
322 clearstatus(repo)
322 clearstatus(repo)
323 ui.note(_("rebase completed\n"))
323 ui.note(_("rebase completed\n"))
324 if os.path.exists(repo.sjoin('undo')):
324 if os.path.exists(repo.sjoin('undo')):
325 util.unlinkpath(repo.sjoin('undo'))
325 util.unlinkpath(repo.sjoin('undo'))
326 if skipped:
326 if skipped:
327 ui.note(_("%d revisions have been skipped\n") % len(skipped))
327 ui.note(_("%d revisions have been skipped\n") % len(skipped))
328 finally:
328 finally:
329 release(lock, wlock)
329 release(lock, wlock)
330
330
331 def checkexternal(repo, state, targetancestors):
331 def checkexternal(repo, state, targetancestors):
332 """Check whether one or more external revisions need to be taken in
332 """Check whether one or more external revisions need to be taken in
333 consideration. In the latter case, abort.
333 consideration. In the latter case, abort.
334 """
334 """
335 external = nullrev
335 external = nullrev
336 source = min(state)
336 source = min(state)
337 for rev in state:
337 for rev in state:
338 if rev == source:
338 if rev == source:
339 continue
339 continue
340 # Check externals and fail if there are more than one
340 # Check externals and fail if there are more than one
341 for p in repo[rev].parents():
341 for p in repo[rev].parents():
342 if (p.rev() not in state
342 if (p.rev() not in state
343 and p.rev() not in targetancestors):
343 and p.rev() not in targetancestors):
344 if external != nullrev:
344 if external != nullrev:
345 raise util.Abort(_('unable to collapse, there is more '
345 raise util.Abort(_('unable to collapse, there is more '
346 'than one external parent'))
346 'than one external parent'))
347 external = p.rev()
347 external = p.rev()
348 return external
348 return external
349
349
350 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
350 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None):
351 'Commit the changes and store useful information in extra'
351 'Commit the changes and store useful information in extra'
352 try:
352 try:
353 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
353 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
354 ctx = repo[rev]
354 ctx = repo[rev]
355 if commitmsg is None:
355 if commitmsg is None:
356 commitmsg = ctx.description()
356 commitmsg = ctx.description()
357 extra = {'rebase_source': ctx.hex()}
357 extra = {'rebase_source': ctx.hex()}
358 if extrafn:
358 if extrafn:
359 extrafn(ctx, extra)
359 extrafn(ctx, extra)
360 # Commit might fail if unresolved files exist
360 # Commit might fail if unresolved files exist
361 newrev = repo.commit(text=commitmsg, user=ctx.user(),
361 newrev = repo.commit(text=commitmsg, user=ctx.user(),
362 date=ctx.date(), extra=extra, editor=editor)
362 date=ctx.date(), extra=extra, editor=editor)
363 repo.dirstate.setbranch(repo[newrev].branch())
363 repo.dirstate.setbranch(repo[newrev].branch())
364 return newrev
364 return newrev
365 except util.Abort:
365 except util.Abort:
366 # Invalidate the previous setparents
366 # Invalidate the previous setparents
367 repo.dirstate.invalidate()
367 repo.dirstate.invalidate()
368 raise
368 raise
369
369
370 def rebasenode(repo, rev, p1, state):
370 def rebasenode(repo, rev, p1, state):
371 'Rebase a single revision'
371 'Rebase a single revision'
372 # Merge phase
372 # Merge phase
373 # Update to target and merge it with local
373 # Update to target and merge it with local
374 if repo['.'].rev() != repo[p1].rev():
374 if repo['.'].rev() != repo[p1].rev():
375 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
375 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
376 merge.update(repo, p1, False, True, False)
376 merge.update(repo, p1, False, True, False)
377 else:
377 else:
378 repo.ui.debug(" already in target\n")
378 repo.ui.debug(" already in target\n")
379 repo.dirstate.write()
379 repo.dirstate.write()
380 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
380 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
381 base = None
381 base = None
382 if repo[rev].rev() != repo[min(state)].rev():
382 if repo[rev].rev() != repo[min(state)].rev():
383 base = repo[rev].p1().node()
383 base = repo[rev].p1().node()
384 return merge.update(repo, rev, True, True, False, base)
384 return merge.update(repo, rev, True, True, False, base)
385
385
386 def defineparents(repo, rev, target, state, targetancestors):
386 def defineparents(repo, rev, target, state, targetancestors):
387 'Return the new parent relationship of the revision that will be rebased'
387 'Return the new parent relationship of the revision that will be rebased'
388 parents = repo[rev].parents()
388 parents = repo[rev].parents()
389 p1 = p2 = nullrev
389 p1 = p2 = nullrev
390
390
391 P1n = parents[0].rev()
391 P1n = parents[0].rev()
392 if P1n in targetancestors:
392 if P1n in targetancestors:
393 p1 = target
393 p1 = target
394 elif P1n in state:
394 elif P1n in state:
395 if state[P1n] == nullmerge:
395 if state[P1n] == nullmerge:
396 p1 = target
396 p1 = target
397 else:
397 else:
398 p1 = state[P1n]
398 p1 = state[P1n]
399 else: # P1n external
399 else: # P1n external
400 p1 = target
400 p1 = target
401 p2 = P1n
401 p2 = P1n
402
402
403 if len(parents) == 2 and parents[1].rev() not in targetancestors:
403 if len(parents) == 2 and parents[1].rev() not in targetancestors:
404 P2n = parents[1].rev()
404 P2n = parents[1].rev()
405 # interesting second parent
405 # interesting second parent
406 if P2n in state:
406 if P2n in state:
407 if p1 == target: # P1n in targetancestors or external
407 if p1 == target: # P1n in targetancestors or external
408 p1 = state[P2n]
408 p1 = state[P2n]
409 else:
409 else:
410 p2 = state[P2n]
410 p2 = state[P2n]
411 else: # P2n external
411 else: # P2n external
412 if p2 != nullrev: # P1n external too => rev is a merged revision
412 if p2 != nullrev: # P1n external too => rev is a merged revision
413 raise util.Abort(_('cannot use revision %d as base, result '
413 raise util.Abort(_('cannot use revision %d as base, result '
414 'would have 3 parents') % rev)
414 'would have 3 parents') % rev)
415 p2 = P2n
415 p2 = P2n
416 repo.ui.debug(" future parents are %d and %d\n" %
416 repo.ui.debug(" future parents are %d and %d\n" %
417 (repo[p1].rev(), repo[p2].rev()))
417 (repo[p1].rev(), repo[p2].rev()))
418 return p1, p2
418 return p1, p2
419
419
420 def isagitpatch(repo, patchname):
420 def isagitpatch(repo, patchname):
421 'Return true if the given patch is in git format'
421 'Return true if the given patch is in git format'
422 mqpatch = os.path.join(repo.mq.path, patchname)
422 mqpatch = os.path.join(repo.mq.path, patchname)
423 for line in patch.linereader(file(mqpatch, 'rb')):
423 for line in patch.linereader(file(mqpatch, 'rb')):
424 if line.startswith('diff --git'):
424 if line.startswith('diff --git'):
425 return True
425 return True
426 return False
426 return False
427
427
428 def updatemq(repo, state, skipped, **opts):
428 def updatemq(repo, state, skipped, **opts):
429 'Update rebased mq patches - finalize and then import them'
429 'Update rebased mq patches - finalize and then import them'
430 mqrebase = {}
430 mqrebase = {}
431 mq = repo.mq
431 mq = repo.mq
432 original_series = mq.fullseries[:]
432 original_series = mq.fullseries[:]
433
433
434 for p in mq.applied:
434 for p in mq.applied:
435 rev = repo[p.node].rev()
435 rev = repo[p.node].rev()
436 if rev in state:
436 if rev in state:
437 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
437 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
438 (rev, p.name))
438 (rev, p.name))
439 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
439 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
440
440
441 if mqrebase:
441 if mqrebase:
442 mq.finish(repo, mqrebase.keys())
442 mq.finish(repo, mqrebase.keys())
443
443
444 # We must start import from the newest revision
444 # We must start import from the newest revision
445 for rev in sorted(mqrebase, reverse=True):
445 for rev in sorted(mqrebase, reverse=True):
446 if rev not in skipped:
446 if rev not in skipped:
447 name, isgit = mqrebase[rev]
447 name, isgit = mqrebase[rev]
448 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
448 repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name))
449 mq.qimport(repo, (), patchname=name, git=isgit,
449 mq.qimport(repo, (), patchname=name, git=isgit,
450 rev=[str(state[rev])])
450 rev=[str(state[rev])])
451
451
452 # restore old series to preserve guards
452 # restore old series to preserve guards
453 mq.fullseries = original_series
453 mq.fullseries = original_series
454 mq.series_dirty = True
454 mq.series_dirty = True
455 mq.savedirty()
455 mq.savedirty()
456
456
457 def updatebookmarks(repo, nstate, originalbookmarks, **opts):
457 def updatebookmarks(repo, nstate, originalbookmarks, **opts):
458 'Move bookmarks to their correct changesets'
458 'Move bookmarks to their correct changesets'
459 current = repo._bookmarkcurrent
459 current = repo._bookmarkcurrent
460 for k, v in originalbookmarks.iteritems():
460 for k, v in originalbookmarks.iteritems():
461 if v in nstate:
461 if v in nstate:
462 if nstate[v] != nullmerge:
462 if nstate[v] != nullmerge:
463 # reset the pointer if the bookmark was moved incorrectly
463 # reset the pointer if the bookmark was moved incorrectly
464 if k != current:
464 if k != current:
465 repo._bookmarks[k] = nstate[v]
465 repo._bookmarks[k] = nstate[v]
466
466
467 bookmarks.write(repo)
467 bookmarks.write(repo)
468
468
469 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
469 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
470 external):
470 external):
471 'Store the current status to allow recovery'
471 'Store the current status to allow recovery'
472 f = repo.opener("rebasestate", "w")
472 f = repo.opener("rebasestate", "w")
473 f.write(repo[originalwd].hex() + '\n')
473 f.write(repo[originalwd].hex() + '\n')
474 f.write(repo[target].hex() + '\n')
474 f.write(repo[target].hex() + '\n')
475 f.write(repo[external].hex() + '\n')
475 f.write(repo[external].hex() + '\n')
476 f.write('%d\n' % int(collapse))
476 f.write('%d\n' % int(collapse))
477 f.write('%d\n' % int(keep))
477 f.write('%d\n' % int(keep))
478 f.write('%d\n' % int(keepbranches))
478 f.write('%d\n' % int(keepbranches))
479 for d, v in state.iteritems():
479 for d, v in state.iteritems():
480 oldrev = repo[d].hex()
480 oldrev = repo[d].hex()
481 if v != nullmerge:
481 if v != nullmerge:
482 newrev = repo[v].hex()
482 newrev = repo[v].hex()
483 else:
483 else:
484 newrev = v
484 newrev = v
485 f.write("%s:%s\n" % (oldrev, newrev))
485 f.write("%s:%s\n" % (oldrev, newrev))
486 f.close()
486 f.close()
487 repo.ui.debug('rebase status stored\n')
487 repo.ui.debug('rebase status stored\n')
488
488
489 def clearstatus(repo):
489 def clearstatus(repo):
490 'Remove the status files'
490 'Remove the status files'
491 if os.path.exists(repo.join("rebasestate")):
491 if os.path.exists(repo.join("rebasestate")):
492 util.unlinkpath(repo.join("rebasestate"))
492 util.unlinkpath(repo.join("rebasestate"))
493
493
494 def restorestatus(repo):
494 def restorestatus(repo):
495 'Restore a previously stored status'
495 'Restore a previously stored status'
496 try:
496 try:
497 target = None
497 target = None
498 collapse = False
498 collapse = False
499 external = nullrev
499 external = nullrev
500 state = {}
500 state = {}
501 f = repo.opener("rebasestate")
501 f = repo.opener("rebasestate")
502 for i, l in enumerate(f.read().splitlines()):
502 for i, l in enumerate(f.read().splitlines()):
503 if i == 0:
503 if i == 0:
504 originalwd = repo[l].rev()
504 originalwd = repo[l].rev()
505 elif i == 1:
505 elif i == 1:
506 target = repo[l].rev()
506 target = repo[l].rev()
507 elif i == 2:
507 elif i == 2:
508 external = repo[l].rev()
508 external = repo[l].rev()
509 elif i == 3:
509 elif i == 3:
510 collapse = bool(int(l))
510 collapse = bool(int(l))
511 elif i == 4:
511 elif i == 4:
512 keep = bool(int(l))
512 keep = bool(int(l))
513 elif i == 5:
513 elif i == 5:
514 keepbranches = bool(int(l))
514 keepbranches = bool(int(l))
515 else:
515 else:
516 oldrev, newrev = l.split(':')
516 oldrev, newrev = l.split(':')
517 if newrev != str(nullmerge):
517 if newrev != str(nullmerge):
518 state[repo[oldrev].rev()] = repo[newrev].rev()
518 state[repo[oldrev].rev()] = repo[newrev].rev()
519 else:
519 else:
520 state[repo[oldrev].rev()] = int(newrev)
520 state[repo[oldrev].rev()] = int(newrev)
521 skipped = set()
521 skipped = set()
522 # recompute the set of skipped revs
522 # recompute the set of skipped revs
523 if not collapse:
523 if not collapse:
524 seen = set([target])
524 seen = set([target])
525 for old, new in sorted(state.items()):
525 for old, new in sorted(state.items()):
526 if new != nullrev and new in seen:
526 if new != nullrev and new in seen:
527 skipped.add(old)
527 skipped.add(old)
528 seen.add(new)
528 seen.add(new)
529 repo.ui.debug('computed skipped revs: %s\n' % skipped)
529 repo.ui.debug('computed skipped revs: %s\n' % skipped)
530 repo.ui.debug('rebase status resumed\n')
530 repo.ui.debug('rebase status resumed\n')
531 return (originalwd, target, state, skipped,
531 return (originalwd, target, state, skipped,
532 collapse, keep, keepbranches, external)
532 collapse, keep, keepbranches, external)
533 except IOError, err:
533 except IOError, err:
534 if err.errno != errno.ENOENT:
534 if err.errno != errno.ENOENT:
535 raise
535 raise
536 raise util.Abort(_('no rebase in progress'))
536 raise util.Abort(_('no rebase in progress'))
537
537
538 def abort(repo, originalwd, target, state):
538 def abort(repo, originalwd, target, state):
539 'Restore the repository to its original state'
539 'Restore the repository to its original state'
540 if set(repo.changelog.descendants(target)) - set(state.values()):
540 if set(repo.changelog.descendants(target)) - set(state.values()):
541 repo.ui.warn(_("warning: new changesets detected on target branch, "
541 repo.ui.warn(_("warning: new changesets detected on target branch, "
542 "can't abort\n"))
542 "can't abort\n"))
543 return -1
543 return -1
544 else:
544 else:
545 # Strip from the first rebased revision
545 # Strip from the first rebased revision
546 merge.update(repo, repo[originalwd].rev(), False, True, False)
546 merge.update(repo, repo[originalwd].rev(), False, True, False)
547 rebased = filter(lambda x: x > -1 and x != target, state.values())
547 rebased = filter(lambda x: x > -1 and x != target, state.values())
548 if rebased:
548 if rebased:
549 strippoint = min(rebased)
549 strippoint = min(rebased)
550 # no backup of rebased cset versions needed
550 # no backup of rebased cset versions needed
551 repair.strip(repo.ui, repo, repo[strippoint].node())
551 repair.strip(repo.ui, repo, repo[strippoint].node())
552 clearstatus(repo)
552 clearstatus(repo)
553 repo.ui.warn(_('rebase aborted\n'))
553 repo.ui.warn(_('rebase aborted\n'))
554 return 0
554 return 0
555
555
556 def buildstate(repo, dest, rebaseset, detach):
556 def buildstate(repo, dest, rebaseset, detach):
557 '''Define which revisions are going to be rebased and where
557 '''Define which revisions are going to be rebased and where
558
558
559 repo: repo
559 repo: repo
560 dest: context
560 dest: context
561 rebaseset: set of rev
561 rebaseset: set of rev
562 detach: boolean'''
562 detach: boolean'''
563
563
564 # This check isn't strictly necessary, since mq detects commits over an
564 # This check isn't strictly necessary, since mq detects commits over an
565 # applied patch. But it prevents messing up the working directory when
565 # applied patch. But it prevents messing up the working directory when
566 # a partially completed rebase is blocked by mq.
566 # a partially completed rebase is blocked by mq.
567 if 'qtip' in repo.tags() and (dest.node() in
567 if 'qtip' in repo.tags() and (dest.node() in
568 [s.node for s in repo.mq.applied]):
568 [s.node for s in repo.mq.applied]):
569 raise util.Abort(_('cannot rebase onto an applied mq patch'))
569 raise util.Abort(_('cannot rebase onto an applied mq patch'))
570
570
571 detachset = set()
571 detachset = set()
572 roots = list(repo.set('roots(%ld)', rebaseset))
572 roots = list(repo.set('roots(%ld)', rebaseset))
573 if not roots:
573 if not roots:
574 raise util.Abort(_('no matching revisions'))
574 raise util.Abort(_('no matching revisions'))
575 if len(roots) > 1:
575 if len(roots) > 1:
576 raise util.Abort(_("can't rebase multiple roots"))
576 raise util.Abort(_("can't rebase multiple roots"))
577 root = roots[0]
577 root = roots[0]
578
578
579 commonbase = root.ancestor(dest)
579 commonbase = root.ancestor(dest)
580 if commonbase == root:
580 if commonbase == root:
581 raise util.Abort(_('source is ancestor of destination'))
581 raise util.Abort(_('source is ancestor of destination'))
582 if commonbase == dest:
582 if commonbase == dest:
583 samebranch = root.branch() == dest.branch()
583 samebranch = root.branch() == dest.branch()
584 if samebranch and root in dest.children():
584 if samebranch and root in dest.children():
585 repo.ui.debug('source is a child of destination')
585 repo.ui.debug('source is a child of destination')
586 return None
586 return None
587 # rebase on ancestor, force detach
587 # rebase on ancestor, force detach
588 detach = True
588 detach = True
589 if detach:
589 if detach:
590 detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root)
590 detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root)
591
591
592 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
592 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
593 state = dict.fromkeys(rebaseset, nullrev)
593 state = dict.fromkeys(rebaseset, nullrev)
594 state.update(dict.fromkeys(detachset, nullmerge))
594 state.update(dict.fromkeys(detachset, nullmerge))
595 return repo['.'].rev(), dest.rev(), state
595 return repo['.'].rev(), dest.rev(), state
596
596
597 def pullrebase(orig, ui, repo, *args, **opts):
597 def pullrebase(orig, ui, repo, *args, **opts):
598 'Call rebase after pull if the latter has been invoked with --rebase'
598 'Call rebase after pull if the latter has been invoked with --rebase'
599 if opts.get('rebase'):
599 if opts.get('rebase'):
600 if opts.get('update'):
600 if opts.get('update'):
601 del opts['update']
601 del opts['update']
602 ui.debug('--update and --rebase are not compatible, ignoring '
602 ui.debug('--update and --rebase are not compatible, ignoring '
603 'the update flag\n')
603 'the update flag\n')
604
604
605 cmdutil.bailifchanged(repo)
605 cmdutil.bailifchanged(repo)
606 revsprepull = len(repo)
606 revsprepull = len(repo)
607 origpostincoming = commands.postincoming
607 origpostincoming = commands.postincoming
608 def _dummy(*args, **kwargs):
608 def _dummy(*args, **kwargs):
609 pass
609 pass
610 commands.postincoming = _dummy
610 commands.postincoming = _dummy
611 try:
611 try:
612 orig(ui, repo, *args, **opts)
612 orig(ui, repo, *args, **opts)
613 finally:
613 finally:
614 commands.postincoming = origpostincoming
614 commands.postincoming = origpostincoming
615 revspostpull = len(repo)
615 revspostpull = len(repo)
616 if revspostpull > revsprepull:
616 if revspostpull > revsprepull:
617 rebase(ui, repo, **opts)
617 rebase(ui, repo, **opts)
618 branch = repo[None].branch()
618 branch = repo[None].branch()
619 dest = repo[branch].rev()
619 dest = repo[branch].rev()
620 if dest != repo['.'].rev():
620 if dest != repo['.'].rev():
621 # there was nothing to rebase we force an update
621 # there was nothing to rebase we force an update
622 hg.update(repo, dest)
622 hg.update(repo, dest)
623 else:
623 else:
624 if opts.get('tool'):
624 if opts.get('tool'):
625 raise util.Abort(_('--tool can only be used with --rebase'))
625 raise util.Abort(_('--tool can only be used with --rebase'))
626 orig(ui, repo, *args, **opts)
626 orig(ui, repo, *args, **opts)
627
627
628 def uisetup(ui):
628 def uisetup(ui):
629 'Replace pull with a decorator to provide --rebase option'
629 'Replace pull with a decorator to provide --rebase option'
630 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
630 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
631 entry[1].append(('', 'rebase', None,
631 entry[1].append(('', 'rebase', None,
632 _("rebase working directory to branch head")))
632 _("rebase working directory to branch head")))
633 entry[1].append(('t', 'tool', '',
633 entry[1].append(('t', 'tool', '',
634 _("specify merge tool for rebase")))
634 _("specify merge tool for rebase")))
General Comments 0
You need to be logged in to leave comments. Login now