##// END OF EJS Templates
rebase: refactoring...
Stefano Tortarolo -
r10351:38fe86fb default
parent child Browse files
Show More
@@ -1,477 +1,476
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 util, repair, merge, cmdutil, commands, error
17 from mercurial import 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 def rebasemerge(repo, rev, first=False):
26 'return the correct ancestor'
27 oldancestor = ancestor.ancestor
28
29 def newancestor(a, b, pfunc):
30 if b == rev:
31 return repo[rev].parents()[0].rev()
32 return oldancestor(a, b, pfunc)
33
34 if not first:
35 ancestor.ancestor = newancestor
36 else:
37 repo.ui.debug("first revision, do not change ancestor\n")
38 try:
39 stats = merge.update(repo, rev, True, True, False)
40 return stats
41 finally:
42 ancestor.ancestor = oldancestor
43
44 def rebase(ui, repo, **opts):
25 def rebase(ui, repo, **opts):
45 """move changeset (and descendants) to a different branch
26 """move changeset (and descendants) to a different branch
46
27
47 Rebase uses repeated merging to graft changesets from one part of
28 Rebase uses repeated merging to graft changesets from one part of
48 history onto another. This can be useful for linearizing local
29 history onto another. This can be useful for linearizing local
49 changes relative to a master development tree.
30 changes relative to a master development tree.
50
31
51 If a rebase is interrupted to manually resolve a merge, it can be
32 If a rebase is interrupted to manually resolve a merge, it can be
52 continued with --continue/-c or aborted with --abort/-a.
33 continued with --continue/-c or aborted with --abort/-a.
53 """
34 """
54 originalwd = target = None
35 originalwd = target = None
55 external = nullrev
36 external = nullrev
56 state = {}
37 state = {}
57 skipped = set()
38 skipped = set()
39 targetancestors = set()
58
40
59 lock = wlock = None
41 lock = wlock = None
60 try:
42 try:
61 lock = repo.lock()
43 lock = repo.lock()
62 wlock = repo.wlock()
44 wlock = repo.wlock()
63
45
64 # Validate input and define rebasing points
46 # Validate input and define rebasing points
65 destf = opts.get('dest', None)
47 destf = opts.get('dest', None)
66 srcf = opts.get('source', None)
48 srcf = opts.get('source', None)
67 basef = opts.get('base', None)
49 basef = opts.get('base', None)
68 contf = opts.get('continue')
50 contf = opts.get('continue')
69 abortf = opts.get('abort')
51 abortf = opts.get('abort')
70 collapsef = opts.get('collapse', False)
52 collapsef = opts.get('collapse', False)
71 extrafn = opts.get('extrafn')
53 extrafn = opts.get('extrafn')
72 keepf = opts.get('keep', False)
54 keepf = opts.get('keep', False)
73 keepbranchesf = opts.get('keepbranches', False)
55 keepbranchesf = opts.get('keepbranches', False)
74
56
75 if contf or abortf:
57 if contf or abortf:
76 if contf and abortf:
58 if contf and abortf:
77 raise error.ParseError('rebase',
59 raise error.ParseError('rebase',
78 _('cannot use both abort and continue'))
60 _('cannot use both abort and continue'))
79 if collapsef:
61 if collapsef:
80 raise error.ParseError(
62 raise error.ParseError(
81 'rebase', _('cannot use collapse with continue or abort'))
63 'rebase', _('cannot use collapse with continue or abort'))
82
64
83 if srcf or basef or destf:
65 if srcf or basef or destf:
84 raise error.ParseError('rebase',
66 raise error.ParseError('rebase',
85 _('abort and continue do not allow specifying revisions'))
67 _('abort and continue do not allow specifying revisions'))
86
68
87 (originalwd, target, state, collapsef, keepf,
69 (originalwd, target, state, collapsef, keepf,
88 keepbranchesf, external) = restorestatus(repo)
70 keepbranchesf, external) = restorestatus(repo)
89 if abortf:
71 if abortf:
90 abort(repo, originalwd, target, state)
72 abort(repo, originalwd, target, state)
91 return
73 return
92 else:
74 else:
93 if srcf and basef:
75 if srcf and basef:
94 raise error.ParseError('rebase', _('cannot specify both a '
76 raise error.ParseError('rebase', _('cannot specify both a '
95 'revision and a base'))
77 'revision and a base'))
96 cmdutil.bail_if_changed(repo)
78 cmdutil.bail_if_changed(repo)
97 result = buildstate(repo, destf, srcf, basef, collapsef)
79 result = buildstate(repo, destf, srcf, basef)
98 if result:
80 if not result:
99 originalwd, target, state, external = result
81 # Empty state built, nothing to rebase
100 else: # Empty state built, nothing to rebase
101 ui.status(_('nothing to rebase\n'))
82 ui.status(_('nothing to rebase\n'))
102 return
83 return
84 else:
85 originalwd, target, state = result
86 if collapsef:
87 targetancestors = set(repo.changelog.ancestors(target))
88 external = checkexternal(repo, state, targetancestors)
103
89
104 if keepbranchesf:
90 if keepbranchesf:
105 if extrafn:
91 if extrafn:
106 raise error.ParseError(
92 raise error.ParseError(
107 'rebase', _('cannot use both keepbranches and extrafn'))
93 'rebase', _('cannot use both keepbranches and extrafn'))
108 def extrafn(ctx, extra):
94 def extrafn(ctx, extra):
109 extra['branch'] = ctx.branch()
95 extra['branch'] = ctx.branch()
110
96
111 # Rebase
97 # Rebase
112 targetancestors = list(repo.changelog.ancestors(target))
98 if not targetancestors:
113 targetancestors.append(target)
99 targetancestors = set(repo.changelog.ancestors(target))
100 targetancestors.add(target)
114
101
115 for rev in sorted(state):
102 for rev in sorted(state):
116 if state[rev] == -1:
103 if state[rev] == -1:
104 ui.debug("rebasing %d:%s\n" % (rev, repo[rev]))
117 storestatus(repo, originalwd, target, state, collapsef, keepf,
105 storestatus(repo, originalwd, target, state, collapsef, keepf,
118 keepbranchesf, external)
106 keepbranchesf, external)
119 rebasenode(repo, rev, target, state, skipped, targetancestors,
107 p1, p2 = defineparents(repo, rev, target, state,
120 collapsef, extrafn)
108 targetancestors)
109 if len(repo.parents()) == 2:
110 repo.ui.debug('resuming interrupted rebase\n')
111 else:
112 stats = rebasenode(repo, rev, p1, p2, state)
113 if stats and stats[3] > 0:
114 raise util.Abort(_('fix unresolved conflicts with hg '
115 'resolve then run hg rebase --continue'))
116 updatedirstate(repo, rev, target, p2)
117 if not collapsef:
118 extra = {'rebase_source': repo[rev].hex()}
119 if extrafn:
120 extrafn(repo[rev], extra)
121 newrev = concludenode(repo, rev, p1, p2, extra=extra)
122 else:
123 # Skip commit if we are collapsing
124 repo.dirstate.setparents(repo[p1].node())
125 newrev = None
126 # Update the state
127 if newrev is not None:
128 state[rev] = repo[newrev].rev()
129 else:
130 if not collapsef:
131 ui.note(_('no changes, revision %d skipped\n') % rev)
132 ui.debug('next revision set to %s\n' % p1)
133 skipped.add(rev)
134 state[rev] = p1
135
121 ui.note(_('rebase merging completed\n'))
136 ui.note(_('rebase merging completed\n'))
122
137
123 if collapsef:
138 if collapsef:
124 p1, p2 = defineparents(repo, min(state), target,
139 p1, p2 = defineparents(repo, min(state), target,
125 state, targetancestors)
140 state, targetancestors)
126 concludenode(repo, rev, p1, external, state, collapsef,
141 commitmsg = 'Collapsed revision'
127 last=True, skipped=skipped, extrafn=extrafn)
142 for rebased in state:
143 if rebased not in skipped:
144 commitmsg += '\n* %s' % repo[rebased].description()
145 commitmsg = ui.edit(commitmsg, repo.ui.username())
146 concludenode(repo, rev, p1, external, commitmsg=commitmsg,
147 extra=extrafn)
128
148
129 if 'qtip' in repo.tags():
149 if 'qtip' in repo.tags():
130 updatemq(repo, state, skipped, **opts)
150 updatemq(repo, state, skipped, **opts)
131
151
132 if not keepf:
152 if not keepf:
133 # Remove no more useful revisions
153 # Remove no more useful revisions
134 if set(repo.changelog.descendants(min(state))) - set(state):
154 if set(repo.changelog.descendants(min(state))) - set(state):
135 ui.warn(_("warning: new changesets detected on source branch, "
155 ui.warn(_("warning: new changesets detected on source branch, "
136 "not stripping\n"))
156 "not stripping\n"))
137 else:
157 else:
138 repair.strip(ui, repo, repo[min(state)].node(), "strip")
158 repair.strip(ui, repo, repo[min(state)].node(), "strip")
139
159
140 clearstatus(repo)
160 clearstatus(repo)
141 ui.status(_("rebase completed\n"))
161 ui.status(_("rebase completed\n"))
142 if os.path.exists(repo.sjoin('undo')):
162 if os.path.exists(repo.sjoin('undo')):
143 util.unlink(repo.sjoin('undo'))
163 util.unlink(repo.sjoin('undo'))
144 if skipped:
164 if skipped:
145 ui.note(_("%d revisions have been skipped\n") % len(skipped))
165 ui.note(_("%d revisions have been skipped\n") % len(skipped))
146 finally:
166 finally:
147 release(lock, wlock)
167 release(lock, wlock)
148
168
149 def concludenode(repo, rev, p1, p2, state, collapse, last=False, skipped=None,
169 def rebasemerge(repo, rev, first=False):
150 extrafn=None):
170 'return the correct ancestor'
151 """Skip commit if collapsing has been required and rev is not the last
171 oldancestor = ancestor.ancestor
152 revision, commit otherwise
172
153 """
173 def newancestor(a, b, pfunc):
154 repo.ui.debug(" set parents\n")
174 if b == rev:
155 if collapse and not last:
175 return repo[rev].parents()[0].rev()
156 repo.dirstate.setparents(repo[p1].node())
176 return oldancestor(a, b, pfunc)
157 return None
158
177
159 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
178 if not first:
160
179 ancestor.ancestor = newancestor
161 if skipped is None:
180 else:
162 skipped = set()
181 repo.ui.debug("first revision, do not change ancestor\n")
182 try:
183 stats = merge.update(repo, rev, True, True, False)
184 return stats
185 finally:
186 ancestor.ancestor = oldancestor
163
187
164 # Commit, record the old nodeid
188 def checkexternal(repo, state, targetancestors):
165 newrev = nullrev
189 """Check whether one or more external revisions need to be taken in
190 consideration. In the latter case, abort.
191 """
192 external = nullrev
193 source = min(state)
194 for rev in state:
195 if rev == source:
196 continue
197 # Check externals and fail if there are more than one
198 for p in repo[rev].parents():
199 if (p.rev() not in state
200 and p.rev() not in targetancestors):
201 if external != nullrev:
202 raise util.Abort(_('unable to collapse, there is more '
203 'than one external parent'))
204 external = p.rev()
205 return external
206
207 def updatedirstate(repo, rev, p1, p2):
208 """Keep track of renamed files in the revision that is going to be rebased
209 """
210 # Here we simulate the copies and renames in the source changeset
211 cop, diver = copies.copies(repo, repo[rev], repo[p1], repo[p2], True)
212 m1 = repo[rev].manifest()
213 m2 = repo[p1].manifest()
214 for k, v in cop.iteritems():
215 if k in m1:
216 if v in m1 or v in m2:
217 repo.dirstate.copy(v, k)
218 if v in m2 and v not in m1:
219 repo.dirstate.remove(v)
220
221 def concludenode(repo, rev, p1, p2, commitmsg=None, extra=None):
222 'Commit the changes and store useful information in extra'
166 try:
223 try:
167 if last:
224 repo.dirstate.setparents(repo[p1].node(), repo[p2].node())
168 # we don't translate commit messages
225 if commitmsg is None:
169 commitmsg = 'Collapsed revision'
170 for rebased in state:
171 if rebased not in skipped:
172 commitmsg += '\n* %s' % repo[rebased].description()
173 commitmsg = repo.ui.edit(commitmsg, repo.ui.username())
174 else:
175 commitmsg = repo[rev].description()
226 commitmsg = repo[rev].description()
227 if extra is None:
228 extra = {}
176 # Commit might fail if unresolved files exist
229 # Commit might fail if unresolved files exist
177 extra = {'rebase_source': repo[rev].hex()}
178 if extrafn:
179 extrafn(repo[rev], extra)
180 newrev = repo.commit(text=commitmsg, user=repo[rev].user(),
230 newrev = repo.commit(text=commitmsg, user=repo[rev].user(),
181 date=repo[rev].date(), extra=extra)
231 date=repo[rev].date(), extra=extra)
182 repo.dirstate.setbranch(repo[newrev].branch())
232 repo.dirstate.setbranch(repo[newrev].branch())
183 return newrev
233 return newrev
184 except util.Abort:
234 except util.Abort:
185 # Invalidate the previous setparents
235 # Invalidate the previous setparents
186 repo.dirstate.invalidate()
236 repo.dirstate.invalidate()
187 raise
237 raise
188
238
189 def rebasenode(repo, rev, target, state, skipped, targetancestors, collapse,
239 def rebasenode(repo, rev, p1, p2, state):
190 extrafn):
191 'Rebase a single revision'
240 'Rebase a single revision'
192 repo.ui.debug("rebasing %d:%s\n" % (rev, repo[rev]))
193
194 p1, p2 = defineparents(repo, rev, target, state, targetancestors)
195
196 repo.ui.debug(" future parents are %d and %d\n" % (repo[p1].rev(),
197 repo[p2].rev()))
198
199 # Merge phase
241 # Merge phase
200 if len(repo.parents()) != 2:
201 # Update to target and merge it with local
242 # Update to target and merge it with local
202 if repo['.'].rev() != repo[p1].rev():
243 if repo['.'].rev() != repo[p1].rev():
203 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
244 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
204 merge.update(repo, p1, False, True, False)
245 merge.update(repo, p1, False, True, False)
205 else:
246 else:
206 repo.ui.debug(" already in target\n")
247 repo.ui.debug(" already in target\n")
207 repo.dirstate.write()
248 repo.dirstate.write()
208 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
249 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
209 first = repo[rev].rev() == repo[min(state)].rev()
250 first = repo[rev].rev() == repo[min(state)].rev()
210 stats = rebasemerge(repo, rev, first)
251 stats = rebasemerge(repo, rev, first)
211
252 return stats
212 if stats[3] > 0:
213 raise util.Abort(_('fix unresolved conflicts with hg resolve then '
214 'run hg rebase --continue'))
215 else: # we have an interrupted rebase
216 repo.ui.debug('resuming interrupted rebase\n')
217
218 # Keep track of renamed files in the revision that is going to be rebased
219 # Here we simulate the copies and renames in the source changeset
220 cop, diver = copies.copies(repo, repo[rev], repo[target], repo[p2], True)
221 m1 = repo[rev].manifest()
222 m2 = repo[target].manifest()
223 for k, v in cop.iteritems():
224 if k in m1:
225 if v in m1 or v in m2:
226 repo.dirstate.copy(v, k)
227 if v in m2 and v not in m1:
228 repo.dirstate.remove(v)
229
230 newrev = concludenode(repo, rev, p1, p2, state, collapse,
231 extrafn=extrafn)
232
233 # Update the state
234 if newrev is not None:
235 state[rev] = repo[newrev].rev()
236 else:
237 if not collapse:
238 repo.ui.note(_('no changes, revision %d skipped\n') % rev)
239 repo.ui.debug('next revision set to %s\n' % p1)
240 skipped.add(rev)
241 state[rev] = p1
242
253
243 def defineparents(repo, rev, target, state, targetancestors):
254 def defineparents(repo, rev, target, state, targetancestors):
244 'Return the new parent relationship of the revision that will be rebased'
255 'Return the new parent relationship of the revision that will be rebased'
245 parents = repo[rev].parents()
256 parents = repo[rev].parents()
246 p1 = p2 = nullrev
257 p1 = p2 = nullrev
247
258
248 P1n = parents[0].rev()
259 P1n = parents[0].rev()
249 if P1n in targetancestors:
260 if P1n in targetancestors:
250 p1 = target
261 p1 = target
251 elif P1n in state:
262 elif P1n in state:
252 p1 = state[P1n]
263 p1 = state[P1n]
253 else: # P1n external
264 else: # P1n external
254 p1 = target
265 p1 = target
255 p2 = P1n
266 p2 = P1n
256
267
257 if len(parents) == 2 and parents[1].rev() not in targetancestors:
268 if len(parents) == 2 and parents[1].rev() not in targetancestors:
258 P2n = parents[1].rev()
269 P2n = parents[1].rev()
259 # interesting second parent
270 # interesting second parent
260 if P2n in state:
271 if P2n in state:
261 if p1 == target: # P1n in targetancestors or external
272 if p1 == target: # P1n in targetancestors or external
262 p1 = state[P2n]
273 p1 = state[P2n]
263 else:
274 else:
264 p2 = state[P2n]
275 p2 = state[P2n]
265 else: # P2n external
276 else: # P2n external
266 if p2 != nullrev: # P1n external too => rev is a merged revision
277 if p2 != nullrev: # P1n external too => rev is a merged revision
267 raise util.Abort(_('cannot use revision %d as base, result '
278 raise util.Abort(_('cannot use revision %d as base, result '
268 'would have 3 parents') % rev)
279 'would have 3 parents') % rev)
269 p2 = P2n
280 p2 = P2n
281 repo.ui.debug(" future parents are %d and %d\n" %
282 (repo[p1].rev(), repo[p2].rev()))
270 return p1, p2
283 return p1, p2
271
284
272 def isagitpatch(repo, patchname):
285 def isagitpatch(repo, patchname):
273 'Return true if the given patch is in git format'
286 'Return true if the given patch is in git format'
274 mqpatch = os.path.join(repo.mq.path, patchname)
287 mqpatch = os.path.join(repo.mq.path, patchname)
275 for line in patch.linereader(file(mqpatch, 'rb')):
288 for line in patch.linereader(file(mqpatch, 'rb')):
276 if line.startswith('diff --git'):
289 if line.startswith('diff --git'):
277 return True
290 return True
278 return False
291 return False
279
292
280 def updatemq(repo, state, skipped, **opts):
293 def updatemq(repo, state, skipped, **opts):
281 'Update rebased mq patches - finalize and then import them'
294 'Update rebased mq patches - finalize and then import them'
282 mqrebase = {}
295 mqrebase = {}
283 for p in repo.mq.applied:
296 for p in repo.mq.applied:
284 if repo[p.rev].rev() in state:
297 if repo[p.rev].rev() in state:
285 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
298 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
286 (repo[p.rev].rev(), p.name))
299 (repo[p.rev].rev(), p.name))
287 mqrebase[repo[p.rev].rev()] = (p.name, isagitpatch(repo, p.name))
300 mqrebase[repo[p.rev].rev()] = (p.name, isagitpatch(repo, p.name))
288
301
289 if mqrebase:
302 if mqrebase:
290 repo.mq.finish(repo, mqrebase.keys())
303 repo.mq.finish(repo, mqrebase.keys())
291
304
292 # We must start import from the newest revision
305 # We must start import from the newest revision
293 for rev in sorted(mqrebase, reverse=True):
306 for rev in sorted(mqrebase, reverse=True):
294 if rev not in skipped:
307 if rev not in skipped:
295 repo.ui.debug('import mq patch %d (%s)\n'
308 repo.ui.debug('import mq patch %d (%s)\n'
296 % (state[rev], mqrebase[rev][0]))
309 % (state[rev], mqrebase[rev][0]))
297 repo.mq.qimport(repo, (), patchname=mqrebase[rev][0],
310 repo.mq.qimport(repo, (), patchname=mqrebase[rev][0],
298 git=mqrebase[rev][1],rev=[str(state[rev])])
311 git=mqrebase[rev][1],rev=[str(state[rev])])
299 repo.mq.save_dirty()
312 repo.mq.save_dirty()
300
313
301 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
314 def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
302 external):
315 external):
303 'Store the current status to allow recovery'
316 'Store the current status to allow recovery'
304 f = repo.opener("rebasestate", "w")
317 f = repo.opener("rebasestate", "w")
305 f.write(repo[originalwd].hex() + '\n')
318 f.write(repo[originalwd].hex() + '\n')
306 f.write(repo[target].hex() + '\n')
319 f.write(repo[target].hex() + '\n')
307 f.write(repo[external].hex() + '\n')
320 f.write(repo[external].hex() + '\n')
308 f.write('%d\n' % int(collapse))
321 f.write('%d\n' % int(collapse))
309 f.write('%d\n' % int(keep))
322 f.write('%d\n' % int(keep))
310 f.write('%d\n' % int(keepbranches))
323 f.write('%d\n' % int(keepbranches))
311 for d, v in state.iteritems():
324 for d, v in state.iteritems():
312 oldrev = repo[d].hex()
325 oldrev = repo[d].hex()
313 newrev = repo[v].hex()
326 newrev = repo[v].hex()
314 f.write("%s:%s\n" % (oldrev, newrev))
327 f.write("%s:%s\n" % (oldrev, newrev))
315 f.close()
328 f.close()
316 repo.ui.debug('rebase status stored\n')
329 repo.ui.debug('rebase status stored\n')
317
330
318 def clearstatus(repo):
331 def clearstatus(repo):
319 'Remove the status files'
332 'Remove the status files'
320 if os.path.exists(repo.join("rebasestate")):
333 if os.path.exists(repo.join("rebasestate")):
321 util.unlink(repo.join("rebasestate"))
334 util.unlink(repo.join("rebasestate"))
322
335
323 def restorestatus(repo):
336 def restorestatus(repo):
324 'Restore a previously stored status'
337 'Restore a previously stored status'
325 try:
338 try:
326 target = None
339 target = None
327 collapse = False
340 collapse = False
328 external = nullrev
341 external = nullrev
329 state = {}
342 state = {}
330 f = repo.opener("rebasestate")
343 f = repo.opener("rebasestate")
331 for i, l in enumerate(f.read().splitlines()):
344 for i, l in enumerate(f.read().splitlines()):
332 if i == 0:
345 if i == 0:
333 originalwd = repo[l].rev()
346 originalwd = repo[l].rev()
334 elif i == 1:
347 elif i == 1:
335 target = repo[l].rev()
348 target = repo[l].rev()
336 elif i == 2:
349 elif i == 2:
337 external = repo[l].rev()
350 external = repo[l].rev()
338 elif i == 3:
351 elif i == 3:
339 collapse = bool(int(l))
352 collapse = bool(int(l))
340 elif i == 4:
353 elif i == 4:
341 keep = bool(int(l))
354 keep = bool(int(l))
342 elif i == 5:
355 elif i == 5:
343 keepbranches = bool(int(l))
356 keepbranches = bool(int(l))
344 else:
357 else:
345 oldrev, newrev = l.split(':')
358 oldrev, newrev = l.split(':')
346 state[repo[oldrev].rev()] = repo[newrev].rev()
359 state[repo[oldrev].rev()] = repo[newrev].rev()
347 repo.ui.debug('rebase status resumed\n')
360 repo.ui.debug('rebase status resumed\n')
348 return originalwd, target, state, collapse, keep, keepbranches, external
361 return originalwd, target, state, collapse, keep, keepbranches, external
349 except IOError, err:
362 except IOError, err:
350 if err.errno != errno.ENOENT:
363 if err.errno != errno.ENOENT:
351 raise
364 raise
352 raise util.Abort(_('no rebase in progress'))
365 raise util.Abort(_('no rebase in progress'))
353
366
354 def abort(repo, originalwd, target, state):
367 def abort(repo, originalwd, target, state):
355 'Restore the repository to its original state'
368 'Restore the repository to its original state'
356 if set(repo.changelog.descendants(target)) - set(state.values()):
369 if set(repo.changelog.descendants(target)) - set(state.values()):
357 repo.ui.warn(_("warning: new changesets detected on target branch, "
370 repo.ui.warn(_("warning: new changesets detected on target branch, "
358 "not stripping\n"))
371 "not stripping\n"))
359 else:
372 else:
360 # Strip from the first rebased revision
373 # Strip from the first rebased revision
361 merge.update(repo, repo[originalwd].rev(), False, True, False)
374 merge.update(repo, repo[originalwd].rev(), False, True, False)
362 rebased = filter(lambda x: x > -1, state.values())
375 rebased = filter(lambda x: x > -1, state.values())
363 if rebased:
376 if rebased:
364 strippoint = min(rebased)
377 strippoint = min(rebased)
365 repair.strip(repo.ui, repo, repo[strippoint].node(), "strip")
378 repair.strip(repo.ui, repo, repo[strippoint].node(), "strip")
366 clearstatus(repo)
379 clearstatus(repo)
367 repo.ui.status(_('rebase aborted\n'))
380 repo.ui.status(_('rebase aborted\n'))
368
381
369 def buildstate(repo, dest, src, base, collapse):
382 def buildstate(repo, dest, src, base):
370 'Define which revisions are going to be rebased and where'
383 'Define which revisions are going to be rebased and where'
371 targetancestors = set()
384 targetancestors = set()
372
385
373 if not dest:
386 if not dest:
374 # Destination defaults to the latest revision in the current branch
387 # Destination defaults to the latest revision in the current branch
375 branch = repo[None].branch()
388 branch = repo[None].branch()
376 dest = repo[branch].rev()
389 dest = repo[branch].rev()
377 else:
390 else:
378 if 'qtip' in repo.tags() and (repo[dest].hex() in
391 if 'qtip' in repo.tags() and (repo[dest].hex() in
379 [s.rev for s in repo.mq.applied]):
392 [s.rev for s in repo.mq.applied]):
380 raise util.Abort(_('cannot rebase onto an applied mq patch'))
393 raise util.Abort(_('cannot rebase onto an applied mq patch'))
381 dest = repo[dest].rev()
394 dest = repo[dest].rev()
382
395
383 if src:
396 if src:
384 commonbase = repo[src].ancestor(repo[dest])
397 commonbase = repo[src].ancestor(repo[dest])
385 if commonbase == repo[src]:
398 if commonbase == repo[src]:
386 raise util.Abort(_('source is ancestor of destination'))
399 raise util.Abort(_('source is ancestor of destination'))
387 if commonbase == repo[dest]:
400 if commonbase == repo[dest]:
388 raise util.Abort(_('source is descendant of destination'))
401 raise util.Abort(_('source is descendant of destination'))
389 source = repo[src].rev()
402 source = repo[src].rev()
390 else:
403 else:
391 if base:
404 if base:
392 cwd = repo[base].rev()
405 cwd = repo[base].rev()
393 else:
406 else:
394 cwd = repo['.'].rev()
407 cwd = repo['.'].rev()
395
408
396 if cwd == dest:
409 if cwd == dest:
397 repo.ui.debug('source and destination are the same\n')
410 repo.ui.debug('source and destination are the same\n')
398 return None
411 return None
399
412
400 targetancestors = set(repo.changelog.ancestors(dest))
413 targetancestors = set(repo.changelog.ancestors(dest))
401 if cwd in targetancestors:
414 if cwd in targetancestors:
402 repo.ui.debug('source is ancestor of destination\n')
415 repo.ui.debug('source is ancestor of destination\n')
403 return None
416 return None
404
417
405 cwdancestors = set(repo.changelog.ancestors(cwd))
418 cwdancestors = set(repo.changelog.ancestors(cwd))
406 if dest in cwdancestors:
419 if dest in cwdancestors:
407 repo.ui.debug('source is descendant of destination\n')
420 repo.ui.debug('source is descendant of destination\n')
408 return None
421 return None
409
422
410 cwdancestors.add(cwd)
423 cwdancestors.add(cwd)
411 rebasingbranch = cwdancestors - targetancestors
424 rebasingbranch = cwdancestors - targetancestors
412 source = min(rebasingbranch)
425 source = min(rebasingbranch)
413
426
414 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
427 repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
415 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
428 state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
416 external = nullrev
417 if collapse:
418 if not targetancestors:
419 targetancestors = set(repo.changelog.ancestors(dest))
420 for rev in state:
421 # Check externals and fail if there are more than one
422 for p in repo[rev].parents():
423 if (p.rev() not in state and p.rev() != source
424 and p.rev() not in targetancestors):
425 if external != nullrev:
426 raise util.Abort(_('unable to collapse, there is more '
427 'than one external parent'))
428 external = p.rev()
429
430 state[source] = nullrev
429 state[source] = nullrev
431 return repo['.'].rev(), repo[dest].rev(), state, external
430 return repo['.'].rev(), repo[dest].rev(), state
432
431
433 def pullrebase(orig, ui, repo, *args, **opts):
432 def pullrebase(orig, ui, repo, *args, **opts):
434 'Call rebase after pull if the latter has been invoked with --rebase'
433 'Call rebase after pull if the latter has been invoked with --rebase'
435 if opts.get('rebase'):
434 if opts.get('rebase'):
436 if opts.get('update'):
435 if opts.get('update'):
437 del opts['update']
436 del opts['update']
438 ui.debug('--update and --rebase are not compatible, ignoring '
437 ui.debug('--update and --rebase are not compatible, ignoring '
439 'the update flag\n')
438 'the update flag\n')
440
439
441 cmdutil.bail_if_changed(repo)
440 cmdutil.bail_if_changed(repo)
442 revsprepull = len(repo)
441 revsprepull = len(repo)
443 orig(ui, repo, *args, **opts)
442 orig(ui, repo, *args, **opts)
444 revspostpull = len(repo)
443 revspostpull = len(repo)
445 if revspostpull > revsprepull:
444 if revspostpull > revsprepull:
446 rebase(ui, repo, **opts)
445 rebase(ui, repo, **opts)
447 branch = repo[None].branch()
446 branch = repo[None].branch()
448 dest = repo[branch].rev()
447 dest = repo[branch].rev()
449 if dest != repo['.'].rev():
448 if dest != repo['.'].rev():
450 # there was nothing to rebase we force an update
449 # there was nothing to rebase we force an update
451 merge.update(repo, dest, False, False, False)
450 merge.update(repo, dest, False, False, False)
452 else:
451 else:
453 orig(ui, repo, *args, **opts)
452 orig(ui, repo, *args, **opts)
454
453
455 def uisetup(ui):
454 def uisetup(ui):
456 'Replace pull with a decorator to provide --rebase option'
455 'Replace pull with a decorator to provide --rebase option'
457 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
456 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
458 entry[1].append(('', 'rebase', None,
457 entry[1].append(('', 'rebase', None,
459 _("rebase working directory to branch head"))
458 _("rebase working directory to branch head"))
460 )
459 )
461
460
462 cmdtable = {
461 cmdtable = {
463 "rebase":
462 "rebase":
464 (rebase,
463 (rebase,
465 [
464 [
466 ('s', 'source', '', _('rebase from a given revision')),
465 ('s', 'source', '', _('rebase from a given revision')),
467 ('b', 'base', '', _('rebase from the base of a given revision')),
466 ('b', 'base', '', _('rebase from the base of a given revision')),
468 ('d', 'dest', '', _('rebase onto a given revision')),
467 ('d', 'dest', '', _('rebase onto a given revision')),
469 ('', 'collapse', False, _('collapse the rebased changesets')),
468 ('', 'collapse', False, _('collapse the rebased changesets')),
470 ('', 'keep', False, _('keep original changesets')),
469 ('', 'keep', False, _('keep original changesets')),
471 ('', 'keepbranches', False, _('keep original branch names')),
470 ('', 'keepbranches', False, _('keep original branch names')),
472 ('c', 'continue', False, _('continue an interrupted rebase')),
471 ('c', 'continue', False, _('continue an interrupted rebase')),
473 ('a', 'abort', False, _('abort an interrupted rebase')),] +
472 ('a', 'abort', False, _('abort an interrupted rebase')),] +
474 templateopts,
473 templateopts,
475 _('hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] '
474 _('hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] '
476 '[--keepbranches] | [-c] | [-a]')),
475 '[--keepbranches] | [-c] | [-a]')),
477 }
476 }
General Comments 0
You need to be logged in to leave comments. Login now