|
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 |