comparison hgext/rebase.py @ 10351:38fe86fb16e3

rebase: refactoring Separate rebase-specific functions, in order to provide a better base to extend rebase's capabilities. Note that this will be useful especially to share functions with 'adapt'.
author Stefano Tortarolo <stefano.tortarolo@gmail.com>
date Sun, 31 Jan 2010 13:30:17 +0100
parents 25e572394f5c
children 66d954e76ffb
comparison
equal deleted inserted replaced
10350:fd511e9eeea6 10351:38fe86fb16e3
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
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()
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:
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
173 def newancestor(a, b, pfunc):
174 if b == rev:
175 return repo[rev].parents()[0].rev()
176 return oldancestor(a, b, pfunc)
177
178 if not first:
179 ancestor.ancestor = newancestor
180 else:
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
187
188 def checkexternal(repo, state, targetancestors):
189 """Check whether one or more external revisions need to be taken in
190 consideration. In the latter case, abort.
153 """ 191 """
154 repo.ui.debug(" set parents\n") 192 external = nullrev
155 if collapse and not last: 193 source = min(state)
156 repo.dirstate.setparents(repo[p1].node()) 194 for rev in state:
157 return None 195 if rev == source:
158 196 continue
159 repo.dirstate.setparents(repo[p1].node(), repo[p2].node()) 197 # Check externals and fail if there are more than one
160 198 for p in repo[rev].parents():
161 if skipped is None: 199 if (p.rev() not in state
162 skipped = set() 200 and p.rev() not in targetancestors):
163 201 if external != nullrev:
164 # Commit, record the old nodeid 202 raise util.Abort(_('unable to collapse, there is more '
165 newrev = nullrev 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: 242 # Update to target and merge it with local
201 # Update to target and merge it with local 243 if repo['.'].rev() != repo[p1].rev():
202 if repo['.'].rev() != repo[p1].rev(): 244 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1]))
203 repo.ui.debug(" update to %d:%s\n" % (repo[p1].rev(), repo[p1])) 245 merge.update(repo, p1, False, True, False)
204 merge.update(repo, p1, False, True, False) 246 else:
205 else: 247 repo.ui.debug(" already in target\n")
206 repo.ui.debug(" already in target\n") 248 repo.dirstate.write()
207 repo.dirstate.write() 249 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
208 repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev])) 250 first = repo[rev].rev() == repo[min(state)].rev()
209 first = repo[rev].rev() == repo[min(state)].rev() 251 stats = rebasemerge(repo, rev, first)
210 stats = rebasemerge(repo, rev, first) 252 return stats
211
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
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)
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
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'):