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