##// END OF EJS Templates
convert: replace cache of (m,a,r) by (ma,r)...
Martin von Zweigbergk -
r27718:6e1fba0f default
parent child Browse files
Show More
@@ -1,625 +1,629
1 1 # hg.py - hg backend for convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # Notes for hg->hg conversion:
9 9 #
10 10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 11 # of commit messages, but new versions do. Changesets created by
12 12 # those older versions, then converted, may thus have different
13 13 # hashes for changesets that are otherwise identical.
14 14 #
15 15 # * Using "--config convert.hg.saverev=true" will make the source
16 16 # identifier to be stored in the converted revision. This will cause
17 17 # the converted revision to have a different identity than the
18 18 # source.
19 19
20 20
21 21 import os, time, cStringIO
22 22 from mercurial.i18n import _
23 23 from mercurial.node import bin, hex, nullid
24 24 from mercurial import hg, util, context, bookmarks, error, scmutil, exchange
25 25 from mercurial import phases
26 26 from mercurial import lock as lockmod
27 27 from mercurial import merge as mergemod
28 28
29 29 from common import NoRepo, commit, converter_source, converter_sink, mapfile
30 30
31 31 import re
32 32 sha1re = re.compile(r'\b[0-9a-f]{12,40}\b')
33 33
34 34 class mercurial_sink(converter_sink):
35 35 def __init__(self, ui, path):
36 36 converter_sink.__init__(self, ui, path)
37 37 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
38 38 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
39 39 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
40 40 self.lastbranch = None
41 41 if os.path.isdir(path) and len(os.listdir(path)) > 0:
42 42 try:
43 43 self.repo = hg.repository(self.ui, path)
44 44 if not self.repo.local():
45 45 raise NoRepo(_('%s is not a local Mercurial repository')
46 46 % path)
47 47 except error.RepoError as err:
48 48 ui.traceback()
49 49 raise NoRepo(err.args[0])
50 50 else:
51 51 try:
52 52 ui.status(_('initializing destination %s repository\n') % path)
53 53 self.repo = hg.repository(self.ui, path, create=True)
54 54 if not self.repo.local():
55 55 raise NoRepo(_('%s is not a local Mercurial repository')
56 56 % path)
57 57 self.created.append(path)
58 58 except error.RepoError:
59 59 ui.traceback()
60 60 raise NoRepo(_("could not create hg repository %s as sink")
61 61 % path)
62 62 self.lock = None
63 63 self.wlock = None
64 64 self.filemapmode = False
65 65 self.subrevmaps = {}
66 66
67 67 def before(self):
68 68 self.ui.debug('run hg sink pre-conversion action\n')
69 69 self.wlock = self.repo.wlock()
70 70 self.lock = self.repo.lock()
71 71
72 72 def after(self):
73 73 self.ui.debug('run hg sink post-conversion action\n')
74 74 if self.lock:
75 75 self.lock.release()
76 76 if self.wlock:
77 77 self.wlock.release()
78 78
79 79 def revmapfile(self):
80 80 return self.repo.join("shamap")
81 81
82 82 def authorfile(self):
83 83 return self.repo.join("authormap")
84 84
85 85 def setbranch(self, branch, pbranches):
86 86 if not self.clonebranches:
87 87 return
88 88
89 89 setbranch = (branch != self.lastbranch)
90 90 self.lastbranch = branch
91 91 if not branch:
92 92 branch = 'default'
93 93 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
94 94 if pbranches:
95 95 pbranch = pbranches[0][1]
96 96 else:
97 97 pbranch = 'default'
98 98
99 99 branchpath = os.path.join(self.path, branch)
100 100 if setbranch:
101 101 self.after()
102 102 try:
103 103 self.repo = hg.repository(self.ui, branchpath)
104 104 except Exception:
105 105 self.repo = hg.repository(self.ui, branchpath, create=True)
106 106 self.before()
107 107
108 108 # pbranches may bring revisions from other branches (merge parents)
109 109 # Make sure we have them, or pull them.
110 110 missings = {}
111 111 for b in pbranches:
112 112 try:
113 113 self.repo.lookup(b[0])
114 114 except Exception:
115 115 missings.setdefault(b[1], []).append(b[0])
116 116
117 117 if missings:
118 118 self.after()
119 119 for pbranch, heads in sorted(missings.iteritems()):
120 120 pbranchpath = os.path.join(self.path, pbranch)
121 121 prepo = hg.peer(self.ui, {}, pbranchpath)
122 122 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
123 123 exchange.pull(self.repo, prepo,
124 124 [prepo.lookup(h) for h in heads])
125 125 self.before()
126 126
127 127 def _rewritetags(self, source, revmap, data):
128 128 fp = cStringIO.StringIO()
129 129 for line in data.splitlines():
130 130 s = line.split(' ', 1)
131 131 if len(s) != 2:
132 132 continue
133 133 revid = revmap.get(source.lookuprev(s[0]))
134 134 if not revid:
135 135 if s[0] == hex(nullid):
136 136 revid = s[0]
137 137 else:
138 138 continue
139 139 fp.write('%s %s\n' % (revid, s[1]))
140 140 return fp.getvalue()
141 141
142 142 def _rewritesubstate(self, source, data):
143 143 fp = cStringIO.StringIO()
144 144 for line in data.splitlines():
145 145 s = line.split(' ', 1)
146 146 if len(s) != 2:
147 147 continue
148 148
149 149 revid = s[0]
150 150 subpath = s[1]
151 151 if revid != hex(nullid):
152 152 revmap = self.subrevmaps.get(subpath)
153 153 if revmap is None:
154 154 revmap = mapfile(self.ui,
155 155 self.repo.wjoin(subpath, '.hg/shamap'))
156 156 self.subrevmaps[subpath] = revmap
157 157
158 158 # It is reasonable that one or more of the subrepos don't
159 159 # need to be converted, in which case they can be cloned
160 160 # into place instead of converted. Therefore, only warn
161 161 # once.
162 162 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
163 163 if len(revmap) == 0:
164 164 sub = self.repo.wvfs.reljoin(subpath, '.hg')
165 165
166 166 if self.repo.wvfs.exists(sub):
167 167 self.ui.warn(msg % subpath)
168 168
169 169 newid = revmap.get(revid)
170 170 if not newid:
171 171 if len(revmap) > 0:
172 172 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
173 173 (revid, subpath))
174 174 else:
175 175 revid = newid
176 176
177 177 fp.write('%s %s\n' % (revid, subpath))
178 178
179 179 return fp.getvalue()
180 180
181 181 def _calculatemergedfiles(self, source, p1ctx, p2ctx):
182 182 """Calculates the files from p2 that we need to pull in when merging p1
183 183 and p2, given that the merge is coming from the given source.
184 184
185 185 This prevents us from losing files that only exist in the target p2 and
186 186 that don't come from the source repo (like if you're merging multiple
187 187 repositories together).
188 188 """
189 189 anc = [p1ctx.ancestor(p2ctx)]
190 190 # Calculate what files are coming from p2
191 191 actions, diverge, rename = mergemod.calculateupdates(
192 192 self.repo, p1ctx, p2ctx, anc,
193 193 True, # branchmerge
194 194 True, # force
195 195 False, # acceptremote
196 196 False, # followcopies
197 197 )
198 198
199 199 for file, (action, info, msg) in actions.iteritems():
200 200 if source.targetfilebelongstosource(file):
201 201 # If the file belongs to the source repo, ignore the p2
202 202 # since it will be covered by the existing fileset.
203 203 continue
204 204
205 205 # If the file requires actual merging, abort. We don't have enough
206 206 # context to resolve merges correctly.
207 207 if action in ['m', 'dm', 'cd', 'dc']:
208 208 raise error.Abort(_("unable to convert merge commit "
209 209 "since target parents do not merge cleanly (file "
210 210 "%s, parents %s and %s)") % (file, p1ctx,
211 211 p2ctx))
212 212 elif action == 'k':
213 213 # 'keep' means nothing changed from p1
214 214 continue
215 215 else:
216 216 # Any other change means we want to take the p2 version
217 217 yield file
218 218
219 219 def putcommit(self, files, copies, parents, commit, source, revmap, full,
220 220 cleanp2):
221 221 files = dict(files)
222 222
223 223 def getfilectx(repo, memctx, f):
224 224 if p2ctx and f in p2files and f not in copies:
225 225 self.ui.debug('reusing %s from p2\n' % f)
226 226 try:
227 227 return p2ctx[f]
228 228 except error.ManifestLookupError:
229 229 # If the file doesn't exist in p2, then we're syncing a
230 230 # delete, so just return None.
231 231 return None
232 232 try:
233 233 v = files[f]
234 234 except KeyError:
235 235 return None
236 236 data, mode = source.getfile(f, v)
237 237 if data is None:
238 238 return None
239 239 if f == '.hgtags':
240 240 data = self._rewritetags(source, revmap, data)
241 241 if f == '.hgsubstate':
242 242 data = self._rewritesubstate(source, data)
243 243 return context.memfilectx(self.repo, f, data, 'l' in mode,
244 244 'x' in mode, copies.get(f))
245 245
246 246 pl = []
247 247 for p in parents:
248 248 if p not in pl:
249 249 pl.append(p)
250 250 parents = pl
251 251 nparents = len(parents)
252 252 if self.filemapmode and nparents == 1:
253 253 m1node = self.repo.changelog.read(bin(parents[0]))[0]
254 254 parent = parents[0]
255 255
256 256 if len(parents) < 2:
257 257 parents.append(nullid)
258 258 if len(parents) < 2:
259 259 parents.append(nullid)
260 260 p2 = parents.pop(0)
261 261
262 262 text = commit.desc
263 263
264 264 sha1s = re.findall(sha1re, text)
265 265 for sha1 in sha1s:
266 266 oldrev = source.lookuprev(sha1)
267 267 newrev = revmap.get(oldrev)
268 268 if newrev is not None:
269 269 text = text.replace(sha1, newrev[:len(sha1)])
270 270
271 271 extra = commit.extra.copy()
272 272
273 273 sourcename = self.repo.ui.config('convert', 'hg.sourcename')
274 274 if sourcename:
275 275 extra['convert_source'] = sourcename
276 276
277 277 for label in ('source', 'transplant_source', 'rebase_source',
278 278 'intermediate-source'):
279 279 node = extra.get(label)
280 280
281 281 if node is None:
282 282 continue
283 283
284 284 # Only transplant stores its reference in binary
285 285 if label == 'transplant_source':
286 286 node = hex(node)
287 287
288 288 newrev = revmap.get(node)
289 289 if newrev is not None:
290 290 if label == 'transplant_source':
291 291 newrev = bin(newrev)
292 292
293 293 extra[label] = newrev
294 294
295 295 if self.branchnames and commit.branch:
296 296 extra['branch'] = commit.branch
297 297 if commit.rev and commit.saverev:
298 298 extra['convert_revision'] = commit.rev
299 299
300 300 while parents:
301 301 p1 = p2
302 302 p2 = parents.pop(0)
303 303 p1ctx = self.repo[p1]
304 304 p2ctx = None
305 305 if p2 != nullid:
306 306 p2ctx = self.repo[p2]
307 307 fileset = set(files)
308 308 if full:
309 309 fileset.update(self.repo[p1])
310 310 fileset.update(self.repo[p2])
311 311
312 312 if p2ctx:
313 313 p2files = set(cleanp2)
314 314 for file in self._calculatemergedfiles(source, p1ctx, p2ctx):
315 315 p2files.add(file)
316 316 fileset.add(file)
317 317
318 318 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
319 319 getfilectx, commit.author, commit.date, extra)
320 320
321 321 # We won't know if the conversion changes the node until after the
322 322 # commit, so copy the source's phase for now.
323 323 self.repo.ui.setconfig('phases', 'new-commit',
324 324 phases.phasenames[commit.phase], 'convert')
325 325
326 326 tr = self.repo.transaction("convert")
327 327
328 328 try:
329 329 node = hex(self.repo.commitctx(ctx))
330 330
331 331 # If the node value has changed, but the phase is lower than
332 332 # draft, set it back to draft since it hasn't been exposed
333 333 # anywhere.
334 334 if commit.rev != node:
335 335 ctx = self.repo[node]
336 336 if ctx.phase() < phases.draft:
337 337 phases.retractboundary(self.repo, tr, phases.draft,
338 338 [ctx.node()])
339 339 tr.close()
340 340 finally:
341 341 tr.release()
342 342
343 343 text = "(octopus merge fixup)\n"
344 344 p2 = node
345 345
346 346 if self.filemapmode and nparents == 1:
347 347 man = self.repo.manifest
348 348 mnode = self.repo.changelog.read(bin(p2))[0]
349 349 closed = 'close' in commit.extra
350 350 if not closed and not man.cmp(m1node, man.revision(mnode)):
351 351 self.ui.status(_("filtering out empty revision\n"))
352 352 self.repo.rollback(force=True)
353 353 return parent
354 354 return p2
355 355
356 356 def puttags(self, tags):
357 357 try:
358 358 parentctx = self.repo[self.tagsbranch]
359 359 tagparent = parentctx.node()
360 360 except error.RepoError:
361 361 parentctx = None
362 362 tagparent = nullid
363 363
364 364 oldlines = set()
365 365 for branch, heads in self.repo.branchmap().iteritems():
366 366 for h in heads:
367 367 if '.hgtags' in self.repo[h]:
368 368 oldlines.update(
369 369 set(self.repo[h]['.hgtags'].data().splitlines(True)))
370 370 oldlines = sorted(list(oldlines))
371 371
372 372 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
373 373 if newlines == oldlines:
374 374 return None, None
375 375
376 376 # if the old and new tags match, then there is nothing to update
377 377 oldtags = set()
378 378 newtags = set()
379 379 for line in oldlines:
380 380 s = line.strip().split(' ', 1)
381 381 if len(s) != 2:
382 382 continue
383 383 oldtags.add(s[1])
384 384 for line in newlines:
385 385 s = line.strip().split(' ', 1)
386 386 if len(s) != 2:
387 387 continue
388 388 if s[1] not in oldtags:
389 389 newtags.add(s[1].strip())
390 390
391 391 if not newtags:
392 392 return None, None
393 393
394 394 data = "".join(newlines)
395 395 def getfilectx(repo, memctx, f):
396 396 return context.memfilectx(repo, f, data, False, False, None)
397 397
398 398 self.ui.status(_("updating tags\n"))
399 399 date = "%s 0" % int(time.mktime(time.gmtime()))
400 400 extra = {'branch': self.tagsbranch}
401 401 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
402 402 [".hgtags"], getfilectx, "convert-repo", date,
403 403 extra)
404 404 node = self.repo.commitctx(ctx)
405 405 return hex(node), hex(tagparent)
406 406
407 407 def setfilemapmode(self, active):
408 408 self.filemapmode = active
409 409
410 410 def putbookmarks(self, updatedbookmark):
411 411 if not len(updatedbookmark):
412 412 return
413 413 wlock = lock = tr = None
414 414 try:
415 415 wlock = self.repo.wlock()
416 416 lock = self.repo.lock()
417 417 tr = self.repo.transaction('bookmark')
418 418 self.ui.status(_("updating bookmarks\n"))
419 419 destmarks = self.repo._bookmarks
420 420 for bookmark in updatedbookmark:
421 421 destmarks[bookmark] = bin(updatedbookmark[bookmark])
422 422 destmarks.recordchange(tr)
423 423 tr.close()
424 424 finally:
425 425 lockmod.release(lock, wlock, tr)
426 426
427 427 def hascommitfrommap(self, rev):
428 428 # the exact semantics of clonebranches is unclear so we can't say no
429 429 return rev in self.repo or self.clonebranches
430 430
431 431 def hascommitforsplicemap(self, rev):
432 432 if rev not in self.repo and self.clonebranches:
433 433 raise error.Abort(_('revision %s not found in destination '
434 434 'repository (lookups with clonebranches=true '
435 435 'are not implemented)') % rev)
436 436 return rev in self.repo
437 437
438 438 class mercurial_source(converter_source):
439 439 def __init__(self, ui, path, revs=None):
440 440 converter_source.__init__(self, ui, path, revs)
441 441 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
442 442 self.ignored = set()
443 443 self.saverev = ui.configbool('convert', 'hg.saverev', False)
444 444 try:
445 445 self.repo = hg.repository(self.ui, path)
446 446 # try to provoke an exception if this isn't really a hg
447 447 # repo, but some other bogus compatible-looking url
448 448 if not self.repo.local():
449 449 raise error.RepoError
450 450 except error.RepoError:
451 451 ui.traceback()
452 452 raise NoRepo(_("%s is not a local Mercurial repository") % path)
453 453 self.lastrev = None
454 454 self.lastctx = None
455 455 self._changescache = None, None
456 456 self.convertfp = None
457 457 # Restrict converted revisions to startrev descendants
458 458 startnode = ui.config('convert', 'hg.startrev')
459 459 hgrevs = ui.config('convert', 'hg.revs')
460 460 if hgrevs is None:
461 461 if startnode is not None:
462 462 try:
463 463 startnode = self.repo.lookup(startnode)
464 464 except error.RepoError:
465 465 raise error.Abort(_('%s is not a valid start revision')
466 466 % startnode)
467 467 startrev = self.repo.changelog.rev(startnode)
468 468 children = {startnode: 1}
469 469 for r in self.repo.changelog.descendants([startrev]):
470 470 children[self.repo.changelog.node(r)] = 1
471 471 self.keep = children.__contains__
472 472 else:
473 473 self.keep = util.always
474 474 if revs:
475 475 self._heads = [self.repo[r].node() for r in revs]
476 476 else:
477 477 self._heads = self.repo.heads()
478 478 else:
479 479 if revs or startnode is not None:
480 480 raise error.Abort(_('hg.revs cannot be combined with '
481 481 'hg.startrev or --rev'))
482 482 nodes = set()
483 483 parents = set()
484 484 for r in scmutil.revrange(self.repo, [hgrevs]):
485 485 ctx = self.repo[r]
486 486 nodes.add(ctx.node())
487 487 parents.update(p.node() for p in ctx.parents())
488 488 self.keep = nodes.__contains__
489 489 self._heads = nodes - parents
490 490
491 491 def _changectx(self, rev):
492 492 if self.lastrev != rev:
493 493 self.lastctx = self.repo[rev]
494 494 self.lastrev = rev
495 495 return self.lastctx
496 496
497 497 def _parents(self, ctx):
498 498 return [p for p in ctx.parents() if p and self.keep(p.node())]
499 499
500 500 def getheads(self):
501 501 return [hex(h) for h in self._heads if self.keep(h)]
502 502
503 503 def getfile(self, name, rev):
504 504 try:
505 505 fctx = self._changectx(rev)[name]
506 506 return fctx.data(), fctx.flags()
507 507 except error.LookupError:
508 508 return None, None
509 509
510 def _changedfiles(self, ctx1, ctx2):
511 m, a, r = ctx1.status(ctx2)[:3]
512 return (m + a, r)
513
510 514 def getchanges(self, rev, full):
511 515 ctx = self._changectx(rev)
512 516 parents = self._parents(ctx)
513 517 if full or not parents:
514 518 files = copyfiles = ctx.manifest()
515 519 if parents:
516 520 if self._changescache[0] == rev:
517 m, a, r = self._changescache[1]
521 ma, r = self._changescache[1]
518 522 else:
519 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
523 ma, r = self._changedfiles(parents[0], ctx)
520 524 if not full:
521 files = m + a + r
522 copyfiles = m + a
525 files = ma + r
526 copyfiles = ma
523 527 # _getcopies() is also run for roots and before filtering so missing
524 528 # revlogs are detected early
525 529 copies = self._getcopies(ctx, parents, copyfiles)
526 530 cleanp2 = set()
527 531 if len(parents) == 2:
528 532 cleanp2.update(self.repo.status(parents[1].node(), ctx.node(),
529 533 clean=True).clean)
530 534 changes = [(f, rev) for f in files if f not in self.ignored]
531 535 changes.sort()
532 536 return changes, copies, cleanp2
533 537
534 538 def _getcopies(self, ctx, parents, files):
535 539 copies = {}
536 540 for name in files:
537 541 if name in self.ignored:
538 542 continue
539 543 try:
540 544 copysource, _copynode = ctx.filectx(name).renamed()
541 545 if copysource in self.ignored:
542 546 continue
543 547 # Ignore copy sources not in parent revisions
544 548 found = False
545 549 for p in parents:
546 550 if copysource in p:
547 551 found = True
548 552 break
549 553 if not found:
550 554 continue
551 555 copies[name] = copysource
552 556 except TypeError:
553 557 pass
554 558 except error.LookupError as e:
555 559 if not self.ignoreerrors:
556 560 raise
557 561 self.ignored.add(name)
558 562 self.ui.warn(_('ignoring: %s\n') % e)
559 563 return copies
560 564
561 565 def getcommit(self, rev):
562 566 ctx = self._changectx(rev)
563 567 parents = [p.hex() for p in self._parents(ctx)]
564 568 crev = rev
565 569
566 570 return commit(author=ctx.user(),
567 571 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
568 572 desc=ctx.description(), rev=crev, parents=parents,
569 573 branch=ctx.branch(), extra=ctx.extra(),
570 574 sortkey=ctx.rev(), saverev=self.saverev,
571 575 phase=ctx.phase())
572 576
573 577 def gettags(self):
574 578 # This will get written to .hgtags, filter non global tags out.
575 579 tags = [t for t in self.repo.tagslist()
576 580 if self.repo.tagtype(t[0]) == 'global']
577 581 return dict([(name, hex(node)) for name, node in tags
578 582 if self.keep(node)])
579 583
580 584 def getchangedfiles(self, rev, i):
581 585 ctx = self._changectx(rev)
582 586 parents = self._parents(ctx)
583 587 if not parents and i is None:
584 588 i = 0
585 changes = [], ctx.manifest().keys(), []
589 ma, r = ctx.manifest().keys(), []
586 590 else:
587 591 i = i or 0
588 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
589 changes = [[f for f in l if f not in self.ignored] for l in changes]
592 ma, r = self._changedfiles(parents[i], ctx)
593 ma, r = [[f for f in l if f not in self.ignored] for l in (ma, r)]
590 594
591 595 if i == 0:
592 self._changescache = (rev, changes)
596 self._changescache = (rev, (ma, r))
593 597
594 return changes[0] + changes[1] + changes[2]
598 return ma + r
595 599
596 600 def converted(self, rev, destrev):
597 601 if self.convertfp is None:
598 602 self.convertfp = open(self.repo.join('shamap'), 'a')
599 603 self.convertfp.write('%s %s\n' % (destrev, rev))
600 604 self.convertfp.flush()
601 605
602 606 def before(self):
603 607 self.ui.debug('run hg source pre-conversion action\n')
604 608
605 609 def after(self):
606 610 self.ui.debug('run hg source post-conversion action\n')
607 611
608 612 def hasnativeorder(self):
609 613 return True
610 614
611 615 def hasnativeclose(self):
612 616 return True
613 617
614 618 def lookuprev(self, rev):
615 619 try:
616 620 return hex(self.repo.lookup(rev))
617 621 except (error.RepoError, error.LookupError):
618 622 return None
619 623
620 624 def getbookmarks(self):
621 625 return bookmarks.listbookmarks(self.repo)
622 626
623 627 def checkrevformat(self, revstr, mapname='splicemap'):
624 628 """ Mercurial, revision string is a 40 byte hex """
625 629 self.checkhexformat(revstr, mapname)
General Comments 0
You need to be logged in to leave comments. Login now