Mercurial > hg
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'): |