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