PageRenderTime 41ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/mercurial/bundlerepo.py

https://bitbucket.org/mirror/mercurial/
Python | 400 lines | 359 code | 13 blank | 28 comment | 15 complexity | 8800bc3e55fae7e9ee0f10ac02f408af MD5 | raw file
Possible License(s): GPL-2.0
  1. # bundlerepo.py - repository class for viewing uncompressed bundles
  2. #
  3. # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.com>
  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. """Repository class for viewing uncompressed bundles.
  8. This provides a read-only repository interface to bundles as if they
  9. were part of the actual repository.
  10. """
  11. from node import nullid
  12. from i18n import _
  13. import os, tempfile, shutil
  14. import changegroup, util, mdiff, discovery, cmdutil, scmutil, exchange
  15. import localrepo, changelog, manifest, filelog, revlog, error
  16. class bundlerevlog(revlog.revlog):
  17. def __init__(self, opener, indexfile, bundle, linkmapper):
  18. # How it works:
  19. # To retrieve a revision, we need to know the offset of the revision in
  20. # the bundle (an unbundle object). We store this offset in the index
  21. # (start). The base of the delta is stored in the base field.
  22. #
  23. # To differentiate a rev in the bundle from a rev in the revlog, we
  24. # check revision against repotiprev.
  25. opener = scmutil.readonlyvfs(opener)
  26. revlog.revlog.__init__(self, opener, indexfile)
  27. self.bundle = bundle
  28. n = len(self)
  29. self.repotiprev = n - 1
  30. chain = None
  31. self.bundlerevs = set() # used by 'bundle()' revset expression
  32. while True:
  33. chunkdata = bundle.deltachunk(chain)
  34. if not chunkdata:
  35. break
  36. node = chunkdata['node']
  37. p1 = chunkdata['p1']
  38. p2 = chunkdata['p2']
  39. cs = chunkdata['cs']
  40. deltabase = chunkdata['deltabase']
  41. delta = chunkdata['delta']
  42. size = len(delta)
  43. start = bundle.tell() - size
  44. link = linkmapper(cs)
  45. if node in self.nodemap:
  46. # this can happen if two branches make the same change
  47. chain = node
  48. self.bundlerevs.add(self.nodemap[node])
  49. continue
  50. for p in (p1, p2):
  51. if p not in self.nodemap:
  52. raise error.LookupError(p, self.indexfile,
  53. _("unknown parent"))
  54. if deltabase not in self.nodemap:
  55. raise LookupError(deltabase, self.indexfile,
  56. _('unknown delta base'))
  57. baserev = self.rev(deltabase)
  58. # start, size, full unc. size, base (unused), link, p1, p2, node
  59. e = (revlog.offset_type(start, 0), size, -1, baserev, link,
  60. self.rev(p1), self.rev(p2), node)
  61. self.index.insert(-1, e)
  62. self.nodemap[node] = n
  63. self.bundlerevs.add(n)
  64. chain = node
  65. n += 1
  66. def _chunk(self, rev):
  67. # Warning: in case of bundle, the diff is against what we stored as
  68. # delta base, not against rev - 1
  69. # XXX: could use some caching
  70. if rev <= self.repotiprev:
  71. return revlog.revlog._chunk(self, rev)
  72. self.bundle.seek(self.start(rev))
  73. return self.bundle.read(self.length(rev))
  74. def revdiff(self, rev1, rev2):
  75. """return or calculate a delta between two revisions"""
  76. if rev1 > self.repotiprev and rev2 > self.repotiprev:
  77. # hot path for bundle
  78. revb = self.index[rev2][3]
  79. if revb == rev1:
  80. return self._chunk(rev2)
  81. elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
  82. return revlog.revlog.revdiff(self, rev1, rev2)
  83. return mdiff.textdiff(self.revision(self.node(rev1)),
  84. self.revision(self.node(rev2)))
  85. def revision(self, nodeorrev):
  86. """return an uncompressed revision of a given node or revision
  87. number.
  88. """
  89. if isinstance(nodeorrev, int):
  90. rev = nodeorrev
  91. node = self.node(rev)
  92. else:
  93. node = nodeorrev
  94. rev = self.rev(node)
  95. if node == nullid:
  96. return ""
  97. text = None
  98. chain = []
  99. iterrev = rev
  100. # reconstruct the revision if it is from a changegroup
  101. while iterrev > self.repotiprev:
  102. if self._cache and self._cache[1] == iterrev:
  103. text = self._cache[2]
  104. break
  105. chain.append(iterrev)
  106. iterrev = self.index[iterrev][3]
  107. if text is None:
  108. text = self.baserevision(iterrev)
  109. while chain:
  110. delta = self._chunk(chain.pop())
  111. text = mdiff.patches(text, [delta])
  112. self._checkhash(text, node, rev)
  113. self._cache = (node, rev, text)
  114. return text
  115. def baserevision(self, nodeorrev):
  116. # Revlog subclasses may override 'revision' method to modify format of
  117. # content retrieved from revlog. To use bundlerevlog with such class one
  118. # needs to override 'baserevision' and make more specific call here.
  119. return revlog.revlog.revision(self, nodeorrev)
  120. def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
  121. raise NotImplementedError
  122. def addgroup(self, revs, linkmapper, transaction):
  123. raise NotImplementedError
  124. def strip(self, rev, minlink):
  125. raise NotImplementedError
  126. def checksize(self):
  127. raise NotImplementedError
  128. class bundlechangelog(bundlerevlog, changelog.changelog):
  129. def __init__(self, opener, bundle):
  130. changelog.changelog.__init__(self, opener)
  131. linkmapper = lambda x: x
  132. bundlerevlog.__init__(self, opener, self.indexfile, bundle,
  133. linkmapper)
  134. def baserevision(self, nodeorrev):
  135. # Although changelog doesn't override 'revision' method, some extensions
  136. # may replace this class with another that does. Same story with
  137. # manifest and filelog classes.
  138. return changelog.changelog.revision(self, nodeorrev)
  139. class bundlemanifest(bundlerevlog, manifest.manifest):
  140. def __init__(self, opener, bundle, linkmapper):
  141. manifest.manifest.__init__(self, opener)
  142. bundlerevlog.__init__(self, opener, self.indexfile, bundle,
  143. linkmapper)
  144. def baserevision(self, nodeorrev):
  145. return manifest.manifest.revision(self, nodeorrev)
  146. class bundlefilelog(bundlerevlog, filelog.filelog):
  147. def __init__(self, opener, path, bundle, linkmapper, repo):
  148. filelog.filelog.__init__(self, opener, path)
  149. bundlerevlog.__init__(self, opener, self.indexfile, bundle,
  150. linkmapper)
  151. self._repo = repo
  152. def baserevision(self, nodeorrev):
  153. return filelog.filelog.revision(self, nodeorrev)
  154. def _file(self, f):
  155. self._repo.file(f)
  156. class bundlepeer(localrepo.localpeer):
  157. def canpush(self):
  158. return False
  159. class bundlerepository(localrepo.localrepository):
  160. def __init__(self, ui, path, bundlename):
  161. self._tempparent = None
  162. try:
  163. localrepo.localrepository.__init__(self, ui, path)
  164. except error.RepoError:
  165. self._tempparent = tempfile.mkdtemp()
  166. localrepo.instance(ui, self._tempparent, 1)
  167. localrepo.localrepository.__init__(self, ui, self._tempparent)
  168. self.ui.setconfig('phases', 'publish', False, 'bundlerepo')
  169. if path:
  170. self._url = 'bundle:' + util.expandpath(path) + '+' + bundlename
  171. else:
  172. self._url = 'bundle:' + bundlename
  173. self.tempfile = None
  174. f = util.posixfile(bundlename, "rb")
  175. self.bundle = exchange.readbundle(ui, f, bundlename)
  176. if self.bundle.compressed():
  177. fdtemp, temp = self.vfs.mkstemp(prefix="hg-bundle-",
  178. suffix=".hg10un")
  179. self.tempfile = temp
  180. fptemp = os.fdopen(fdtemp, 'wb')
  181. try:
  182. fptemp.write("HG10UN")
  183. while True:
  184. chunk = self.bundle.read(2**18)
  185. if not chunk:
  186. break
  187. fptemp.write(chunk)
  188. finally:
  189. fptemp.close()
  190. f = self.vfs.open(self.tempfile, mode="rb")
  191. self.bundle = exchange.readbundle(ui, f, bundlename, self.vfs)
  192. # dict with the mapping 'filename' -> position in the bundle
  193. self.bundlefilespos = {}
  194. @localrepo.unfilteredpropertycache
  195. def changelog(self):
  196. # consume the header if it exists
  197. self.bundle.changelogheader()
  198. c = bundlechangelog(self.sopener, self.bundle)
  199. self.manstart = self.bundle.tell()
  200. return c
  201. @localrepo.unfilteredpropertycache
  202. def manifest(self):
  203. self.bundle.seek(self.manstart)
  204. # consume the header if it exists
  205. self.bundle.manifestheader()
  206. m = bundlemanifest(self.sopener, self.bundle, self.changelog.rev)
  207. self.filestart = self.bundle.tell()
  208. return m
  209. @localrepo.unfilteredpropertycache
  210. def manstart(self):
  211. self.changelog
  212. return self.manstart
  213. @localrepo.unfilteredpropertycache
  214. def filestart(self):
  215. self.manifest
  216. return self.filestart
  217. def url(self):
  218. return self._url
  219. def file(self, f):
  220. if not self.bundlefilespos:
  221. self.bundle.seek(self.filestart)
  222. while True:
  223. chunkdata = self.bundle.filelogheader()
  224. if not chunkdata:
  225. break
  226. fname = chunkdata['filename']
  227. self.bundlefilespos[fname] = self.bundle.tell()
  228. while True:
  229. c = self.bundle.deltachunk(None)
  230. if not c:
  231. break
  232. if f in self.bundlefilespos:
  233. self.bundle.seek(self.bundlefilespos[f])
  234. return bundlefilelog(self.sopener, f, self.bundle,
  235. self.changelog.rev, self)
  236. else:
  237. return filelog.filelog(self.sopener, f)
  238. def close(self):
  239. """Close assigned bundle file immediately."""
  240. self.bundle.close()
  241. if self.tempfile is not None:
  242. self.vfs.unlink(self.tempfile)
  243. if self._tempparent:
  244. shutil.rmtree(self._tempparent, True)
  245. def cancopy(self):
  246. return False
  247. def peer(self):
  248. return bundlepeer(self)
  249. def getcwd(self):
  250. return os.getcwd() # always outside the repo
  251. def instance(ui, path, create):
  252. if create:
  253. raise util.Abort(_('cannot create new bundle repository'))
  254. parentpath = ui.config("bundle", "mainreporoot", "")
  255. if not parentpath:
  256. # try to find the correct path to the working directory repo
  257. parentpath = cmdutil.findrepo(os.getcwd())
  258. if parentpath is None:
  259. parentpath = ''
  260. if parentpath:
  261. # Try to make the full path relative so we get a nice, short URL.
  262. # In particular, we don't want temp dir names in test outputs.
  263. cwd = os.getcwd()
  264. if parentpath == cwd:
  265. parentpath = ''
  266. else:
  267. cwd = os.path.join(cwd,'')
  268. if parentpath.startswith(cwd):
  269. parentpath = parentpath[len(cwd):]
  270. u = util.url(path)
  271. path = u.localpath()
  272. if u.scheme == 'bundle':
  273. s = path.split("+", 1)
  274. if len(s) == 1:
  275. repopath, bundlename = parentpath, s[0]
  276. else:
  277. repopath, bundlename = s
  278. else:
  279. repopath, bundlename = parentpath, path
  280. return bundlerepository(ui, repopath, bundlename)
  281. def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
  282. force=False):
  283. '''obtains a bundle of changes incoming from other
  284. "onlyheads" restricts the returned changes to those reachable from the
  285. specified heads.
  286. "bundlename", if given, stores the bundle to this file path permanently;
  287. otherwise it's stored to a temp file and gets deleted again when you call
  288. the returned "cleanupfn".
  289. "force" indicates whether to proceed on unrelated repos.
  290. Returns a tuple (local, csets, cleanupfn):
  291. "local" is a local repo from which to obtain the actual incoming
  292. changesets; it is a bundlerepo for the obtained bundle when the
  293. original "other" is remote.
  294. "csets" lists the incoming changeset node ids.
  295. "cleanupfn" must be called without arguments when you're done processing
  296. the changes; it closes both the original "other" and the one returned
  297. here.
  298. '''
  299. tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
  300. force=force)
  301. common, incoming, rheads = tmp
  302. if not incoming:
  303. try:
  304. if bundlename:
  305. os.unlink(bundlename)
  306. except OSError:
  307. pass
  308. return repo, [], other.close
  309. bundle = None
  310. bundlerepo = None
  311. localrepo = other.local()
  312. if bundlename or not localrepo:
  313. # create a bundle (uncompressed if other repo is not local)
  314. if other.capable('getbundle'):
  315. cg = other.getbundle('incoming', common=common, heads=rheads)
  316. elif onlyheads is None and not other.capable('changegroupsubset'):
  317. # compat with older servers when pulling all remote heads
  318. cg = other.changegroup(incoming, "incoming")
  319. rheads = None
  320. else:
  321. cg = other.changegroupsubset(incoming, rheads, 'incoming')
  322. bundletype = localrepo and "HG10BZ" or "HG10UN"
  323. fname = bundle = changegroup.writebundle(cg, bundlename, bundletype)
  324. # keep written bundle?
  325. if bundlename:
  326. bundle = None
  327. if not localrepo:
  328. # use the created uncompressed bundlerepo
  329. localrepo = bundlerepo = bundlerepository(repo.baseui, repo.root,
  330. fname)
  331. # this repo contains local and other now, so filter out local again
  332. common = repo.heads()
  333. if localrepo:
  334. # Part of common may be remotely filtered
  335. # So use an unfiltered version
  336. # The discovery process probably need cleanup to avoid that
  337. localrepo = localrepo.unfiltered()
  338. csets = localrepo.changelog.findmissing(common, rheads)
  339. def cleanup():
  340. if bundlerepo:
  341. bundlerepo.close()
  342. if bundle:
  343. os.unlink(bundle)
  344. other.close()
  345. return (localrepo, csets, cleanup)