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