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