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