##// END OF EJS Templates
store: remove pointless pathjoiner parameter...
Adrian Buehlmann -
r13426:643b8212 default
parent child Browse files
Show More
@@ -1,147 +1,145 b''
1 1 # statichttprepo.py - simple http repository class for mercurial
2 2 #
3 3 # This provides read-only repo access to repositories exported via static http
4 4 #
5 5 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from i18n import _
11 11 import changelog, byterange, url, error
12 12 import localrepo, manifest, util, store
13 13 import urllib, urllib2, errno
14 14
15 15 class httprangereader(object):
16 16 def __init__(self, url, opener):
17 17 # we assume opener has HTTPRangeHandler
18 18 self.url = url
19 19 self.pos = 0
20 20 self.opener = opener
21 21 self.name = url
22 22 def seek(self, pos):
23 23 self.pos = pos
24 24 def read(self, bytes=None):
25 25 req = urllib2.Request(self.url)
26 26 end = ''
27 27 if bytes:
28 28 end = self.pos + bytes - 1
29 29 req.add_header('Range', 'bytes=%d-%s' % (self.pos, end))
30 30
31 31 try:
32 32 f = self.opener.open(req)
33 33 data = f.read()
34 34 if hasattr(f, 'getcode'):
35 35 # python 2.6+
36 36 code = f.getcode()
37 37 elif hasattr(f, 'code'):
38 38 # undocumented attribute, seems to be set in 2.4 and 2.5
39 39 code = f.code
40 40 else:
41 41 # Don't know how to check, hope for the best.
42 42 code = 206
43 43 except urllib2.HTTPError, inst:
44 44 num = inst.code == 404 and errno.ENOENT or None
45 45 raise IOError(num, inst)
46 46 except urllib2.URLError, inst:
47 47 raise IOError(None, inst.reason[1])
48 48
49 49 if code == 200:
50 50 # HTTPRangeHandler does nothing if remote does not support
51 51 # Range headers and returns the full entity. Let's slice it.
52 52 if bytes:
53 53 data = data[self.pos:self.pos + bytes]
54 54 else:
55 55 data = data[self.pos:]
56 56 elif bytes:
57 57 data = data[:bytes]
58 58 self.pos += len(data)
59 59 return data
60 60 def __iter__(self):
61 61 return iter(self.read().splitlines(1))
62 62 def close(self):
63 63 pass
64 64
65 65 def build_opener(ui, authinfo):
66 66 # urllib cannot handle URLs with embedded user or passwd
67 67 urlopener = url.opener(ui, authinfo)
68 68 urlopener.add_handler(byterange.HTTPRangeHandler())
69 69
70 70 def opener(base):
71 71 """return a function that opens files over http"""
72 72 p = base
73 73 def o(path, mode="r", atomictemp=None):
74 74 if 'a' in mode or 'w' in mode:
75 75 raise IOError('Permission denied')
76 76 f = "/".join((p, urllib.quote(path)))
77 77 return httprangereader(f, urlopener)
78 78 return o
79 79
80 80 return opener
81 81
82 82 class statichttprepository(localrepo.localrepository):
83 83 def __init__(self, ui, path):
84 84 self._url = path
85 85 self.ui = ui
86 86
87 87 self.root = path
88 88 self.path, authinfo = url.getauthinfo(path.rstrip('/') + "/.hg")
89 89
90 90 opener = build_opener(ui, authinfo)
91 91 self.opener = opener(self.path)
92 92
93 93 # find requirements
94 94 try:
95 95 requirements = self.opener("requires").read().splitlines()
96 96 except IOError, inst:
97 97 if inst.errno != errno.ENOENT:
98 98 raise
99 99 # check if it is a non-empty old-style repository
100 100 try:
101 101 fp = self.opener("00changelog.i")
102 102 fp.read(1)
103 103 fp.close()
104 104 except IOError, inst:
105 105 if inst.errno != errno.ENOENT:
106 106 raise
107 107 # we do not care about empty old-style repositories here
108 108 msg = _("'%s' does not appear to be an hg repository") % path
109 109 raise error.RepoError(msg)
110 110 requirements = []
111 111
112 112 # check them
113 113 for r in requirements:
114 114 if r not in self.supported:
115 115 raise error.RepoError(_("requirement '%s' not supported") % r)
116 116
117 117 # setup store
118 def pjoin(a, b):
119 return a + '/' + b
120 self.store = store.store(requirements, self.path, opener, pjoin)
118 self.store = store.store(requirements, self.path, opener)
121 119 self.spath = self.store.path
122 120 self.sopener = self.store.opener
123 121 self.sjoin = self.store.join
124 122
125 123 self.manifest = manifest.manifest(self.sopener)
126 124 self.changelog = changelog.changelog(self.sopener)
127 125 self._tags = None
128 126 self.nodetagscache = None
129 127 self._branchcache = None
130 128 self._branchcachetip = None
131 129 self.encodepats = None
132 130 self.decodepats = None
133 131 self.capabilities.remove("pushkey")
134 132
135 133 def url(self):
136 134 return self._url
137 135
138 136 def local(self):
139 137 return False
140 138
141 139 def lock(self, wait=True):
142 140 raise util.Abort(_('cannot lock static-http repository'))
143 141
144 142 def instance(ui, path, create):
145 143 if create:
146 144 raise util.Abort(_('cannot create new static-http repository'))
147 145 return statichttprepository(ui, path[7:])
@@ -1,357 +1,354 b''
1 1 # store.py - repository store handling for Mercurial
2 2 #
3 3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
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 from i18n import _
9 9 import osutil, util
10 10 import os, stat
11 11
12 12 _sha = util.sha1
13 13
14 14 # This avoids a collision between a file named foo and a dir named
15 15 # foo.i or foo.d
16 16 def encodedir(path):
17 17 if not path.startswith('data/'):
18 18 return path
19 19 return (path
20 20 .replace(".hg/", ".hg.hg/")
21 21 .replace(".i/", ".i.hg/")
22 22 .replace(".d/", ".d.hg/"))
23 23
24 24 def decodedir(path):
25 25 if not path.startswith('data/') or ".hg/" not in path:
26 26 return path
27 27 return (path
28 28 .replace(".d.hg/", ".d/")
29 29 .replace(".i.hg/", ".i/")
30 30 .replace(".hg.hg/", ".hg/"))
31 31
32 32 def _buildencodefun():
33 33 e = '_'
34 34 win_reserved = [ord(x) for x in '\\:*?"<>|']
35 35 cmap = dict([(chr(x), chr(x)) for x in xrange(127)])
36 36 for x in (range(32) + range(126, 256) + win_reserved):
37 37 cmap[chr(x)] = "~%02x" % x
38 38 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
39 39 cmap[chr(x)] = e + chr(x).lower()
40 40 dmap = {}
41 41 for k, v in cmap.iteritems():
42 42 dmap[v] = k
43 43 def decode(s):
44 44 i = 0
45 45 while i < len(s):
46 46 for l in xrange(1, 4):
47 47 try:
48 48 yield dmap[s[i:i + l]]
49 49 i += l
50 50 break
51 51 except KeyError:
52 52 pass
53 53 else:
54 54 raise KeyError
55 55 return (lambda s: "".join([cmap[c] for c in encodedir(s)]),
56 56 lambda s: decodedir("".join(list(decode(s)))))
57 57
58 58 encodefilename, decodefilename = _buildencodefun()
59 59
60 60 def _build_lower_encodefun():
61 61 win_reserved = [ord(x) for x in '\\:*?"<>|']
62 62 cmap = dict([(chr(x), chr(x)) for x in xrange(127)])
63 63 for x in (range(32) + range(126, 256) + win_reserved):
64 64 cmap[chr(x)] = "~%02x" % x
65 65 for x in range(ord("A"), ord("Z")+1):
66 66 cmap[chr(x)] = chr(x).lower()
67 67 return lambda s: "".join([cmap[c] for c in s])
68 68
69 69 lowerencode = _build_lower_encodefun()
70 70
71 71 _windows_reserved_filenames = '''con prn aux nul
72 72 com1 com2 com3 com4 com5 com6 com7 com8 com9
73 73 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
74 74 def _auxencode(path, dotencode):
75 75 res = []
76 76 for n in path.split('/'):
77 77 if n:
78 78 base = n.split('.')[0]
79 79 if base and (base in _windows_reserved_filenames):
80 80 # encode third letter ('aux' -> 'au~78')
81 81 ec = "~%02x" % ord(n[2])
82 82 n = n[0:2] + ec + n[3:]
83 83 if n[-1] in '. ':
84 84 # encode last period or space ('foo...' -> 'foo..~2e')
85 85 n = n[:-1] + "~%02x" % ord(n[-1])
86 86 if dotencode and n[0] in '. ':
87 87 n = "~%02x" % ord(n[0]) + n[1:]
88 88 res.append(n)
89 89 return '/'.join(res)
90 90
91 91 MAX_PATH_LEN_IN_HGSTORE = 120
92 92 DIR_PREFIX_LEN = 8
93 93 _MAX_SHORTENED_DIRS_LEN = 8 * (DIR_PREFIX_LEN + 1) - 4
94 94 def _hybridencode(path, auxencode):
95 95 '''encodes path with a length limit
96 96
97 97 Encodes all paths that begin with 'data/', according to the following.
98 98
99 99 Default encoding (reversible):
100 100
101 101 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
102 102 characters are encoded as '~xx', where xx is the two digit hex code
103 103 of the character (see encodefilename).
104 104 Relevant path components consisting of Windows reserved filenames are
105 105 masked by encoding the third character ('aux' -> 'au~78', see auxencode).
106 106
107 107 Hashed encoding (not reversible):
108 108
109 109 If the default-encoded path is longer than MAX_PATH_LEN_IN_HGSTORE, a
110 110 non-reversible hybrid hashing of the path is done instead.
111 111 This encoding uses up to DIR_PREFIX_LEN characters of all directory
112 112 levels of the lowerencoded path, but not more levels than can fit into
113 113 _MAX_SHORTENED_DIRS_LEN.
114 114 Then follows the filler followed by the sha digest of the full path.
115 115 The filler is the beginning of the basename of the lowerencoded path
116 116 (the basename is everything after the last path separator). The filler
117 117 is as long as possible, filling in characters from the basename until
118 118 the encoded path has MAX_PATH_LEN_IN_HGSTORE characters (or all chars
119 119 of the basename have been taken).
120 120 The extension (e.g. '.i' or '.d') is preserved.
121 121
122 122 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
123 123 encoding was used.
124 124 '''
125 125 if not path.startswith('data/'):
126 126 return path
127 127 # escape directories ending with .i and .d
128 128 path = encodedir(path)
129 129 ndpath = path[len('data/'):]
130 130 res = 'data/' + auxencode(encodefilename(ndpath))
131 131 if len(res) > MAX_PATH_LEN_IN_HGSTORE:
132 132 digest = _sha(path).hexdigest()
133 133 aep = auxencode(lowerencode(ndpath))
134 134 _root, ext = os.path.splitext(aep)
135 135 parts = aep.split('/')
136 136 basename = parts[-1]
137 137 sdirs = []
138 138 for p in parts[:-1]:
139 139 d = p[:DIR_PREFIX_LEN]
140 140 if d[-1] in '. ':
141 141 # Windows can't access dirs ending in period or space
142 142 d = d[:-1] + '_'
143 143 t = '/'.join(sdirs) + '/' + d
144 144 if len(t) > _MAX_SHORTENED_DIRS_LEN:
145 145 break
146 146 sdirs.append(d)
147 147 dirs = '/'.join(sdirs)
148 148 if len(dirs) > 0:
149 149 dirs += '/'
150 150 res = 'dh/' + dirs + digest + ext
151 151 space_left = MAX_PATH_LEN_IN_HGSTORE - len(res)
152 152 if space_left > 0:
153 153 filler = basename[:space_left]
154 154 res = 'dh/' + dirs + filler + digest + ext
155 155 return res
156 156
157 157 def _calcmode(path):
158 158 try:
159 159 # files in .hg/ will be created using this mode
160 160 mode = os.stat(path).st_mode
161 161 # avoid some useless chmods
162 162 if (0777 & ~util.umask) == (0777 & mode):
163 163 mode = None
164 164 except OSError:
165 165 mode = None
166 166 return mode
167 167
168 168 _data = 'data 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
169 169
170 170 class basicstore(object):
171 171 '''base class for local repository stores'''
172 def __init__(self, path, opener, pathjoiner):
173 self.pathjoiner = pathjoiner
172 def __init__(self, path, opener):
174 173 self.path = path
175 174 self.createmode = _calcmode(path)
176 175 op = opener(self.path)
177 176 op.createmode = self.createmode
178 177 self.opener = lambda f, *args, **kw: op(encodedir(f), *args, **kw)
179 178
180 179 def join(self, f):
181 return self.pathjoiner(self.path, encodedir(f))
180 return self.path + '/' + encodedir(f)
182 181
183 182 def _walk(self, relpath, recurse):
184 183 '''yields (unencoded, encoded, size)'''
185 path = self.pathjoiner(self.path, relpath)
186 striplen = len(self.path) + len(os.sep)
184 path = self.path
185 if relpath:
186 path += '/' + relpath
187 striplen = len(self.path) + 1
187 188 l = []
188 189 if os.path.isdir(path):
189 190 visit = [path]
190 191 while visit:
191 192 p = visit.pop()
192 193 for f, kind, st in osutil.listdir(p, stat=True):
193 fp = self.pathjoiner(p, f)
194 fp = p + '/' + f
194 195 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
195 196 n = util.pconvert(fp[striplen:])
196 197 l.append((decodedir(n), n, st.st_size))
197 198 elif kind == stat.S_IFDIR and recurse:
198 199 visit.append(fp)
199 200 return sorted(l)
200 201
201 202 def datafiles(self):
202 203 return self._walk('data', True)
203 204
204 205 def walk(self):
205 206 '''yields (unencoded, encoded, size)'''
206 207 # yield data files first
207 208 for x in self.datafiles():
208 209 yield x
209 210 # yield manifest before changelog
210 211 for x in reversed(self._walk('', False)):
211 212 yield x
212 213
213 214 def copylist(self):
214 215 return ['requires'] + _data.split()
215 216
216 217 def write(self):
217 218 pass
218 219
219 220 class encodedstore(basicstore):
220 def __init__(self, path, opener, pathjoiner):
221 self.pathjoiner = pathjoiner
222 self.path = self.pathjoiner(path, 'store')
221 def __init__(self, path, opener):
222 self.path = path + '/store'
223 223 self.createmode = _calcmode(self.path)
224 224 op = opener(self.path)
225 225 op.createmode = self.createmode
226 226 self.opener = lambda f, *args, **kw: op(encodefilename(f), *args, **kw)
227 227
228 228 def datafiles(self):
229 229 for a, b, size in self._walk('data', True):
230 230 try:
231 231 a = decodefilename(a)
232 232 except KeyError:
233 233 a = None
234 234 yield a, b, size
235 235
236 236 def join(self, f):
237 return self.pathjoiner(self.path, encodefilename(f))
237 return self.path + '/' + encodefilename(f)
238 238
239 239 def copylist(self):
240 240 return (['requires', '00changelog.i'] +
241 [self.pathjoiner('store', f) for f in _data.split()])
241 ['store/' + f for f in _data.split()])
242 242
243 243 class fncache(object):
244 244 # the filename used to be partially encoded
245 245 # hence the encodedir/decodedir dance
246 246 def __init__(self, opener):
247 247 self.opener = opener
248 248 self.entries = None
249 249 self._dirty = False
250 250
251 251 def _load(self):
252 252 '''fill the entries from the fncache file'''
253 253 self.entries = set()
254 254 self._dirty = False
255 255 try:
256 256 fp = self.opener('fncache', mode='rb')
257 257 except IOError:
258 258 # skip nonexistent file
259 259 return
260 260 for n, line in enumerate(fp):
261 261 if (len(line) < 2) or (line[-1] != '\n'):
262 262 t = _('invalid entry in fncache, line %s') % (n + 1)
263 263 raise util.Abort(t)
264 264 self.entries.add(decodedir(line[:-1]))
265 265 fp.close()
266 266
267 267 def rewrite(self, files):
268 268 fp = self.opener('fncache', mode='wb')
269 269 for p in files:
270 270 fp.write(encodedir(p) + '\n')
271 271 fp.close()
272 272 self.entries = set(files)
273 273 self._dirty = False
274 274
275 275 def write(self):
276 276 if not self._dirty:
277 277 return
278 278 fp = self.opener('fncache', mode='wb', atomictemp=True)
279 279 for p in self.entries:
280 280 fp.write(encodedir(p) + '\n')
281 281 fp.rename()
282 282 self._dirty = False
283 283
284 284 def add(self, fn):
285 285 if self.entries is None:
286 286 self._load()
287 287 if fn not in self.entries:
288 288 self._dirty = True
289 289 self.entries.add(fn)
290 290
291 291 def __contains__(self, fn):
292 292 if self.entries is None:
293 293 self._load()
294 294 return fn in self.entries
295 295
296 296 def __iter__(self):
297 297 if self.entries is None:
298 298 self._load()
299 299 return iter(self.entries)
300 300
301 301 class fncachestore(basicstore):
302 def __init__(self, path, opener, pathjoiner, encode):
302 def __init__(self, path, opener, encode):
303 303 self.encode = encode
304 self.pathjoiner = pathjoiner
305 self.path = self.pathjoiner(path, 'store')
304 self.path = path + '/store'
306 305 self.createmode = _calcmode(self.path)
307 306 op = opener(self.path)
308 307 op.createmode = self.createmode
309 308 fnc = fncache(op)
310 309 self.fncache = fnc
311 310
312 311 def fncacheopener(path, mode='r', *args, **kw):
313 312 if mode not in ('r', 'rb') and path.startswith('data/'):
314 313 fnc.add(path)
315 314 return op(self.encode(path), mode, *args, **kw)
316 315 self.opener = fncacheopener
317 316
318 317 def join(self, f):
319 return self.pathjoiner(self.path, self.encode(f))
318 return self.path + '/' + self.encode(f)
320 319
321 320 def datafiles(self):
322 321 rewrite = False
323 322 existing = []
324 pjoin = self.pathjoiner
325 323 spath = self.path
326 324 for f in self.fncache:
327 325 ef = self.encode(f)
328 326 try:
329 st = os.stat(pjoin(spath, ef))
327 st = os.stat(spath + '/' + ef)
330 328 yield f, ef, st.st_size
331 329 existing.append(f)
332 330 except OSError:
333 331 # nonexistent entry
334 332 rewrite = True
335 333 if rewrite:
336 334 # rewrite fncache to remove nonexistent entries
337 335 # (may be caused by rollback / strip)
338 336 self.fncache.rewrite(existing)
339 337
340 338 def copylist(self):
341 339 d = ('data dh fncache'
342 340 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
343 341 return (['requires', '00changelog.i'] +
344 [self.pathjoiner('store', f) for f in d.split()])
342 ['store/' + f for f in d.split()])
345 343
346 344 def write(self):
347 345 self.fncache.write()
348 346
349 def store(requirements, path, opener, pathjoiner=None):
350 pathjoiner = pathjoiner or os.path.join
347 def store(requirements, path, opener):
351 348 if 'store' in requirements:
352 349 if 'fncache' in requirements:
353 350 auxencode = lambda f: _auxencode(f, 'dotencode' in requirements)
354 351 encode = lambda f: _hybridencode(f, auxencode)
355 return fncachestore(path, opener, pathjoiner, encode)
356 return encodedstore(path, opener, pathjoiner)
357 return basicstore(path, opener, pathjoiner)
352 return fncachestore(path, opener, encode)
353 return encodedstore(path, opener)
354 return basicstore(path, opener)
General Comments 0
You need to be logged in to leave comments. Login now