##// END OF EJS Templates
convert: indentation change to make the next patch more legible...
Laurent Charignon -
r26973:fdd63acf default
parent child Browse files
Show More
@@ -1,618 +1,618 b''
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 merge as mergemod
27 27
28 28 from common import NoRepo, commit, converter_source, converter_sink, mapfile
29 29
30 30 import re
31 31 sha1re = re.compile(r'\b[0-9a-f]{12,40}\b')
32 32
33 33 class mercurial_sink(converter_sink):
34 34 def __init__(self, ui, path):
35 35 converter_sink.__init__(self, ui, path)
36 36 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
37 37 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
38 38 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
39 39 self.lastbranch = None
40 40 if os.path.isdir(path) and len(os.listdir(path)) > 0:
41 41 try:
42 42 self.repo = hg.repository(self.ui, path)
43 43 if not self.repo.local():
44 44 raise NoRepo(_('%s is not a local Mercurial repository')
45 45 % path)
46 46 except error.RepoError as err:
47 47 ui.traceback()
48 48 raise NoRepo(err.args[0])
49 49 else:
50 50 try:
51 51 ui.status(_('initializing destination %s repository\n') % path)
52 52 self.repo = hg.repository(self.ui, path, create=True)
53 53 if not self.repo.local():
54 54 raise NoRepo(_('%s is not a local Mercurial repository')
55 55 % path)
56 56 self.created.append(path)
57 57 except error.RepoError:
58 58 ui.traceback()
59 59 raise NoRepo(_("could not create hg repository %s as sink")
60 60 % path)
61 61 self.lock = None
62 62 self.wlock = None
63 63 self.filemapmode = False
64 64 self.subrevmaps = {}
65 65
66 66 def before(self):
67 67 self.ui.debug('run hg sink pre-conversion action\n')
68 68 self.wlock = self.repo.wlock()
69 69 self.lock = self.repo.lock()
70 70
71 71 def after(self):
72 72 self.ui.debug('run hg sink post-conversion action\n')
73 73 if self.lock:
74 74 self.lock.release()
75 75 if self.wlock:
76 76 self.wlock.release()
77 77
78 78 def revmapfile(self):
79 79 return self.repo.join("shamap")
80 80
81 81 def authorfile(self):
82 82 return self.repo.join("authormap")
83 83
84 84 def setbranch(self, branch, pbranches):
85 85 if not self.clonebranches:
86 86 return
87 87
88 88 setbranch = (branch != self.lastbranch)
89 89 self.lastbranch = branch
90 90 if not branch:
91 91 branch = 'default'
92 92 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
93 93 if pbranches:
94 94 pbranch = pbranches[0][1]
95 95 else:
96 96 pbranch = 'default'
97 97
98 98 branchpath = os.path.join(self.path, branch)
99 99 if setbranch:
100 100 self.after()
101 101 try:
102 102 self.repo = hg.repository(self.ui, branchpath)
103 103 except Exception:
104 104 self.repo = hg.repository(self.ui, branchpath, create=True)
105 105 self.before()
106 106
107 107 # pbranches may bring revisions from other branches (merge parents)
108 108 # Make sure we have them, or pull them.
109 109 missings = {}
110 110 for b in pbranches:
111 111 try:
112 112 self.repo.lookup(b[0])
113 113 except Exception:
114 114 missings.setdefault(b[1], []).append(b[0])
115 115
116 116 if missings:
117 117 self.after()
118 118 for pbranch, heads in sorted(missings.iteritems()):
119 119 pbranchpath = os.path.join(self.path, pbranch)
120 120 prepo = hg.peer(self.ui, {}, pbranchpath)
121 121 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
122 122 exchange.pull(self.repo, prepo,
123 123 [prepo.lookup(h) for h in heads])
124 124 self.before()
125 125
126 126 def _rewritetags(self, source, revmap, data):
127 127 fp = cStringIO.StringIO()
128 128 for line in data.splitlines():
129 129 s = line.split(' ', 1)
130 130 if len(s) != 2:
131 131 continue
132 132 revid = revmap.get(source.lookuprev(s[0]))
133 133 if not revid:
134 134 if s[0] == hex(nullid):
135 135 revid = s[0]
136 136 else:
137 137 continue
138 138 fp.write('%s %s\n' % (revid, s[1]))
139 139 return fp.getvalue()
140 140
141 141 def _rewritesubstate(self, source, data):
142 142 fp = cStringIO.StringIO()
143 143 for line in data.splitlines():
144 144 s = line.split(' ', 1)
145 145 if len(s) != 2:
146 146 continue
147 147
148 148 revid = s[0]
149 149 subpath = s[1]
150 150 if revid != hex(nullid):
151 151 revmap = self.subrevmaps.get(subpath)
152 152 if revmap is None:
153 153 revmap = mapfile(self.ui,
154 154 self.repo.wjoin(subpath, '.hg/shamap'))
155 155 self.subrevmaps[subpath] = revmap
156 156
157 157 # It is reasonable that one or more of the subrepos don't
158 158 # need to be converted, in which case they can be cloned
159 159 # into place instead of converted. Therefore, only warn
160 160 # once.
161 161 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
162 162 if len(revmap) == 0:
163 163 sub = self.repo.wvfs.reljoin(subpath, '.hg')
164 164
165 165 if self.repo.wvfs.exists(sub):
166 166 self.ui.warn(msg % subpath)
167 167
168 168 newid = revmap.get(revid)
169 169 if not newid:
170 170 if len(revmap) > 0:
171 171 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
172 172 (revid, subpath))
173 173 else:
174 174 revid = newid
175 175
176 176 fp.write('%s %s\n' % (revid, subpath))
177 177
178 178 return fp.getvalue()
179 179
180 180 def _calculatemergedfiles(self, source, p1ctx, p2ctx):
181 181 """Calculates the files from p2 that we need to pull in when merging p1
182 182 and p2, given that the merge is coming from the given source.
183 183
184 184 This prevents us from losing files that only exist in the target p2 and
185 185 that don't come from the source repo (like if you're merging multiple
186 186 repositories together).
187 187 """
188 188 anc = [p1ctx.ancestor(p2ctx)]
189 189 # Calculate what files are coming from p2
190 190 actions, diverge, rename = mergemod.calculateupdates(
191 191 self.repo, p1ctx, p2ctx, anc,
192 192 True, # branchmerge
193 193 True, # force
194 194 False, # partial
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 if True:
414 414 self.ui.status(_("updating bookmarks\n"))
415 415 destmarks = self.repo._bookmarks
416 416 for bookmark in updatedbookmark:
417 417 destmarks[bookmark] = bin(updatedbookmark[bookmark])
418 418 destmarks.write()
419 419
420 420 def hascommitfrommap(self, rev):
421 421 # the exact semantics of clonebranches is unclear so we can't say no
422 422 return rev in self.repo or self.clonebranches
423 423
424 424 def hascommitforsplicemap(self, rev):
425 425 if rev not in self.repo and self.clonebranches:
426 426 raise error.Abort(_('revision %s not found in destination '
427 427 'repository (lookups with clonebranches=true '
428 428 'are not implemented)') % rev)
429 429 return rev in self.repo
430 430
431 431 class mercurial_source(converter_source):
432 432 def __init__(self, ui, path, revs=None):
433 433 converter_source.__init__(self, ui, path, revs)
434 434 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
435 435 self.ignored = set()
436 436 self.saverev = ui.configbool('convert', 'hg.saverev', False)
437 437 try:
438 438 self.repo = hg.repository(self.ui, path)
439 439 # try to provoke an exception if this isn't really a hg
440 440 # repo, but some other bogus compatible-looking url
441 441 if not self.repo.local():
442 442 raise error.RepoError
443 443 except error.RepoError:
444 444 ui.traceback()
445 445 raise NoRepo(_("%s is not a local Mercurial repository") % path)
446 446 self.lastrev = None
447 447 self.lastctx = None
448 448 self._changescache = None, None
449 449 self.convertfp = None
450 450 # Restrict converted revisions to startrev descendants
451 451 startnode = ui.config('convert', 'hg.startrev')
452 452 hgrevs = ui.config('convert', 'hg.revs')
453 453 if hgrevs is None:
454 454 if startnode is not None:
455 455 try:
456 456 startnode = self.repo.lookup(startnode)
457 457 except error.RepoError:
458 458 raise error.Abort(_('%s is not a valid start revision')
459 459 % startnode)
460 460 startrev = self.repo.changelog.rev(startnode)
461 461 children = {startnode: 1}
462 462 for r in self.repo.changelog.descendants([startrev]):
463 463 children[self.repo.changelog.node(r)] = 1
464 464 self.keep = children.__contains__
465 465 else:
466 466 self.keep = util.always
467 467 if revs:
468 468 self._heads = [self.repo[r].node() for r in revs]
469 469 else:
470 470 self._heads = self.repo.heads()
471 471 else:
472 472 if revs or startnode is not None:
473 473 raise error.Abort(_('hg.revs cannot be combined with '
474 474 'hg.startrev or --rev'))
475 475 nodes = set()
476 476 parents = set()
477 477 for r in scmutil.revrange(self.repo, [hgrevs]):
478 478 ctx = self.repo[r]
479 479 nodes.add(ctx.node())
480 480 parents.update(p.node() for p in ctx.parents())
481 481 self.keep = nodes.__contains__
482 482 self._heads = nodes - parents
483 483
484 484 def changectx(self, rev):
485 485 if self.lastrev != rev:
486 486 self.lastctx = self.repo[rev]
487 487 self.lastrev = rev
488 488 return self.lastctx
489 489
490 490 def parents(self, ctx):
491 491 return [p for p in ctx.parents() if p and self.keep(p.node())]
492 492
493 493 def getheads(self):
494 494 return [hex(h) for h in self._heads if self.keep(h)]
495 495
496 496 def getfile(self, name, rev):
497 497 try:
498 498 fctx = self.changectx(rev)[name]
499 499 return fctx.data(), fctx.flags()
500 500 except error.LookupError:
501 501 return None, None
502 502
503 503 def getchanges(self, rev, full):
504 504 ctx = self.changectx(rev)
505 505 parents = self.parents(ctx)
506 506 if full or not parents:
507 507 files = copyfiles = ctx.manifest()
508 508 if parents:
509 509 if self._changescache[0] == rev:
510 510 m, a, r = self._changescache[1]
511 511 else:
512 512 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
513 513 if not full:
514 514 files = m + a + r
515 515 copyfiles = m + a
516 516 # getcopies() is also run for roots and before filtering so missing
517 517 # revlogs are detected early
518 518 copies = self.getcopies(ctx, parents, copyfiles)
519 519 cleanp2 = set()
520 520 if len(parents) == 2:
521 521 cleanp2.update(self.repo.status(parents[1].node(), ctx.node(),
522 522 clean=True).clean)
523 523 changes = [(f, rev) for f in files if f not in self.ignored]
524 524 changes.sort()
525 525 return changes, copies, cleanp2
526 526
527 527 def getcopies(self, ctx, parents, files):
528 528 copies = {}
529 529 for name in files:
530 530 if name in self.ignored:
531 531 continue
532 532 try:
533 533 copysource, _copynode = ctx.filectx(name).renamed()
534 534 if copysource in self.ignored:
535 535 continue
536 536 # Ignore copy sources not in parent revisions
537 537 found = False
538 538 for p in parents:
539 539 if copysource in p:
540 540 found = True
541 541 break
542 542 if not found:
543 543 continue
544 544 copies[name] = copysource
545 545 except TypeError:
546 546 pass
547 547 except error.LookupError as e:
548 548 if not self.ignoreerrors:
549 549 raise
550 550 self.ignored.add(name)
551 551 self.ui.warn(_('ignoring: %s\n') % e)
552 552 return copies
553 553
554 554 def getcommit(self, rev):
555 555 ctx = self.changectx(rev)
556 556 parents = [p.hex() for p in self.parents(ctx)]
557 557 crev = rev
558 558
559 559 return commit(author=ctx.user(),
560 560 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
561 561 desc=ctx.description(), rev=crev, parents=parents,
562 562 branch=ctx.branch(), extra=ctx.extra(),
563 563 sortkey=ctx.rev(), saverev=self.saverev,
564 564 phase=ctx.phase())
565 565
566 566 def gettags(self):
567 567 # This will get written to .hgtags, filter non global tags out.
568 568 tags = [t for t in self.repo.tagslist()
569 569 if self.repo.tagtype(t[0]) == 'global']
570 570 return dict([(name, hex(node)) for name, node in tags
571 571 if self.keep(node)])
572 572
573 573 def getchangedfiles(self, rev, i):
574 574 ctx = self.changectx(rev)
575 575 parents = self.parents(ctx)
576 576 if not parents and i is None:
577 577 i = 0
578 578 changes = [], ctx.manifest().keys(), []
579 579 else:
580 580 i = i or 0
581 581 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
582 582 changes = [[f for f in l if f not in self.ignored] for l in changes]
583 583
584 584 if i == 0:
585 585 self._changescache = (rev, changes)
586 586
587 587 return changes[0] + changes[1] + changes[2]
588 588
589 589 def converted(self, rev, destrev):
590 590 if self.convertfp is None:
591 591 self.convertfp = open(self.repo.join('shamap'), 'a')
592 592 self.convertfp.write('%s %s\n' % (destrev, rev))
593 593 self.convertfp.flush()
594 594
595 595 def before(self):
596 596 self.ui.debug('run hg source pre-conversion action\n')
597 597
598 598 def after(self):
599 599 self.ui.debug('run hg source post-conversion action\n')
600 600
601 601 def hasnativeorder(self):
602 602 return True
603 603
604 604 def hasnativeclose(self):
605 605 return True
606 606
607 607 def lookuprev(self, rev):
608 608 try:
609 609 return hex(self.repo.lookup(rev))
610 610 except (error.RepoError, error.LookupError):
611 611 return None
612 612
613 613 def getbookmarks(self):
614 614 return bookmarks.listbookmarks(self.repo)
615 615
616 616 def checkrevformat(self, revstr, mapname='splicemap'):
617 617 """ Mercurial, revision string is a 40 byte hex """
618 618 self.checkhexformat(revstr, mapname)
General Comments 0
You need to be logged in to leave comments. Login now