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