comparison mercurial/bundlerepo.py @ 1942:9da45de3118d

add bundlerepo.py: a read-only repo that can use uncompressed bundles The idea is to create a repo consisting of a normal local repository plus all the changesets contained in a bundle. The bundle needs to be uncompressed. A futur version could implement the seeking through a compressed bundle.
author Benoit Boissinot <benoit.boissinot@ens-lyon.org>
date Mon, 13 Mar 2006 03:54:23 +0100
parents
children 9fee186f7f0d
comparison
equal deleted inserted replaced
1941:7518823709a2 1942:9da45de3118d
1 """
2 bundlerepo.py - repository class for viewing uncompressed bundles
3
4 This provides a read-only repository interface to bundles as if
5 they were part of the actual repository.
6
7 Copyright 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
8
9 This software may be used and distributed according to the terms
10 of the GNU General Public License, incorporated herein by reference.
11 """
12
13 from node import *
14 from i18n import gettext as _
15 from demandload import demandload
16 demandload(globals(), "util os struct")
17
18 from changelog import changelog
19 from manifest import manifest
20 from filelog import filelog
21 from localrepo import localrepository
22 from revlog import *
23
24 def getchunk(source):
25 """get a chunk from a group"""
26 d = source.read(4)
27 if not d:
28 return ""
29 l = struct.unpack(">l", d)[0]
30 if l <= 4:
31 return ""
32 d = source.read(l - 4)
33 if len(d) < l - 4:
34 raise util.Abort(_("premature EOF reading chunk"
35 " (got %d bytes, expected %d)")
36 % (len(d), l - 4))
37 return d
38
39 class bundlerevlog(revlog):
40 def __init__(self, opener, indexfile, datafile, bundlefile,
41 linkmapper=None):
42 # How it works:
43 # to retrieve a revision, we need to know the offset of
44 # the revision in the bundlefile (an opened file).
45 #
46 # We store this offset in the index (start), to differentiate a
47 # rev in the bundle and from a rev in the revlog, we check
48 # len(index[r]). If the tuple is bigger than 7, it is a bundle
49 # (it is bigger since we store the node to which the delta is)
50 #
51 revlog.__init__(self, opener, indexfile, datafile)
52 self.bundlefile = bundlefile
53 def genchunk():
54 while 1:
55 pos = bundlefile.tell()
56 chunk = getchunk(bundlefile)
57 if not chunk:
58 break
59 yield chunk, pos + 4 # XXX struct.calcsize(">l") == 4
60 n = self.count()
61 prev = None
62 for chunk, start in genchunk():
63 size = len(chunk)
64 if size < 80:
65 raise util.Abort("invalid changegroup")
66 start += 80
67 size -= 80
68 node, p1, p2, cs = struct.unpack("20s20s20s20s", chunk[:80])
69 if node in self.nodemap:
70 prev = node
71 continue
72 for p in (p1, p2):
73 if not p in self.nodemap:
74 raise RevlogError(_("unknown parent %s") % short(p1))
75 if linkmapper is None:
76 link = n
77 else:
78 link = linkmapper(cs)
79
80 if not prev:
81 prev = p1
82 # start, size, base is not used, link, p1, p2, delta ref
83 # warning:
84 e = (start, size, None, link, p1, p2, node, prev)
85 self.index.append(e)
86 self.nodemap[node] = n
87 prev = node
88 n += 1
89
90 def bundle(self, rev):
91 """is rev from the bundle"""
92 if rev < 0:
93 return False
94 return len(self.index[rev]) > 7
95 def bundlebase(self, rev): return self.index[rev][7]
96 def chunk(self, rev):
97 # Warning: in case of bundle, the diff is against bundlebase,
98 # not against rev - 1
99 # XXX: could use some caching
100 if not self.bundle(rev):
101 return revlog.chunk(self, rev)
102 self.bundlefile.seek(self.start(rev))
103 return self.bundlefile.read(self.length(rev))
104
105 def revdiff(self, rev1, rev2):
106 """return or calculate a delta between two revisions"""
107 if self.bundle(rev1) and self.bundle(rev2):
108 # hot path for bundle
109 revb = self.rev(self.bundlebase(rev2))
110 if revb == rev1:
111 return self.chunk(rev2)
112 elif not self.bundle(rev1) and not self.bundle(rev2):
113 return revlog.chunk(self, rev1, rev2)
114
115 return self.diff(self.revision(self.node(rev1)),
116 self.revision(self.node(rev2)))
117
118 def revision(self, node):
119 """return an uncompressed revision of a given"""
120 if node == nullid: return ""
121
122 text = None
123 chain = []
124 iter_node = node
125 rev = self.rev(iter_node)
126 # reconstruct the revision if it is from a changegroup
127 while self.bundle(rev):
128 if self.cache and self.cache[0] == iter_node:
129 text = self.cache[2]
130 break
131 chain.append(rev)
132 iter_node = self.bundlebase(rev)
133 rev = self.rev(iter_node)
134 if text is None:
135 text = revlog.revision(self, iter_node)
136
137 while chain:
138 delta = self.chunk(chain.pop())
139 text = self.patches(text, [delta])
140
141 p1, p2 = self.parents(node)
142 if node != hash(text, p1, p2):
143 raise RevlogError(_("integrity check failed on %s:%d")
144 % (self.datafile, self.rev(node)))
145
146 self.cache = (node, rev, text)
147 return text
148
149 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
150 raise NotImplementedError
151 def addgroup(self, revs, linkmapper, transaction, unique=0):
152 raise NotImplementedError
153 def strip(self, rev, minlink):
154 raise NotImplementedError
155 def checksize(self):
156 raise NotImplementedError
157
158 class bundlechangelog(bundlerevlog, changelog):
159 def __init__(self, opener, bundlefile):
160 changelog.__init__(self, opener)
161 bundlerevlog.__init__(self, opener, "00changelog.i", "00changelog.d",
162 bundlefile)
163
164 class bundlemanifest(bundlerevlog, manifest):
165 def __init__(self, opener, bundlefile, linkmapper):
166 manifest.__init__(self, opener)
167 bundlerevlog.__init__(self, opener, self.indexfile, self.datafile,
168 bundlefile, linkmapper)
169
170 class bundlefilelog(bundlerevlog, filelog):
171 def __init__(self, opener, path, bundlefile, linkmapper):
172 filelog.__init__(self, opener, path)
173 bundlerevlog.__init__(self, opener, self.indexfile, self.datafile,
174 bundlefile, linkmapper)
175
176 class bundlerepository(localrepository):
177 def __init__(self, ui, path, bundlename):
178 localrepository.__init__(self, ui, path)
179 f = open(bundlename, "rb")
180 s = os.fstat(f.fileno())
181 self.bundlefile = f
182 header = self.bundlefile.read(4)
183 if header == "HG10":
184 raise util.Abort(_("%s: compressed bundle not supported")
185 % bundlename)
186 elif header != "HG11":
187 raise util.Abort(_("%s: not a Mercurial bundle file") % bundlename)
188 self.changelog = bundlechangelog(self.opener, self.bundlefile)
189 self.manifest = bundlemanifest(self.opener, self.bundlefile,
190 self.changelog.rev)
191 # dict with the mapping 'filename' -> position in the bundle
192 self.bundlefilespos = {}
193 while 1:
194 f = getchunk(self.bundlefile)
195 if not f:
196 break
197 self.bundlefilespos[f] = self.bundlefile.tell()
198 while getchunk(self.bundlefile):
199 pass
200
201 def dev(self):
202 return -1
203
204 def file(self, f):
205 if f[0] == '/':
206 f = f[1:]
207 if f in self.bundlefilespos:
208 self.bundlefile.seek(self.bundlefilespos[f])
209 return bundlefilelog(self.opener, f, self.bundlefile,
210 self.changelog.rev)
211 else:
212 return filelog(self.opener, f)
213