##// END OF EJS Templates
shallowutil: slice off a byte instead of subscripting...
Augie Fackler -
r41293:d08c816a default
parent child Browse files
Show More
@@ -1,492 +1,492 b''
1 1 # shallowutil.py -- remotefilelog utilities
2 2 #
3 3 # Copyright 2014 Facebook, Inc.
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 from __future__ import absolute_import
8 8
9 9 import collections
10 10 import errno
11 11 import hashlib
12 12 import os
13 13 import stat
14 14 import struct
15 15 import tempfile
16 16
17 17 from mercurial.i18n import _
18 18 from mercurial import (
19 19 error,
20 20 node,
21 21 pycompat,
22 22 revlog,
23 23 util,
24 24 )
25 25 from mercurial.utils import (
26 26 storageutil,
27 27 stringutil,
28 28 )
29 29 from . import constants
30 30
31 31 if not pycompat.iswindows:
32 32 import grp
33 33
34 34 def isenabled(repo):
35 35 """returns whether the repository is remotefilelog enabled or not"""
36 36 return constants.SHALLOWREPO_REQUIREMENT in repo.requirements
37 37
38 38 def getcachekey(reponame, file, id):
39 39 pathhash = node.hex(hashlib.sha1(file).digest())
40 40 return os.path.join(reponame, pathhash[:2], pathhash[2:], id)
41 41
42 42 def getlocalkey(file, id):
43 43 pathhash = node.hex(hashlib.sha1(file).digest())
44 44 return os.path.join(pathhash, id)
45 45
46 46 def getcachepath(ui, allowempty=False):
47 47 cachepath = ui.config("remotefilelog", "cachepath")
48 48 if not cachepath:
49 49 if allowempty:
50 50 return None
51 51 else:
52 52 raise error.Abort(_("could not find config option "
53 53 "remotefilelog.cachepath"))
54 54 return util.expandpath(cachepath)
55 55
56 56 def getcachepackpath(repo, category):
57 57 cachepath = getcachepath(repo.ui)
58 58 if category != constants.FILEPACK_CATEGORY:
59 59 return os.path.join(cachepath, repo.name, 'packs', category)
60 60 else:
61 61 return os.path.join(cachepath, repo.name, 'packs')
62 62
63 63 def getlocalpackpath(base, category):
64 64 return os.path.join(base, 'packs', category)
65 65
66 66 def createrevlogtext(text, copyfrom=None, copyrev=None):
67 67 """returns a string that matches the revlog contents in a
68 68 traditional revlog
69 69 """
70 70 meta = {}
71 71 if copyfrom or text.startswith('\1\n'):
72 72 if copyfrom:
73 73 meta['copy'] = copyfrom
74 74 meta['copyrev'] = copyrev
75 75 text = storageutil.packmeta(meta, text)
76 76
77 77 return text
78 78
79 79 def parsemeta(text):
80 80 """parse mercurial filelog metadata"""
81 81 meta, size = storageutil.parsemeta(text)
82 82 if text.startswith('\1\n'):
83 83 s = text.index('\1\n', 2)
84 84 text = text[s + 2:]
85 85 return meta or {}, text
86 86
87 87 def sumdicts(*dicts):
88 88 """Adds all the values of *dicts together into one dictionary. This assumes
89 89 the values in *dicts are all summable.
90 90
91 91 e.g. [{'a': 4', 'b': 2}, {'b': 3, 'c': 1}] -> {'a': 4, 'b': 5, 'c': 1}
92 92 """
93 93 result = collections.defaultdict(lambda: 0)
94 94 for dict in dicts:
95 95 for k, v in dict.iteritems():
96 96 result[k] += v
97 97 return result
98 98
99 99 def prefixkeys(dict, prefix):
100 100 """Returns ``dict`` with ``prefix`` prepended to all its keys."""
101 101 result = {}
102 102 for k, v in dict.iteritems():
103 103 result[prefix + k] = v
104 104 return result
105 105
106 106 def reportpackmetrics(ui, prefix, *stores):
107 107 dicts = [s.getmetrics() for s in stores]
108 108 dict = prefixkeys(sumdicts(*dicts), prefix + '_')
109 109 ui.log(prefix + "_packsizes", "\n", **pycompat.strkwargs(dict))
110 110
111 111 def _parsepackmeta(metabuf):
112 112 """parse datapack meta, bytes (<metadata-list>) -> dict
113 113
114 114 The dict contains raw content - both keys and values are strings.
115 115 Upper-level business may want to convert some of them to other types like
116 116 integers, on their own.
117 117
118 118 raise ValueError if the data is corrupted
119 119 """
120 120 metadict = {}
121 121 offset = 0
122 122 buflen = len(metabuf)
123 123 while buflen - offset >= 3:
124 key = metabuf[offset]
124 key = metabuf[offset:offset + 1]
125 125 offset += 1
126 126 metalen = struct.unpack_from('!H', metabuf, offset)[0]
127 127 offset += 2
128 128 if offset + metalen > buflen:
129 129 raise ValueError('corrupted metadata: incomplete buffer')
130 130 value = metabuf[offset:offset + metalen]
131 131 metadict[key] = value
132 132 offset += metalen
133 133 if offset != buflen:
134 134 raise ValueError('corrupted metadata: redundant data')
135 135 return metadict
136 136
137 137 def _buildpackmeta(metadict):
138 138 """reverse of _parsepackmeta, dict -> bytes (<metadata-list>)
139 139
140 140 The dict contains raw content - both keys and values are strings.
141 141 Upper-level business may want to serialize some of other types (like
142 142 integers) to strings before calling this function.
143 143
144 144 raise ProgrammingError when metadata key is illegal, or ValueError if
145 145 length limit is exceeded
146 146 """
147 147 metabuf = ''
148 148 for k, v in sorted((metadict or {}).iteritems()):
149 149 if len(k) != 1:
150 150 raise error.ProgrammingError('packmeta: illegal key: %s' % k)
151 151 if len(v) > 0xfffe:
152 152 raise ValueError('metadata value is too long: 0x%x > 0xfffe'
153 153 % len(v))
154 154 metabuf += k
155 155 metabuf += struct.pack('!H', len(v))
156 156 metabuf += v
157 157 # len(metabuf) is guaranteed representable in 4 bytes, because there are
158 158 # only 256 keys, and for each value, len(value) <= 0xfffe.
159 159 return metabuf
160 160
161 161 _metaitemtypes = {
162 162 constants.METAKEYFLAG: (int, pycompat.long),
163 163 constants.METAKEYSIZE: (int, pycompat.long),
164 164 }
165 165
166 166 def buildpackmeta(metadict):
167 167 """like _buildpackmeta, but typechecks metadict and normalize it.
168 168
169 169 This means, METAKEYSIZE and METAKEYSIZE should have integers as values,
170 170 and METAKEYFLAG will be dropped if its value is 0.
171 171 """
172 172 newmeta = {}
173 173 for k, v in (metadict or {}).iteritems():
174 174 expectedtype = _metaitemtypes.get(k, (bytes,))
175 175 if not isinstance(v, expectedtype):
176 176 raise error.ProgrammingError('packmeta: wrong type of key %s' % k)
177 177 # normalize int to binary buffer
178 178 if int in expectedtype:
179 179 # optimization: remove flag if it's 0 to save space
180 180 if k == constants.METAKEYFLAG and v == 0:
181 181 continue
182 182 v = int2bin(v)
183 183 newmeta[k] = v
184 184 return _buildpackmeta(newmeta)
185 185
186 186 def parsepackmeta(metabuf):
187 187 """like _parsepackmeta, but convert fields to desired types automatically.
188 188
189 189 This means, METAKEYFLAG and METAKEYSIZE fields will be converted to
190 190 integers.
191 191 """
192 192 metadict = _parsepackmeta(metabuf)
193 193 for k, v in metadict.iteritems():
194 194 if k in _metaitemtypes and int in _metaitemtypes[k]:
195 195 metadict[k] = bin2int(v)
196 196 return metadict
197 197
198 198 def int2bin(n):
199 199 """convert a non-negative integer to raw binary buffer"""
200 200 buf = bytearray()
201 201 while n > 0:
202 202 buf.insert(0, n & 0xff)
203 203 n >>= 8
204 204 return bytes(buf)
205 205
206 206 def bin2int(buf):
207 207 """the reverse of int2bin, convert a binary buffer to an integer"""
208 208 x = 0
209 209 for b in bytearray(buf):
210 210 x <<= 8
211 211 x |= b
212 212 return x
213 213
214 214 def parsesizeflags(raw):
215 215 """given a remotefilelog blob, return (headersize, rawtextsize, flags)
216 216
217 217 see remotefilelogserver.createfileblob for the format.
218 218 raise RuntimeError if the content is illformed.
219 219 """
220 220 flags = revlog.REVIDX_DEFAULT_FLAGS
221 221 size = None
222 222 try:
223 223 index = raw.index('\0')
224 224 header = raw[:index]
225 225 if header.startswith('v'):
226 226 # v1 and above, header starts with 'v'
227 227 if header.startswith('v1\n'):
228 228 for s in header.split('\n'):
229 229 if s.startswith(constants.METAKEYSIZE):
230 230 size = int(s[len(constants.METAKEYSIZE):])
231 231 elif s.startswith(constants.METAKEYFLAG):
232 232 flags = int(s[len(constants.METAKEYFLAG):])
233 233 else:
234 234 raise RuntimeError('unsupported remotefilelog header: %s'
235 235 % header)
236 236 else:
237 237 # v0, str(int(size)) is the header
238 238 size = int(header)
239 239 except ValueError:
240 240 raise RuntimeError("unexpected remotefilelog header: illegal format")
241 241 if size is None:
242 242 raise RuntimeError("unexpected remotefilelog header: no size found")
243 243 return index + 1, size, flags
244 244
245 245 def buildfileblobheader(size, flags, version=None):
246 246 """return the header of a remotefilelog blob.
247 247
248 248 see remotefilelogserver.createfileblob for the format.
249 249 approximately the reverse of parsesizeflags.
250 250
251 251 version could be 0 or 1, or None (auto decide).
252 252 """
253 253 # choose v0 if flags is empty, otherwise v1
254 254 if version is None:
255 255 version = int(bool(flags))
256 256 if version == 1:
257 257 header = ('v1\n%s%d\n%s%d'
258 258 % (constants.METAKEYSIZE, size,
259 259 constants.METAKEYFLAG, flags))
260 260 elif version == 0:
261 261 if flags:
262 262 raise error.ProgrammingError('fileblob v0 does not support flag')
263 263 header = '%d' % size
264 264 else:
265 265 raise error.ProgrammingError('unknown fileblob version %d' % version)
266 266 return header
267 267
268 268 def ancestormap(raw):
269 269 offset, size, flags = parsesizeflags(raw)
270 270 start = offset + size
271 271
272 272 mapping = {}
273 273 while start < len(raw):
274 274 divider = raw.index('\0', start + 80)
275 275
276 276 currentnode = raw[start:(start + 20)]
277 277 p1 = raw[(start + 20):(start + 40)]
278 278 p2 = raw[(start + 40):(start + 60)]
279 279 linknode = raw[(start + 60):(start + 80)]
280 280 copyfrom = raw[(start + 80):divider]
281 281
282 282 mapping[currentnode] = (p1, p2, linknode, copyfrom)
283 283 start = divider + 1
284 284
285 285 return mapping
286 286
287 287 def readfile(path):
288 288 f = open(path, 'rb')
289 289 try:
290 290 result = f.read()
291 291
292 292 # we should never have empty files
293 293 if not result:
294 294 os.remove(path)
295 295 raise IOError("empty file: %s" % path)
296 296
297 297 return result
298 298 finally:
299 299 f.close()
300 300
301 301 def unlinkfile(filepath):
302 302 if pycompat.iswindows:
303 303 # On Windows, os.unlink cannnot delete readonly files
304 304 os.chmod(filepath, stat.S_IWUSR)
305 305 os.unlink(filepath)
306 306
307 307 def renamefile(source, destination):
308 308 if pycompat.iswindows:
309 309 # On Windows, os.rename cannot rename readonly files
310 310 # and cannot overwrite destination if it exists
311 311 os.chmod(source, stat.S_IWUSR)
312 312 if os.path.isfile(destination):
313 313 os.chmod(destination, stat.S_IWUSR)
314 314 os.unlink(destination)
315 315
316 316 os.rename(source, destination)
317 317
318 318 def writefile(path, content, readonly=False):
319 319 dirname, filename = os.path.split(path)
320 320 if not os.path.exists(dirname):
321 321 try:
322 322 os.makedirs(dirname)
323 323 except OSError as ex:
324 324 if ex.errno != errno.EEXIST:
325 325 raise
326 326
327 327 fd, temp = tempfile.mkstemp(prefix='.%s-' % filename, dir=dirname)
328 328 os.close(fd)
329 329
330 330 try:
331 331 f = util.posixfile(temp, 'wb')
332 332 f.write(content)
333 333 f.close()
334 334
335 335 if readonly:
336 336 mode = 0o444
337 337 else:
338 338 # tempfiles are created with 0o600, so we need to manually set the
339 339 # mode.
340 340 oldumask = os.umask(0)
341 341 # there's no way to get the umask without modifying it, so set it
342 342 # back
343 343 os.umask(oldumask)
344 344 mode = ~oldumask
345 345
346 346 renamefile(temp, path)
347 347 os.chmod(path, mode)
348 348 except Exception:
349 349 try:
350 350 unlinkfile(temp)
351 351 except OSError:
352 352 pass
353 353 raise
354 354
355 355 def sortnodes(nodes, parentfunc):
356 356 """Topologically sorts the nodes, using the parentfunc to find
357 357 the parents of nodes."""
358 358 nodes = set(nodes)
359 359 childmap = {}
360 360 parentmap = {}
361 361 roots = []
362 362
363 363 # Build a child and parent map
364 364 for n in nodes:
365 365 parents = [p for p in parentfunc(n) if p in nodes]
366 366 parentmap[n] = set(parents)
367 367 for p in parents:
368 368 childmap.setdefault(p, set()).add(n)
369 369 if not parents:
370 370 roots.append(n)
371 371
372 372 roots.sort()
373 373 # Process roots, adding children to the queue as they become roots
374 374 results = []
375 375 while roots:
376 376 n = roots.pop(0)
377 377 results.append(n)
378 378 if n in childmap:
379 379 children = childmap[n]
380 380 for c in children:
381 381 childparents = parentmap[c]
382 382 childparents.remove(n)
383 383 if len(childparents) == 0:
384 384 # insert at the beginning, that way child nodes
385 385 # are likely to be output immediately after their
386 386 # parents. This gives better compression results.
387 387 roots.insert(0, c)
388 388
389 389 return results
390 390
391 391 def readexactly(stream, n):
392 392 '''read n bytes from stream.read and abort if less was available'''
393 393 s = stream.read(n)
394 394 if len(s) < n:
395 395 raise error.Abort(_("stream ended unexpectedly"
396 396 " (got %d bytes, expected %d)")
397 397 % (len(s), n))
398 398 return s
399 399
400 400 def readunpack(stream, fmt):
401 401 data = readexactly(stream, struct.calcsize(fmt))
402 402 return struct.unpack(fmt, data)
403 403
404 404 def readpath(stream):
405 405 rawlen = readexactly(stream, constants.FILENAMESIZE)
406 406 pathlen = struct.unpack(constants.FILENAMESTRUCT, rawlen)[0]
407 407 return readexactly(stream, pathlen)
408 408
409 409 def readnodelist(stream):
410 410 rawlen = readexactly(stream, constants.NODECOUNTSIZE)
411 411 nodecount = struct.unpack(constants.NODECOUNTSTRUCT, rawlen)[0]
412 412 for i in pycompat.xrange(nodecount):
413 413 yield readexactly(stream, constants.NODESIZE)
414 414
415 415 def readpathlist(stream):
416 416 rawlen = readexactly(stream, constants.PATHCOUNTSIZE)
417 417 pathcount = struct.unpack(constants.PATHCOUNTSTRUCT, rawlen)[0]
418 418 for i in pycompat.xrange(pathcount):
419 419 yield readpath(stream)
420 420
421 421 def getgid(groupname):
422 422 try:
423 423 gid = grp.getgrnam(groupname).gr_gid
424 424 return gid
425 425 except KeyError:
426 426 return None
427 427
428 428 def setstickygroupdir(path, gid, warn=None):
429 429 if gid is None:
430 430 return
431 431 try:
432 432 os.chown(path, -1, gid)
433 433 os.chmod(path, 0o2775)
434 434 except (IOError, OSError) as ex:
435 435 if warn:
436 436 warn(_('unable to chown/chmod on %s: %s\n') % (path, ex))
437 437
438 438 def mkstickygroupdir(ui, path):
439 439 """Creates the given directory (if it doesn't exist) and give it a
440 440 particular group with setgid enabled."""
441 441 gid = None
442 442 groupname = ui.config("remotefilelog", "cachegroup")
443 443 if groupname:
444 444 gid = getgid(groupname)
445 445 if gid is None:
446 446 ui.warn(_('unable to resolve group name: %s\n') % groupname)
447 447
448 448 # we use a single stat syscall to test the existence and mode / group bit
449 449 st = None
450 450 try:
451 451 st = os.stat(path)
452 452 except OSError:
453 453 pass
454 454
455 455 if st:
456 456 # exists
457 457 if (st.st_mode & 0o2775) != 0o2775 or st.st_gid != gid:
458 458 # permission needs to be fixed
459 459 setstickygroupdir(path, gid, ui.warn)
460 460 return
461 461
462 462 oldumask = os.umask(0o002)
463 463 try:
464 464 missingdirs = [path]
465 465 path = os.path.dirname(path)
466 466 while path and not os.path.exists(path):
467 467 missingdirs.append(path)
468 468 path = os.path.dirname(path)
469 469
470 470 for path in reversed(missingdirs):
471 471 try:
472 472 os.mkdir(path)
473 473 except OSError as ex:
474 474 if ex.errno != errno.EEXIST:
475 475 raise
476 476
477 477 for path in missingdirs:
478 478 setstickygroupdir(path, gid, ui.warn)
479 479 finally:
480 480 os.umask(oldumask)
481 481
482 482 def getusername(ui):
483 483 try:
484 484 return stringutil.shortuser(ui.username())
485 485 except Exception:
486 486 return 'unknown'
487 487
488 488 def getreponame(ui):
489 489 reponame = ui.config('paths', 'default')
490 490 if reponame:
491 491 return os.path.basename(reponame)
492 492 return "unknown"
General Comments 0
You need to be logged in to leave comments. Login now