/hgext/convert/hg.py

https://bitbucket.org/mirror/mercurial/ · Python · 470 lines · 374 code · 63 blank · 33 comment · 130 complexity · 7af81c935711d01a1f1e53abb44ddf1e MD5 · raw file

  1. # hg.py - hg backend for convert extension
  2. #
  3. # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2 or any later version.
  7. # Notes for hg->hg conversion:
  8. #
  9. # * Old versions of Mercurial didn't trim the whitespace from the ends
  10. # of commit messages, but new versions do. Changesets created by
  11. # those older versions, then converted, may thus have different
  12. # hashes for changesets that are otherwise identical.
  13. #
  14. # * Using "--config convert.hg.saverev=true" will make the source
  15. # identifier to be stored in the converted revision. This will cause
  16. # the converted revision to have a different identity than the
  17. # source.
  18. import os, time, cStringIO
  19. from mercurial.i18n import _
  20. from mercurial.node import bin, hex, nullid
  21. from mercurial import hg, util, context, bookmarks, error, scmutil
  22. from common import NoRepo, commit, converter_source, converter_sink
  23. import re
  24. sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
  25. class mercurial_sink(converter_sink):
  26. def __init__(self, ui, path):
  27. converter_sink.__init__(self, ui, path)
  28. self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
  29. self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
  30. self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
  31. self.lastbranch = None
  32. if os.path.isdir(path) and len(os.listdir(path)) > 0:
  33. try:
  34. self.repo = hg.repository(self.ui, path)
  35. if not self.repo.local():
  36. raise NoRepo(_('%s is not a local Mercurial repository')
  37. % path)
  38. except error.RepoError, err:
  39. ui.traceback()
  40. raise NoRepo(err.args[0])
  41. else:
  42. try:
  43. ui.status(_('initializing destination %s repository\n') % path)
  44. self.repo = hg.repository(self.ui, path, create=True)
  45. if not self.repo.local():
  46. raise NoRepo(_('%s is not a local Mercurial repository')
  47. % path)
  48. self.created.append(path)
  49. except error.RepoError:
  50. ui.traceback()
  51. raise NoRepo(_("could not create hg repository %s as sink")
  52. % path)
  53. self.lock = None
  54. self.wlock = None
  55. self.filemapmode = False
  56. def before(self):
  57. self.ui.debug('run hg sink pre-conversion action\n')
  58. self.wlock = self.repo.wlock()
  59. self.lock = self.repo.lock()
  60. def after(self):
  61. self.ui.debug('run hg sink post-conversion action\n')
  62. if self.lock:
  63. self.lock.release()
  64. if self.wlock:
  65. self.wlock.release()
  66. def revmapfile(self):
  67. return self.repo.join("shamap")
  68. def authorfile(self):
  69. return self.repo.join("authormap")
  70. def setbranch(self, branch, pbranches):
  71. if not self.clonebranches:
  72. return
  73. setbranch = (branch != self.lastbranch)
  74. self.lastbranch = branch
  75. if not branch:
  76. branch = 'default'
  77. pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
  78. pbranch = pbranches and pbranches[0][1] or 'default'
  79. branchpath = os.path.join(self.path, branch)
  80. if setbranch:
  81. self.after()
  82. try:
  83. self.repo = hg.repository(self.ui, branchpath)
  84. except Exception:
  85. self.repo = hg.repository(self.ui, branchpath, create=True)
  86. self.before()
  87. # pbranches may bring revisions from other branches (merge parents)
  88. # Make sure we have them, or pull them.
  89. missings = {}
  90. for b in pbranches:
  91. try:
  92. self.repo.lookup(b[0])
  93. except Exception:
  94. missings.setdefault(b[1], []).append(b[0])
  95. if missings:
  96. self.after()
  97. for pbranch, heads in sorted(missings.iteritems()):
  98. pbranchpath = os.path.join(self.path, pbranch)
  99. prepo = hg.peer(self.ui, {}, pbranchpath)
  100. self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
  101. self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
  102. self.before()
  103. def _rewritetags(self, source, revmap, data):
  104. fp = cStringIO.StringIO()
  105. for line in data.splitlines():
  106. s = line.split(' ', 1)
  107. if len(s) != 2:
  108. continue
  109. revid = revmap.get(source.lookuprev(s[0]))
  110. if not revid:
  111. continue
  112. fp.write('%s %s\n' % (revid, s[1]))
  113. return fp.getvalue()
  114. def putcommit(self, files, copies, parents, commit, source, revmap):
  115. files = dict(files)
  116. def getfilectx(repo, memctx, f):
  117. v = files[f]
  118. data, mode = source.getfile(f, v)
  119. if f == '.hgtags':
  120. data = self._rewritetags(source, revmap, data)
  121. return context.memfilectx(self.repo, f, data, 'l' in mode,
  122. 'x' in mode, copies.get(f))
  123. pl = []
  124. for p in parents:
  125. if p not in pl:
  126. pl.append(p)
  127. parents = pl
  128. nparents = len(parents)
  129. if self.filemapmode and nparents == 1:
  130. m1node = self.repo.changelog.read(bin(parents[0]))[0]
  131. parent = parents[0]
  132. if len(parents) < 2:
  133. parents.append(nullid)
  134. if len(parents) < 2:
  135. parents.append(nullid)
  136. p2 = parents.pop(0)
  137. text = commit.desc
  138. sha1s = re.findall(sha1re, text)
  139. for sha1 in sha1s:
  140. oldrev = source.lookuprev(sha1)
  141. newrev = revmap.get(oldrev)
  142. if newrev is not None:
  143. text = text.replace(sha1, newrev[:len(sha1)])
  144. extra = commit.extra.copy()
  145. for label in ('source', 'transplant_source', 'rebase_source'):
  146. node = extra.get(label)
  147. if node is None:
  148. continue
  149. # Only transplant stores its reference in binary
  150. if label == 'transplant_source':
  151. node = hex(node)
  152. newrev = revmap.get(node)
  153. if newrev is not None:
  154. if label == 'transplant_source':
  155. newrev = bin(newrev)
  156. extra[label] = newrev
  157. if self.branchnames and commit.branch:
  158. extra['branch'] = commit.branch
  159. if commit.rev:
  160. extra['convert_revision'] = commit.rev
  161. while parents:
  162. p1 = p2
  163. p2 = parents.pop(0)
  164. ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
  165. getfilectx, commit.author, commit.date, extra)
  166. self.repo.commitctx(ctx)
  167. text = "(octopus merge fixup)\n"
  168. p2 = hex(self.repo.changelog.tip())
  169. if self.filemapmode and nparents == 1:
  170. man = self.repo.manifest
  171. mnode = self.repo.changelog.read(bin(p2))[0]
  172. closed = 'close' in commit.extra
  173. if not closed and not man.cmp(m1node, man.revision(mnode)):
  174. self.ui.status(_("filtering out empty revision\n"))
  175. self.repo.rollback(force=True)
  176. return parent
  177. return p2
  178. def puttags(self, tags):
  179. try:
  180. parentctx = self.repo[self.tagsbranch]
  181. tagparent = parentctx.node()
  182. except error.RepoError:
  183. parentctx = None
  184. tagparent = nullid
  185. oldlines = set()
  186. for branch, heads in self.repo.branchmap().iteritems():
  187. for h in heads:
  188. if '.hgtags' in self.repo[h]:
  189. oldlines.update(
  190. set(self.repo[h]['.hgtags'].data().splitlines(True)))
  191. oldlines = sorted(list(oldlines))
  192. newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
  193. if newlines == oldlines:
  194. return None, None
  195. # if the old and new tags match, then there is nothing to update
  196. oldtags = set()
  197. newtags = set()
  198. for line in oldlines:
  199. s = line.strip().split(' ', 1)
  200. if len(s) != 2:
  201. continue
  202. oldtags.add(s[1])
  203. for line in newlines:
  204. s = line.strip().split(' ', 1)
  205. if len(s) != 2:
  206. continue
  207. if s[1] not in oldtags:
  208. newtags.add(s[1].strip())
  209. if not newtags:
  210. return None, None
  211. data = "".join(newlines)
  212. def getfilectx(repo, memctx, f):
  213. return context.memfilectx(repo, f, data, False, False, None)
  214. self.ui.status(_("updating tags\n"))
  215. date = "%s 0" % int(time.mktime(time.gmtime()))
  216. extra = {'branch': self.tagsbranch}
  217. ctx = context.memctx(self.repo, (tagparent, None), "update tags",
  218. [".hgtags"], getfilectx, "convert-repo", date,
  219. extra)
  220. self.repo.commitctx(ctx)
  221. return hex(self.repo.changelog.tip()), hex(tagparent)
  222. def setfilemapmode(self, active):
  223. self.filemapmode = active
  224. def putbookmarks(self, updatedbookmark):
  225. if not len(updatedbookmark):
  226. return
  227. self.ui.status(_("updating bookmarks\n"))
  228. destmarks = self.repo._bookmarks
  229. for bookmark in updatedbookmark:
  230. destmarks[bookmark] = bin(updatedbookmark[bookmark])
  231. destmarks.write()
  232. def hascommitfrommap(self, rev):
  233. # the exact semantics of clonebranches is unclear so we can't say no
  234. return rev in self.repo or self.clonebranches
  235. def hascommitforsplicemap(self, rev):
  236. if rev not in self.repo and self.clonebranches:
  237. raise util.Abort(_('revision %s not found in destination '
  238. 'repository (lookups with clonebranches=true '
  239. 'are not implemented)') % rev)
  240. return rev in self.repo
  241. class mercurial_source(converter_source):
  242. def __init__(self, ui, path, rev=None):
  243. converter_source.__init__(self, ui, path, rev)
  244. self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
  245. self.ignored = set()
  246. self.saverev = ui.configbool('convert', 'hg.saverev', False)
  247. try:
  248. self.repo = hg.repository(self.ui, path)
  249. # try to provoke an exception if this isn't really a hg
  250. # repo, but some other bogus compatible-looking url
  251. if not self.repo.local():
  252. raise error.RepoError
  253. except error.RepoError:
  254. ui.traceback()
  255. raise NoRepo(_("%s is not a local Mercurial repository") % path)
  256. self.lastrev = None
  257. self.lastctx = None
  258. self._changescache = None
  259. self.convertfp = None
  260. # Restrict converted revisions to startrev descendants
  261. startnode = ui.config('convert', 'hg.startrev')
  262. hgrevs = ui.config('convert', 'hg.revs')
  263. if hgrevs is None:
  264. if startnode is not None:
  265. try:
  266. startnode = self.repo.lookup(startnode)
  267. except error.RepoError:
  268. raise util.Abort(_('%s is not a valid start revision')
  269. % startnode)
  270. startrev = self.repo.changelog.rev(startnode)
  271. children = {startnode: 1}
  272. for r in self.repo.changelog.descendants([startrev]):
  273. children[self.repo.changelog.node(r)] = 1
  274. self.keep = children.__contains__
  275. else:
  276. self.keep = util.always
  277. if rev:
  278. self._heads = [self.repo[rev].node()]
  279. else:
  280. self._heads = self.repo.heads()
  281. else:
  282. if rev or startnode is not None:
  283. raise util.Abort(_('hg.revs cannot be combined with '
  284. 'hg.startrev or --rev'))
  285. nodes = set()
  286. parents = set()
  287. for r in scmutil.revrange(self.repo, [hgrevs]):
  288. ctx = self.repo[r]
  289. nodes.add(ctx.node())
  290. parents.update(p.node() for p in ctx.parents())
  291. self.keep = nodes.__contains__
  292. self._heads = nodes - parents
  293. def changectx(self, rev):
  294. if self.lastrev != rev:
  295. self.lastctx = self.repo[rev]
  296. self.lastrev = rev
  297. return self.lastctx
  298. def parents(self, ctx):
  299. return [p for p in ctx.parents() if p and self.keep(p.node())]
  300. def getheads(self):
  301. return [hex(h) for h in self._heads if self.keep(h)]
  302. def getfile(self, name, rev):
  303. try:
  304. fctx = self.changectx(rev)[name]
  305. return fctx.data(), fctx.flags()
  306. except error.LookupError, err:
  307. raise IOError(err)
  308. def getchanges(self, rev):
  309. ctx = self.changectx(rev)
  310. parents = self.parents(ctx)
  311. if not parents:
  312. files = sorted(ctx.manifest())
  313. # getcopies() is not needed for roots, but it is a simple way to
  314. # detect missing revlogs and abort on errors or populate
  315. # self.ignored
  316. self.getcopies(ctx, parents, files)
  317. return [(f, rev) for f in files if f not in self.ignored], {}
  318. if self._changescache and self._changescache[0] == rev:
  319. m, a, r = self._changescache[1]
  320. else:
  321. m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
  322. # getcopies() detects missing revlogs early, run it before
  323. # filtering the changes.
  324. copies = self.getcopies(ctx, parents, m + a)
  325. changes = [(name, rev) for name in m + a + r
  326. if name not in self.ignored]
  327. return sorted(changes), copies
  328. def getcopies(self, ctx, parents, files):
  329. copies = {}
  330. for name in files:
  331. if name in self.ignored:
  332. continue
  333. try:
  334. copysource, _copynode = ctx.filectx(name).renamed()
  335. if copysource in self.ignored:
  336. continue
  337. # Ignore copy sources not in parent revisions
  338. found = False
  339. for p in parents:
  340. if copysource in p:
  341. found = True
  342. break
  343. if not found:
  344. continue
  345. copies[name] = copysource
  346. except TypeError:
  347. pass
  348. except error.LookupError, e:
  349. if not self.ignoreerrors:
  350. raise
  351. self.ignored.add(name)
  352. self.ui.warn(_('ignoring: %s\n') % e)
  353. return copies
  354. def getcommit(self, rev):
  355. ctx = self.changectx(rev)
  356. parents = [p.hex() for p in self.parents(ctx)]
  357. if self.saverev:
  358. crev = rev
  359. else:
  360. crev = None
  361. return commit(author=ctx.user(),
  362. date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
  363. desc=ctx.description(), rev=crev, parents=parents,
  364. branch=ctx.branch(), extra=ctx.extra(),
  365. sortkey=ctx.rev())
  366. def gettags(self):
  367. # This will get written to .hgtags, filter non global tags out.
  368. tags = [t for t in self.repo.tagslist()
  369. if self.repo.tagtype(t[0]) == 'global']
  370. return dict([(name, hex(node)) for name, node in tags
  371. if self.keep(node)])
  372. def getchangedfiles(self, rev, i):
  373. ctx = self.changectx(rev)
  374. parents = self.parents(ctx)
  375. if not parents and i is None:
  376. i = 0
  377. changes = [], ctx.manifest().keys(), []
  378. else:
  379. i = i or 0
  380. changes = self.repo.status(parents[i].node(), ctx.node())[:3]
  381. changes = [[f for f in l if f not in self.ignored] for l in changes]
  382. if i == 0:
  383. self._changescache = (rev, changes)
  384. return changes[0] + changes[1] + changes[2]
  385. def converted(self, rev, destrev):
  386. if self.convertfp is None:
  387. self.convertfp = open(self.repo.join('shamap'), 'a')
  388. self.convertfp.write('%s %s\n' % (destrev, rev))
  389. self.convertfp.flush()
  390. def before(self):
  391. self.ui.debug('run hg source pre-conversion action\n')
  392. def after(self):
  393. self.ui.debug('run hg source post-conversion action\n')
  394. def hasnativeorder(self):
  395. return True
  396. def hasnativeclose(self):
  397. return True
  398. def lookuprev(self, rev):
  399. try:
  400. return hex(self.repo.lookup(rev))
  401. except error.RepoError:
  402. return None
  403. def getbookmarks(self):
  404. return bookmarks.listbookmarks(self.repo)
  405. def checkrevformat(self, revstr, mapname='splicemap'):
  406. """ Mercurial, revision string is a 40 byte hex """
  407. self.checkhexformat(revstr, mapname)