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