##// END OF EJS Templates
vfs: make atomictempfile avoid ambiguity of file stat if needed...
FUJIWARA Katsunori -
r29202:76f1ea36 default
parent child Browse files
Show More
@@ -1,1378 +1,1381
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright 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 __future__ import absolute_import
9 9
10 10 import contextlib
11 11 import errno
12 12 import glob
13 13 import os
14 14 import re
15 15 import shutil
16 16 import stat
17 17 import tempfile
18 18 import threading
19 19
20 20 from .i18n import _
21 21 from .node import wdirrev
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 match as matchmod,
26 26 osutil,
27 27 pathutil,
28 28 phases,
29 29 revset,
30 30 similar,
31 31 util,
32 32 )
33 33
34 34 if os.name == 'nt':
35 35 from . import scmwindows as scmplatform
36 36 else:
37 37 from . import scmposix as scmplatform
38 38
39 39 systemrcpath = scmplatform.systemrcpath
40 40 userrcpath = scmplatform.userrcpath
41 41
42 42 class status(tuple):
43 43 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
44 44 and 'ignored' properties are only relevant to the working copy.
45 45 '''
46 46
47 47 __slots__ = ()
48 48
49 49 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
50 50 clean):
51 51 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
52 52 ignored, clean))
53 53
54 54 @property
55 55 def modified(self):
56 56 '''files that have been modified'''
57 57 return self[0]
58 58
59 59 @property
60 60 def added(self):
61 61 '''files that have been added'''
62 62 return self[1]
63 63
64 64 @property
65 65 def removed(self):
66 66 '''files that have been removed'''
67 67 return self[2]
68 68
69 69 @property
70 70 def deleted(self):
71 71 '''files that are in the dirstate, but have been deleted from the
72 72 working copy (aka "missing")
73 73 '''
74 74 return self[3]
75 75
76 76 @property
77 77 def unknown(self):
78 78 '''files not in the dirstate that are not ignored'''
79 79 return self[4]
80 80
81 81 @property
82 82 def ignored(self):
83 83 '''files not in the dirstate that are ignored (by _dirignore())'''
84 84 return self[5]
85 85
86 86 @property
87 87 def clean(self):
88 88 '''files that have not been modified'''
89 89 return self[6]
90 90
91 91 def __repr__(self, *args, **kwargs):
92 92 return (('<status modified=%r, added=%r, removed=%r, deleted=%r, '
93 93 'unknown=%r, ignored=%r, clean=%r>') % self)
94 94
95 95 def itersubrepos(ctx1, ctx2):
96 96 """find subrepos in ctx1 or ctx2"""
97 97 # Create a (subpath, ctx) mapping where we prefer subpaths from
98 98 # ctx1. The subpaths from ctx2 are important when the .hgsub file
99 99 # has been modified (in ctx2) but not yet committed (in ctx1).
100 100 subpaths = dict.fromkeys(ctx2.substate, ctx2)
101 101 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
102 102
103 103 missing = set()
104 104
105 105 for subpath in ctx2.substate:
106 106 if subpath not in ctx1.substate:
107 107 del subpaths[subpath]
108 108 missing.add(subpath)
109 109
110 110 for subpath, ctx in sorted(subpaths.iteritems()):
111 111 yield subpath, ctx.sub(subpath)
112 112
113 113 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
114 114 # status and diff will have an accurate result when it does
115 115 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
116 116 # against itself.
117 117 for subpath in missing:
118 118 yield subpath, ctx2.nullsub(subpath, ctx1)
119 119
120 120 def nochangesfound(ui, repo, excluded=None):
121 121 '''Report no changes for push/pull, excluded is None or a list of
122 122 nodes excluded from the push/pull.
123 123 '''
124 124 secretlist = []
125 125 if excluded:
126 126 for n in excluded:
127 127 if n not in repo:
128 128 # discovery should not have included the filtered revision,
129 129 # we have to explicitly exclude it until discovery is cleanup.
130 130 continue
131 131 ctx = repo[n]
132 132 if ctx.phase() >= phases.secret and not ctx.extinct():
133 133 secretlist.append(n)
134 134
135 135 if secretlist:
136 136 ui.status(_("no changes found (ignored %d secret changesets)\n")
137 137 % len(secretlist))
138 138 else:
139 139 ui.status(_("no changes found\n"))
140 140
141 141 def checknewlabel(repo, lbl, kind):
142 142 # Do not use the "kind" parameter in ui output.
143 143 # It makes strings difficult to translate.
144 144 if lbl in ['tip', '.', 'null']:
145 145 raise error.Abort(_("the name '%s' is reserved") % lbl)
146 146 for c in (':', '\0', '\n', '\r'):
147 147 if c in lbl:
148 148 raise error.Abort(_("%r cannot be used in a name") % c)
149 149 try:
150 150 int(lbl)
151 151 raise error.Abort(_("cannot use an integer as a name"))
152 152 except ValueError:
153 153 pass
154 154
155 155 def checkfilename(f):
156 156 '''Check that the filename f is an acceptable filename for a tracked file'''
157 157 if '\r' in f or '\n' in f:
158 158 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
159 159
160 160 def checkportable(ui, f):
161 161 '''Check if filename f is portable and warn or abort depending on config'''
162 162 checkfilename(f)
163 163 abort, warn = checkportabilityalert(ui)
164 164 if abort or warn:
165 165 msg = util.checkwinfilename(f)
166 166 if msg:
167 167 msg = "%s: %r" % (msg, f)
168 168 if abort:
169 169 raise error.Abort(msg)
170 170 ui.warn(_("warning: %s\n") % msg)
171 171
172 172 def checkportabilityalert(ui):
173 173 '''check if the user's config requests nothing, a warning, or abort for
174 174 non-portable filenames'''
175 175 val = ui.config('ui', 'portablefilenames', 'warn')
176 176 lval = val.lower()
177 177 bval = util.parsebool(val)
178 178 abort = os.name == 'nt' or lval == 'abort'
179 179 warn = bval or lval == 'warn'
180 180 if bval is None and not (warn or abort or lval == 'ignore'):
181 181 raise error.ConfigError(
182 182 _("ui.portablefilenames value is invalid ('%s')") % val)
183 183 return abort, warn
184 184
185 185 class casecollisionauditor(object):
186 186 def __init__(self, ui, abort, dirstate):
187 187 self._ui = ui
188 188 self._abort = abort
189 189 allfiles = '\0'.join(dirstate._map)
190 190 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
191 191 self._dirstate = dirstate
192 192 # The purpose of _newfiles is so that we don't complain about
193 193 # case collisions if someone were to call this object with the
194 194 # same filename twice.
195 195 self._newfiles = set()
196 196
197 197 def __call__(self, f):
198 198 if f in self._newfiles:
199 199 return
200 200 fl = encoding.lower(f)
201 201 if fl in self._loweredfiles and f not in self._dirstate:
202 202 msg = _('possible case-folding collision for %s') % f
203 203 if self._abort:
204 204 raise error.Abort(msg)
205 205 self._ui.warn(_("warning: %s\n") % msg)
206 206 self._loweredfiles.add(fl)
207 207 self._newfiles.add(f)
208 208
209 209 def filteredhash(repo, maxrev):
210 210 """build hash of filtered revisions in the current repoview.
211 211
212 212 Multiple caches perform up-to-date validation by checking that the
213 213 tiprev and tipnode stored in the cache file match the current repository.
214 214 However, this is not sufficient for validating repoviews because the set
215 215 of revisions in the view may change without the repository tiprev and
216 216 tipnode changing.
217 217
218 218 This function hashes all the revs filtered from the view and returns
219 219 that SHA-1 digest.
220 220 """
221 221 cl = repo.changelog
222 222 if not cl.filteredrevs:
223 223 return None
224 224 key = None
225 225 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
226 226 if revs:
227 227 s = util.sha1()
228 228 for rev in revs:
229 229 s.update('%s;' % rev)
230 230 key = s.digest()
231 231 return key
232 232
233 233 class abstractvfs(object):
234 234 """Abstract base class; cannot be instantiated"""
235 235
236 236 def __init__(self, *args, **kwargs):
237 237 '''Prevent instantiation; don't call this from subclasses.'''
238 238 raise NotImplementedError('attempted instantiating ' + str(type(self)))
239 239
240 240 def tryread(self, path):
241 241 '''gracefully return an empty string for missing files'''
242 242 try:
243 243 return self.read(path)
244 244 except IOError as inst:
245 245 if inst.errno != errno.ENOENT:
246 246 raise
247 247 return ""
248 248
249 249 def tryreadlines(self, path, mode='rb'):
250 250 '''gracefully return an empty array for missing files'''
251 251 try:
252 252 return self.readlines(path, mode=mode)
253 253 except IOError as inst:
254 254 if inst.errno != errno.ENOENT:
255 255 raise
256 256 return []
257 257
258 258 def open(self, path, mode="r", text=False, atomictemp=False,
259 259 notindexed=False, backgroundclose=False):
260 260 '''Open ``path`` file, which is relative to vfs root.
261 261
262 262 Newly created directories are marked as "not to be indexed by
263 263 the content indexing service", if ``notindexed`` is specified
264 264 for "write" mode access.
265 265 '''
266 266 self.open = self.__call__
267 267 return self.__call__(path, mode, text, atomictemp, notindexed,
268 268 backgroundclose=backgroundclose)
269 269
270 270 def read(self, path):
271 271 with self(path, 'rb') as fp:
272 272 return fp.read()
273 273
274 274 def readlines(self, path, mode='rb'):
275 275 with self(path, mode=mode) as fp:
276 276 return fp.readlines()
277 277
278 278 def write(self, path, data, backgroundclose=False):
279 279 with self(path, 'wb', backgroundclose=backgroundclose) as fp:
280 280 return fp.write(data)
281 281
282 282 def writelines(self, path, data, mode='wb', notindexed=False):
283 283 with self(path, mode=mode, notindexed=notindexed) as fp:
284 284 return fp.writelines(data)
285 285
286 286 def append(self, path, data):
287 287 with self(path, 'ab') as fp:
288 288 return fp.write(data)
289 289
290 290 def basename(self, path):
291 291 """return base element of a path (as os.path.basename would do)
292 292
293 293 This exists to allow handling of strange encoding if needed."""
294 294 return os.path.basename(path)
295 295
296 296 def chmod(self, path, mode):
297 297 return os.chmod(self.join(path), mode)
298 298
299 299 def dirname(self, path):
300 300 """return dirname element of a path (as os.path.dirname would do)
301 301
302 302 This exists to allow handling of strange encoding if needed."""
303 303 return os.path.dirname(path)
304 304
305 305 def exists(self, path=None):
306 306 return os.path.exists(self.join(path))
307 307
308 308 def fstat(self, fp):
309 309 return util.fstat(fp)
310 310
311 311 def isdir(self, path=None):
312 312 return os.path.isdir(self.join(path))
313 313
314 314 def isfile(self, path=None):
315 315 return os.path.isfile(self.join(path))
316 316
317 317 def islink(self, path=None):
318 318 return os.path.islink(self.join(path))
319 319
320 320 def isfileorlink(self, path=None):
321 321 '''return whether path is a regular file or a symlink
322 322
323 323 Unlike isfile, this doesn't follow symlinks.'''
324 324 try:
325 325 st = self.lstat(path)
326 326 except OSError:
327 327 return False
328 328 mode = st.st_mode
329 329 return stat.S_ISREG(mode) or stat.S_ISLNK(mode)
330 330
331 331 def reljoin(self, *paths):
332 332 """join various elements of a path together (as os.path.join would do)
333 333
334 334 The vfs base is not injected so that path stay relative. This exists
335 335 to allow handling of strange encoding if needed."""
336 336 return os.path.join(*paths)
337 337
338 338 def split(self, path):
339 339 """split top-most element of a path (as os.path.split would do)
340 340
341 341 This exists to allow handling of strange encoding if needed."""
342 342 return os.path.split(path)
343 343
344 344 def lexists(self, path=None):
345 345 return os.path.lexists(self.join(path))
346 346
347 347 def lstat(self, path=None):
348 348 return os.lstat(self.join(path))
349 349
350 350 def listdir(self, path=None):
351 351 return os.listdir(self.join(path))
352 352
353 353 def makedir(self, path=None, notindexed=True):
354 354 return util.makedir(self.join(path), notindexed)
355 355
356 356 def makedirs(self, path=None, mode=None):
357 357 return util.makedirs(self.join(path), mode)
358 358
359 359 def makelock(self, info, path):
360 360 return util.makelock(info, self.join(path))
361 361
362 362 def mkdir(self, path=None):
363 363 return os.mkdir(self.join(path))
364 364
365 365 def mkstemp(self, suffix='', prefix='tmp', dir=None, text=False):
366 366 fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix,
367 367 dir=self.join(dir), text=text)
368 368 dname, fname = util.split(name)
369 369 if dir:
370 370 return fd, os.path.join(dir, fname)
371 371 else:
372 372 return fd, fname
373 373
374 374 def readdir(self, path=None, stat=None, skip=None):
375 375 return osutil.listdir(self.join(path), stat, skip)
376 376
377 377 def readlock(self, path):
378 378 return util.readlock(self.join(path))
379 379
380 380 def rename(self, src, dst):
381 381 return util.rename(self.join(src), self.join(dst))
382 382
383 383 def readlink(self, path):
384 384 return os.readlink(self.join(path))
385 385
386 386 def removedirs(self, path=None):
387 387 """Remove a leaf directory and all empty intermediate ones
388 388 """
389 389 return util.removedirs(self.join(path))
390 390
391 391 def rmtree(self, path=None, ignore_errors=False, forcibly=False):
392 392 """Remove a directory tree recursively
393 393
394 394 If ``forcibly``, this tries to remove READ-ONLY files, too.
395 395 """
396 396 if forcibly:
397 397 def onerror(function, path, excinfo):
398 398 if function is not os.remove:
399 399 raise
400 400 # read-only files cannot be unlinked under Windows
401 401 s = os.stat(path)
402 402 if (s.st_mode & stat.S_IWRITE) != 0:
403 403 raise
404 404 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
405 405 os.remove(path)
406 406 else:
407 407 onerror = None
408 408 return shutil.rmtree(self.join(path),
409 409 ignore_errors=ignore_errors, onerror=onerror)
410 410
411 411 def setflags(self, path, l, x):
412 412 return util.setflags(self.join(path), l, x)
413 413
414 414 def stat(self, path=None):
415 415 return os.stat(self.join(path))
416 416
417 417 def unlink(self, path=None):
418 418 return util.unlink(self.join(path))
419 419
420 420 def unlinkpath(self, path=None, ignoremissing=False):
421 421 return util.unlinkpath(self.join(path), ignoremissing)
422 422
423 423 def utime(self, path=None, t=None):
424 424 return os.utime(self.join(path), t)
425 425
426 426 def walk(self, path=None, onerror=None):
427 427 """Yield (dirpath, dirs, files) tuple for each directories under path
428 428
429 429 ``dirpath`` is relative one from the root of this vfs. This
430 430 uses ``os.sep`` as path separator, even you specify POSIX
431 431 style ``path``.
432 432
433 433 "The root of this vfs" is represented as empty ``dirpath``.
434 434 """
435 435 root = os.path.normpath(self.join(None))
436 436 # when dirpath == root, dirpath[prefixlen:] becomes empty
437 437 # because len(dirpath) < prefixlen.
438 438 prefixlen = len(pathutil.normasprefix(root))
439 439 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
440 440 yield (dirpath[prefixlen:], dirs, files)
441 441
442 442 @contextlib.contextmanager
443 443 def backgroundclosing(self, ui, expectedcount=-1):
444 444 """Allow files to be closed asynchronously.
445 445
446 446 When this context manager is active, ``backgroundclose`` can be passed
447 447 to ``__call__``/``open`` to result in the file possibly being closed
448 448 asynchronously, on a background thread.
449 449 """
450 450 # This is an arbitrary restriction and could be changed if we ever
451 451 # have a use case.
452 452 vfs = getattr(self, 'vfs', self)
453 453 if getattr(vfs, '_backgroundfilecloser', None):
454 454 raise error.Abort('can only have 1 active background file closer')
455 455
456 456 with backgroundfilecloser(ui, expectedcount=expectedcount) as bfc:
457 457 try:
458 458 vfs._backgroundfilecloser = bfc
459 459 yield bfc
460 460 finally:
461 461 vfs._backgroundfilecloser = None
462 462
463 463 class vfs(abstractvfs):
464 464 '''Operate files relative to a base directory
465 465
466 466 This class is used to hide the details of COW semantics and
467 467 remote file access from higher level code.
468 468 '''
469 469 def __init__(self, base, audit=True, expandpath=False, realpath=False):
470 470 if expandpath:
471 471 base = util.expandpath(base)
472 472 if realpath:
473 473 base = os.path.realpath(base)
474 474 self.base = base
475 475 self.mustaudit = audit
476 476 self.createmode = None
477 477 self._trustnlink = None
478 478
479 479 @property
480 480 def mustaudit(self):
481 481 return self._audit
482 482
483 483 @mustaudit.setter
484 484 def mustaudit(self, onoff):
485 485 self._audit = onoff
486 486 if onoff:
487 487 self.audit = pathutil.pathauditor(self.base)
488 488 else:
489 489 self.audit = util.always
490 490
491 491 @util.propertycache
492 492 def _cansymlink(self):
493 493 return util.checklink(self.base)
494 494
495 495 @util.propertycache
496 496 def _chmod(self):
497 497 return util.checkexec(self.base)
498 498
499 499 def _fixfilemode(self, name):
500 500 if self.createmode is None or not self._chmod:
501 501 return
502 502 os.chmod(name, self.createmode & 0o666)
503 503
504 504 def __call__(self, path, mode="r", text=False, atomictemp=False,
505 notindexed=False, backgroundclose=False):
505 notindexed=False, backgroundclose=False, checkambig=False):
506 506 '''Open ``path`` file, which is relative to vfs root.
507 507
508 508 Newly created directories are marked as "not to be indexed by
509 509 the content indexing service", if ``notindexed`` is specified
510 510 for "write" mode access.
511 511
512 512 If ``backgroundclose`` is passed, the file may be closed asynchronously.
513 513 It can only be used if the ``self.backgroundclosing()`` context manager
514 514 is active. This should only be specified if the following criteria hold:
515 515
516 516 1. There is a potential for writing thousands of files. Unless you
517 517 are writing thousands of files, the performance benefits of
518 518 asynchronously closing files is not realized.
519 519 2. Files are opened exactly once for the ``backgroundclosing``
520 520 active duration and are therefore free of race conditions between
521 521 closing a file on a background thread and reopening it. (If the
522 522 file were opened multiple times, there could be unflushed data
523 523 because the original file handle hasn't been flushed/closed yet.)
524
525 ``checkambig`` is passed to atomictempfile (valid only for writing).
524 526 '''
525 527 if self._audit:
526 528 r = util.checkosfilename(path)
527 529 if r:
528 530 raise error.Abort("%s: %r" % (r, path))
529 531 self.audit(path)
530 532 f = self.join(path)
531 533
532 534 if not text and "b" not in mode:
533 535 mode += "b" # for that other OS
534 536
535 537 nlink = -1
536 538 if mode not in ('r', 'rb'):
537 539 dirname, basename = util.split(f)
538 540 # If basename is empty, then the path is malformed because it points
539 541 # to a directory. Let the posixfile() call below raise IOError.
540 542 if basename:
541 543 if atomictemp:
542 544 util.makedirs(dirname, self.createmode, notindexed)
543 return util.atomictempfile(f, mode, self.createmode)
545 return util.atomictempfile(f, mode, self.createmode,
546 checkambig=checkambig)
544 547 try:
545 548 if 'w' in mode:
546 549 util.unlink(f)
547 550 nlink = 0
548 551 else:
549 552 # nlinks() may behave differently for files on Windows
550 553 # shares if the file is open.
551 554 with util.posixfile(f):
552 555 nlink = util.nlinks(f)
553 556 if nlink < 1:
554 557 nlink = 2 # force mktempcopy (issue1922)
555 558 except (OSError, IOError) as e:
556 559 if e.errno != errno.ENOENT:
557 560 raise
558 561 nlink = 0
559 562 util.makedirs(dirname, self.createmode, notindexed)
560 563 if nlink > 0:
561 564 if self._trustnlink is None:
562 565 self._trustnlink = nlink > 1 or util.checknlink(f)
563 566 if nlink > 1 or not self._trustnlink:
564 567 util.rename(util.mktempcopy(f), f)
565 568 fp = util.posixfile(f, mode)
566 569 if nlink == 0:
567 570 self._fixfilemode(f)
568 571
569 572 if backgroundclose:
570 573 if not self._backgroundfilecloser:
571 574 raise error.Abort('backgroundclose can only be used when a '
572 575 'backgroundclosing context manager is active')
573 576
574 577 fp = delayclosedfile(fp, self._backgroundfilecloser)
575 578
576 579 return fp
577 580
578 581 def symlink(self, src, dst):
579 582 self.audit(dst)
580 583 linkname = self.join(dst)
581 584 try:
582 585 os.unlink(linkname)
583 586 except OSError:
584 587 pass
585 588
586 589 util.makedirs(os.path.dirname(linkname), self.createmode)
587 590
588 591 if self._cansymlink:
589 592 try:
590 593 os.symlink(src, linkname)
591 594 except OSError as err:
592 595 raise OSError(err.errno, _('could not symlink to %r: %s') %
593 596 (src, err.strerror), linkname)
594 597 else:
595 598 self.write(dst, src)
596 599
597 600 def join(self, path, *insidef):
598 601 if path:
599 602 return os.path.join(self.base, path, *insidef)
600 603 else:
601 604 return self.base
602 605
603 606 opener = vfs
604 607
605 608 class auditvfs(object):
606 609 def __init__(self, vfs):
607 610 self.vfs = vfs
608 611
609 612 @property
610 613 def mustaudit(self):
611 614 return self.vfs.mustaudit
612 615
613 616 @mustaudit.setter
614 617 def mustaudit(self, onoff):
615 618 self.vfs.mustaudit = onoff
616 619
617 620 class filtervfs(abstractvfs, auditvfs):
618 621 '''Wrapper vfs for filtering filenames with a function.'''
619 622
620 623 def __init__(self, vfs, filter):
621 624 auditvfs.__init__(self, vfs)
622 625 self._filter = filter
623 626
624 627 def __call__(self, path, *args, **kwargs):
625 628 return self.vfs(self._filter(path), *args, **kwargs)
626 629
627 630 def join(self, path, *insidef):
628 631 if path:
629 632 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
630 633 else:
631 634 return self.vfs.join(path)
632 635
633 636 filteropener = filtervfs
634 637
635 638 class readonlyvfs(abstractvfs, auditvfs):
636 639 '''Wrapper vfs preventing any writing.'''
637 640
638 641 def __init__(self, vfs):
639 642 auditvfs.__init__(self, vfs)
640 643
641 644 def __call__(self, path, mode='r', *args, **kw):
642 645 if mode not in ('r', 'rb'):
643 646 raise error.Abort('this vfs is read only')
644 647 return self.vfs(path, mode, *args, **kw)
645 648
646 649 def join(self, path, *insidef):
647 650 return self.vfs.join(path, *insidef)
648 651
649 652 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
650 653 '''yield every hg repository under path, always recursively.
651 654 The recurse flag will only control recursion into repo working dirs'''
652 655 def errhandler(err):
653 656 if err.filename == path:
654 657 raise err
655 658 samestat = getattr(os.path, 'samestat', None)
656 659 if followsym and samestat is not None:
657 660 def adddir(dirlst, dirname):
658 661 match = False
659 662 dirstat = os.stat(dirname)
660 663 for lstdirstat in dirlst:
661 664 if samestat(dirstat, lstdirstat):
662 665 match = True
663 666 break
664 667 if not match:
665 668 dirlst.append(dirstat)
666 669 return not match
667 670 else:
668 671 followsym = False
669 672
670 673 if (seen_dirs is None) and followsym:
671 674 seen_dirs = []
672 675 adddir(seen_dirs, path)
673 676 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
674 677 dirs.sort()
675 678 if '.hg' in dirs:
676 679 yield root # found a repository
677 680 qroot = os.path.join(root, '.hg', 'patches')
678 681 if os.path.isdir(os.path.join(qroot, '.hg')):
679 682 yield qroot # we have a patch queue repo here
680 683 if recurse:
681 684 # avoid recursing inside the .hg directory
682 685 dirs.remove('.hg')
683 686 else:
684 687 dirs[:] = [] # don't descend further
685 688 elif followsym:
686 689 newdirs = []
687 690 for d in dirs:
688 691 fname = os.path.join(root, d)
689 692 if adddir(seen_dirs, fname):
690 693 if os.path.islink(fname):
691 694 for hgname in walkrepos(fname, True, seen_dirs):
692 695 yield hgname
693 696 else:
694 697 newdirs.append(d)
695 698 dirs[:] = newdirs
696 699
697 700 def osrcpath():
698 701 '''return default os-specific hgrc search path'''
699 702 path = []
700 703 defaultpath = os.path.join(util.datapath, 'default.d')
701 704 if os.path.isdir(defaultpath):
702 705 for f, kind in osutil.listdir(defaultpath):
703 706 if f.endswith('.rc'):
704 707 path.append(os.path.join(defaultpath, f))
705 708 path.extend(systemrcpath())
706 709 path.extend(userrcpath())
707 710 path = [os.path.normpath(f) for f in path]
708 711 return path
709 712
710 713 _rcpath = None
711 714
712 715 def rcpath():
713 716 '''return hgrc search path. if env var HGRCPATH is set, use it.
714 717 for each item in path, if directory, use files ending in .rc,
715 718 else use item.
716 719 make HGRCPATH empty to only look in .hg/hgrc of current repo.
717 720 if no HGRCPATH, use default os-specific path.'''
718 721 global _rcpath
719 722 if _rcpath is None:
720 723 if 'HGRCPATH' in os.environ:
721 724 _rcpath = []
722 725 for p in os.environ['HGRCPATH'].split(os.pathsep):
723 726 if not p:
724 727 continue
725 728 p = util.expandpath(p)
726 729 if os.path.isdir(p):
727 730 for f, kind in osutil.listdir(p):
728 731 if f.endswith('.rc'):
729 732 _rcpath.append(os.path.join(p, f))
730 733 else:
731 734 _rcpath.append(p)
732 735 else:
733 736 _rcpath = osrcpath()
734 737 return _rcpath
735 738
736 739 def intrev(rev):
737 740 """Return integer for a given revision that can be used in comparison or
738 741 arithmetic operation"""
739 742 if rev is None:
740 743 return wdirrev
741 744 return rev
742 745
743 746 def revsingle(repo, revspec, default='.'):
744 747 if not revspec and revspec != 0:
745 748 return repo[default]
746 749
747 750 l = revrange(repo, [revspec])
748 751 if not l:
749 752 raise error.Abort(_('empty revision set'))
750 753 return repo[l.last()]
751 754
752 755 def _pairspec(revspec):
753 756 tree = revset.parse(revspec)
754 757 tree = revset.optimize(tree) # fix up "x^:y" -> "(x^):y"
755 758 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
756 759
757 760 def revpair(repo, revs):
758 761 if not revs:
759 762 return repo.dirstate.p1(), None
760 763
761 764 l = revrange(repo, revs)
762 765
763 766 if not l:
764 767 first = second = None
765 768 elif l.isascending():
766 769 first = l.min()
767 770 second = l.max()
768 771 elif l.isdescending():
769 772 first = l.max()
770 773 second = l.min()
771 774 else:
772 775 first = l.first()
773 776 second = l.last()
774 777
775 778 if first is None:
776 779 raise error.Abort(_('empty revision range'))
777 780 if (first == second and len(revs) >= 2
778 781 and not all(revrange(repo, [r]) for r in revs)):
779 782 raise error.Abort(_('empty revision on one side of range'))
780 783
781 784 # if top-level is range expression, the result must always be a pair
782 785 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
783 786 return repo.lookup(first), None
784 787
785 788 return repo.lookup(first), repo.lookup(second)
786 789
787 790 def revrange(repo, revs):
788 791 """Yield revision as strings from a list of revision specifications."""
789 792 allspecs = []
790 793 for spec in revs:
791 794 if isinstance(spec, int):
792 795 spec = revset.formatspec('rev(%d)', spec)
793 796 allspecs.append(spec)
794 797 m = revset.matchany(repo.ui, allspecs, repo)
795 798 return m(repo)
796 799
797 800 def meaningfulparents(repo, ctx):
798 801 """Return list of meaningful (or all if debug) parentrevs for rev.
799 802
800 803 For merges (two non-nullrev revisions) both parents are meaningful.
801 804 Otherwise the first parent revision is considered meaningful if it
802 805 is not the preceding revision.
803 806 """
804 807 parents = ctx.parents()
805 808 if len(parents) > 1:
806 809 return parents
807 810 if repo.ui.debugflag:
808 811 return [parents[0], repo['null']]
809 812 if parents[0].rev() >= intrev(ctx.rev()) - 1:
810 813 return []
811 814 return parents
812 815
813 816 def expandpats(pats):
814 817 '''Expand bare globs when running on windows.
815 818 On posix we assume it already has already been done by sh.'''
816 819 if not util.expandglobs:
817 820 return list(pats)
818 821 ret = []
819 822 for kindpat in pats:
820 823 kind, pat = matchmod._patsplit(kindpat, None)
821 824 if kind is None:
822 825 try:
823 826 globbed = glob.glob(pat)
824 827 except re.error:
825 828 globbed = [pat]
826 829 if globbed:
827 830 ret.extend(globbed)
828 831 continue
829 832 ret.append(kindpat)
830 833 return ret
831 834
832 835 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
833 836 badfn=None):
834 837 '''Return a matcher and the patterns that were used.
835 838 The matcher will warn about bad matches, unless an alternate badfn callback
836 839 is provided.'''
837 840 if pats == ("",):
838 841 pats = []
839 842 if opts is None:
840 843 opts = {}
841 844 if not globbed and default == 'relpath':
842 845 pats = expandpats(pats or [])
843 846
844 847 def bad(f, msg):
845 848 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
846 849
847 850 if badfn is None:
848 851 badfn = bad
849 852
850 853 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
851 854 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
852 855
853 856 if m.always():
854 857 pats = []
855 858 return m, pats
856 859
857 860 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
858 861 badfn=None):
859 862 '''Return a matcher that will warn about bad matches.'''
860 863 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
861 864
862 865 def matchall(repo):
863 866 '''Return a matcher that will efficiently match everything.'''
864 867 return matchmod.always(repo.root, repo.getcwd())
865 868
866 869 def matchfiles(repo, files, badfn=None):
867 870 '''Return a matcher that will efficiently match exactly these files.'''
868 871 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
869 872
870 873 def origpath(ui, repo, filepath):
871 874 '''customize where .orig files are created
872 875
873 876 Fetch user defined path from config file: [ui] origbackuppath = <path>
874 877 Fall back to default (filepath) if not specified
875 878 '''
876 879 origbackuppath = ui.config('ui', 'origbackuppath', None)
877 880 if origbackuppath is None:
878 881 return filepath + ".orig"
879 882
880 883 filepathfromroot = os.path.relpath(filepath, start=repo.root)
881 884 fullorigpath = repo.wjoin(origbackuppath, filepathfromroot)
882 885
883 886 origbackupdir = repo.vfs.dirname(fullorigpath)
884 887 if not repo.vfs.exists(origbackupdir):
885 888 ui.note(_('creating directory: %s\n') % origbackupdir)
886 889 util.makedirs(origbackupdir)
887 890
888 891 return fullorigpath + ".orig"
889 892
890 893 def addremove(repo, matcher, prefix, opts=None, dry_run=None, similarity=None):
891 894 if opts is None:
892 895 opts = {}
893 896 m = matcher
894 897 if dry_run is None:
895 898 dry_run = opts.get('dry_run')
896 899 if similarity is None:
897 900 similarity = float(opts.get('similarity') or 0)
898 901
899 902 ret = 0
900 903 join = lambda f: os.path.join(prefix, f)
901 904
902 905 def matchessubrepo(matcher, subpath):
903 906 if matcher.exact(subpath):
904 907 return True
905 908 for f in matcher.files():
906 909 if f.startswith(subpath):
907 910 return True
908 911 return False
909 912
910 913 wctx = repo[None]
911 914 for subpath in sorted(wctx.substate):
912 915 if opts.get('subrepos') or matchessubrepo(m, subpath):
913 916 sub = wctx.sub(subpath)
914 917 try:
915 918 submatch = matchmod.subdirmatcher(subpath, m)
916 919 if sub.addremove(submatch, prefix, opts, dry_run, similarity):
917 920 ret = 1
918 921 except error.LookupError:
919 922 repo.ui.status(_("skipping missing subrepository: %s\n")
920 923 % join(subpath))
921 924
922 925 rejected = []
923 926 def badfn(f, msg):
924 927 if f in m.files():
925 928 m.bad(f, msg)
926 929 rejected.append(f)
927 930
928 931 badmatch = matchmod.badmatch(m, badfn)
929 932 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
930 933 badmatch)
931 934
932 935 unknownset = set(unknown + forgotten)
933 936 toprint = unknownset.copy()
934 937 toprint.update(deleted)
935 938 for abs in sorted(toprint):
936 939 if repo.ui.verbose or not m.exact(abs):
937 940 if abs in unknownset:
938 941 status = _('adding %s\n') % m.uipath(abs)
939 942 else:
940 943 status = _('removing %s\n') % m.uipath(abs)
941 944 repo.ui.status(status)
942 945
943 946 renames = _findrenames(repo, m, added + unknown, removed + deleted,
944 947 similarity)
945 948
946 949 if not dry_run:
947 950 _markchanges(repo, unknown + forgotten, deleted, renames)
948 951
949 952 for f in rejected:
950 953 if f in m.files():
951 954 return 1
952 955 return ret
953 956
954 957 def marktouched(repo, files, similarity=0.0):
955 958 '''Assert that files have somehow been operated upon. files are relative to
956 959 the repo root.'''
957 960 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
958 961 rejected = []
959 962
960 963 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
961 964
962 965 if repo.ui.verbose:
963 966 unknownset = set(unknown + forgotten)
964 967 toprint = unknownset.copy()
965 968 toprint.update(deleted)
966 969 for abs in sorted(toprint):
967 970 if abs in unknownset:
968 971 status = _('adding %s\n') % abs
969 972 else:
970 973 status = _('removing %s\n') % abs
971 974 repo.ui.status(status)
972 975
973 976 renames = _findrenames(repo, m, added + unknown, removed + deleted,
974 977 similarity)
975 978
976 979 _markchanges(repo, unknown + forgotten, deleted, renames)
977 980
978 981 for f in rejected:
979 982 if f in m.files():
980 983 return 1
981 984 return 0
982 985
983 986 def _interestingfiles(repo, matcher):
984 987 '''Walk dirstate with matcher, looking for files that addremove would care
985 988 about.
986 989
987 990 This is different from dirstate.status because it doesn't care about
988 991 whether files are modified or clean.'''
989 992 added, unknown, deleted, removed, forgotten = [], [], [], [], []
990 993 audit_path = pathutil.pathauditor(repo.root)
991 994
992 995 ctx = repo[None]
993 996 dirstate = repo.dirstate
994 997 walkresults = dirstate.walk(matcher, sorted(ctx.substate), True, False,
995 998 full=False)
996 999 for abs, st in walkresults.iteritems():
997 1000 dstate = dirstate[abs]
998 1001 if dstate == '?' and audit_path.check(abs):
999 1002 unknown.append(abs)
1000 1003 elif dstate != 'r' and not st:
1001 1004 deleted.append(abs)
1002 1005 elif dstate == 'r' and st:
1003 1006 forgotten.append(abs)
1004 1007 # for finding renames
1005 1008 elif dstate == 'r' and not st:
1006 1009 removed.append(abs)
1007 1010 elif dstate == 'a':
1008 1011 added.append(abs)
1009 1012
1010 1013 return added, unknown, deleted, removed, forgotten
1011 1014
1012 1015 def _findrenames(repo, matcher, added, removed, similarity):
1013 1016 '''Find renames from removed files to added ones.'''
1014 1017 renames = {}
1015 1018 if similarity > 0:
1016 1019 for old, new, score in similar.findrenames(repo, added, removed,
1017 1020 similarity):
1018 1021 if (repo.ui.verbose or not matcher.exact(old)
1019 1022 or not matcher.exact(new)):
1020 1023 repo.ui.status(_('recording removal of %s as rename to %s '
1021 1024 '(%d%% similar)\n') %
1022 1025 (matcher.rel(old), matcher.rel(new),
1023 1026 score * 100))
1024 1027 renames[new] = old
1025 1028 return renames
1026 1029
1027 1030 def _markchanges(repo, unknown, deleted, renames):
1028 1031 '''Marks the files in unknown as added, the files in deleted as removed,
1029 1032 and the files in renames as copied.'''
1030 1033 wctx = repo[None]
1031 1034 with repo.wlock():
1032 1035 wctx.forget(deleted)
1033 1036 wctx.add(unknown)
1034 1037 for new, old in renames.iteritems():
1035 1038 wctx.copy(old, new)
1036 1039
1037 1040 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
1038 1041 """Update the dirstate to reflect the intent of copying src to dst. For
1039 1042 different reasons it might not end with dst being marked as copied from src.
1040 1043 """
1041 1044 origsrc = repo.dirstate.copied(src) or src
1042 1045 if dst == origsrc: # copying back a copy?
1043 1046 if repo.dirstate[dst] not in 'mn' and not dryrun:
1044 1047 repo.dirstate.normallookup(dst)
1045 1048 else:
1046 1049 if repo.dirstate[origsrc] == 'a' and origsrc == src:
1047 1050 if not ui.quiet:
1048 1051 ui.warn(_("%s has not been committed yet, so no copy "
1049 1052 "data will be stored for %s.\n")
1050 1053 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
1051 1054 if repo.dirstate[dst] in '?r' and not dryrun:
1052 1055 wctx.add([dst])
1053 1056 elif not dryrun:
1054 1057 wctx.copy(origsrc, dst)
1055 1058
1056 1059 def readrequires(opener, supported):
1057 1060 '''Reads and parses .hg/requires and checks if all entries found
1058 1061 are in the list of supported features.'''
1059 1062 requirements = set(opener.read("requires").splitlines())
1060 1063 missings = []
1061 1064 for r in requirements:
1062 1065 if r not in supported:
1063 1066 if not r or not r[0].isalnum():
1064 1067 raise error.RequirementError(_(".hg/requires file is corrupt"))
1065 1068 missings.append(r)
1066 1069 missings.sort()
1067 1070 if missings:
1068 1071 raise error.RequirementError(
1069 1072 _("repository requires features unknown to this Mercurial: %s")
1070 1073 % " ".join(missings),
1071 1074 hint=_("see https://mercurial-scm.org/wiki/MissingRequirement"
1072 1075 " for more information"))
1073 1076 return requirements
1074 1077
1075 1078 def writerequires(opener, requirements):
1076 1079 with opener('requires', 'w') as fp:
1077 1080 for r in sorted(requirements):
1078 1081 fp.write("%s\n" % r)
1079 1082
1080 1083 class filecachesubentry(object):
1081 1084 def __init__(self, path, stat):
1082 1085 self.path = path
1083 1086 self.cachestat = None
1084 1087 self._cacheable = None
1085 1088
1086 1089 if stat:
1087 1090 self.cachestat = filecachesubentry.stat(self.path)
1088 1091
1089 1092 if self.cachestat:
1090 1093 self._cacheable = self.cachestat.cacheable()
1091 1094 else:
1092 1095 # None means we don't know yet
1093 1096 self._cacheable = None
1094 1097
1095 1098 def refresh(self):
1096 1099 if self.cacheable():
1097 1100 self.cachestat = filecachesubentry.stat(self.path)
1098 1101
1099 1102 def cacheable(self):
1100 1103 if self._cacheable is not None:
1101 1104 return self._cacheable
1102 1105
1103 1106 # we don't know yet, assume it is for now
1104 1107 return True
1105 1108
1106 1109 def changed(self):
1107 1110 # no point in going further if we can't cache it
1108 1111 if not self.cacheable():
1109 1112 return True
1110 1113
1111 1114 newstat = filecachesubentry.stat(self.path)
1112 1115
1113 1116 # we may not know if it's cacheable yet, check again now
1114 1117 if newstat and self._cacheable is None:
1115 1118 self._cacheable = newstat.cacheable()
1116 1119
1117 1120 # check again
1118 1121 if not self._cacheable:
1119 1122 return True
1120 1123
1121 1124 if self.cachestat != newstat:
1122 1125 self.cachestat = newstat
1123 1126 return True
1124 1127 else:
1125 1128 return False
1126 1129
1127 1130 @staticmethod
1128 1131 def stat(path):
1129 1132 try:
1130 1133 return util.cachestat(path)
1131 1134 except OSError as e:
1132 1135 if e.errno != errno.ENOENT:
1133 1136 raise
1134 1137
1135 1138 class filecacheentry(object):
1136 1139 def __init__(self, paths, stat=True):
1137 1140 self._entries = []
1138 1141 for path in paths:
1139 1142 self._entries.append(filecachesubentry(path, stat))
1140 1143
1141 1144 def changed(self):
1142 1145 '''true if any entry has changed'''
1143 1146 for entry in self._entries:
1144 1147 if entry.changed():
1145 1148 return True
1146 1149 return False
1147 1150
1148 1151 def refresh(self):
1149 1152 for entry in self._entries:
1150 1153 entry.refresh()
1151 1154
1152 1155 class filecache(object):
1153 1156 '''A property like decorator that tracks files under .hg/ for updates.
1154 1157
1155 1158 Records stat info when called in _filecache.
1156 1159
1157 1160 On subsequent calls, compares old stat info with new info, and recreates the
1158 1161 object when any of the files changes, updating the new stat info in
1159 1162 _filecache.
1160 1163
1161 1164 Mercurial either atomic renames or appends for files under .hg,
1162 1165 so to ensure the cache is reliable we need the filesystem to be able
1163 1166 to tell us if a file has been replaced. If it can't, we fallback to
1164 1167 recreating the object on every call (essentially the same behavior as
1165 1168 propertycache).
1166 1169
1167 1170 '''
1168 1171 def __init__(self, *paths):
1169 1172 self.paths = paths
1170 1173
1171 1174 def join(self, obj, fname):
1172 1175 """Used to compute the runtime path of a cached file.
1173 1176
1174 1177 Users should subclass filecache and provide their own version of this
1175 1178 function to call the appropriate join function on 'obj' (an instance
1176 1179 of the class that its member function was decorated).
1177 1180 """
1178 1181 return obj.join(fname)
1179 1182
1180 1183 def __call__(self, func):
1181 1184 self.func = func
1182 1185 self.name = func.__name__
1183 1186 return self
1184 1187
1185 1188 def __get__(self, obj, type=None):
1186 1189 # do we need to check if the file changed?
1187 1190 if self.name in obj.__dict__:
1188 1191 assert self.name in obj._filecache, self.name
1189 1192 return obj.__dict__[self.name]
1190 1193
1191 1194 entry = obj._filecache.get(self.name)
1192 1195
1193 1196 if entry:
1194 1197 if entry.changed():
1195 1198 entry.obj = self.func(obj)
1196 1199 else:
1197 1200 paths = [self.join(obj, path) for path in self.paths]
1198 1201
1199 1202 # We stat -before- creating the object so our cache doesn't lie if
1200 1203 # a writer modified between the time we read and stat
1201 1204 entry = filecacheentry(paths, True)
1202 1205 entry.obj = self.func(obj)
1203 1206
1204 1207 obj._filecache[self.name] = entry
1205 1208
1206 1209 obj.__dict__[self.name] = entry.obj
1207 1210 return entry.obj
1208 1211
1209 1212 def __set__(self, obj, value):
1210 1213 if self.name not in obj._filecache:
1211 1214 # we add an entry for the missing value because X in __dict__
1212 1215 # implies X in _filecache
1213 1216 paths = [self.join(obj, path) for path in self.paths]
1214 1217 ce = filecacheentry(paths, False)
1215 1218 obj._filecache[self.name] = ce
1216 1219 else:
1217 1220 ce = obj._filecache[self.name]
1218 1221
1219 1222 ce.obj = value # update cached copy
1220 1223 obj.__dict__[self.name] = value # update copy returned by obj.x
1221 1224
1222 1225 def __delete__(self, obj):
1223 1226 try:
1224 1227 del obj.__dict__[self.name]
1225 1228 except KeyError:
1226 1229 raise AttributeError(self.name)
1227 1230
1228 1231 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1229 1232 if lock is None:
1230 1233 raise error.LockInheritanceContractViolation(
1231 1234 'lock can only be inherited while held')
1232 1235 if environ is None:
1233 1236 environ = {}
1234 1237 with lock.inherit() as locker:
1235 1238 environ[envvar] = locker
1236 1239 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1237 1240
1238 1241 def wlocksub(repo, cmd, *args, **kwargs):
1239 1242 """run cmd as a subprocess that allows inheriting repo's wlock
1240 1243
1241 1244 This can only be called while the wlock is held. This takes all the
1242 1245 arguments that ui.system does, and returns the exit code of the
1243 1246 subprocess."""
1244 1247 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
1245 1248 **kwargs)
1246 1249
1247 1250 def gdinitconfig(ui):
1248 1251 """helper function to know if a repo should be created as general delta
1249 1252 """
1250 1253 # experimental config: format.generaldelta
1251 1254 return (ui.configbool('format', 'generaldelta', False)
1252 1255 or ui.configbool('format', 'usegeneraldelta', True))
1253 1256
1254 1257 def gddeltaconfig(ui):
1255 1258 """helper function to know if incoming delta should be optimised
1256 1259 """
1257 1260 # experimental config: format.generaldelta
1258 1261 return ui.configbool('format', 'generaldelta', False)
1259 1262
1260 1263 class delayclosedfile(object):
1261 1264 """Proxy for a file object whose close is delayed.
1262 1265
1263 1266 Do not instantiate outside of the vfs layer.
1264 1267 """
1265 1268
1266 1269 def __init__(self, fh, closer):
1267 1270 object.__setattr__(self, '_origfh', fh)
1268 1271 object.__setattr__(self, '_closer', closer)
1269 1272
1270 1273 def __getattr__(self, attr):
1271 1274 return getattr(self._origfh, attr)
1272 1275
1273 1276 def __setattr__(self, attr, value):
1274 1277 return setattr(self._origfh, attr, value)
1275 1278
1276 1279 def __delattr__(self, attr):
1277 1280 return delattr(self._origfh, attr)
1278 1281
1279 1282 def __enter__(self):
1280 1283 return self._origfh.__enter__()
1281 1284
1282 1285 def __exit__(self, exc_type, exc_value, exc_tb):
1283 1286 self._closer.close(self._origfh)
1284 1287
1285 1288 def close(self):
1286 1289 self._closer.close(self._origfh)
1287 1290
1288 1291 class backgroundfilecloser(object):
1289 1292 """Coordinates background closing of file handles on multiple threads."""
1290 1293 def __init__(self, ui, expectedcount=-1):
1291 1294 self._running = False
1292 1295 self._entered = False
1293 1296 self._threads = []
1294 1297 self._threadexception = None
1295 1298
1296 1299 # Only Windows/NTFS has slow file closing. So only enable by default
1297 1300 # on that platform. But allow to be enabled elsewhere for testing.
1298 1301 defaultenabled = os.name == 'nt'
1299 1302 enabled = ui.configbool('worker', 'backgroundclose', defaultenabled)
1300 1303
1301 1304 if not enabled:
1302 1305 return
1303 1306
1304 1307 # There is overhead to starting and stopping the background threads.
1305 1308 # Don't do background processing unless the file count is large enough
1306 1309 # to justify it.
1307 1310 minfilecount = ui.configint('worker', 'backgroundcloseminfilecount',
1308 1311 2048)
1309 1312 # FUTURE dynamically start background threads after minfilecount closes.
1310 1313 # (We don't currently have any callers that don't know their file count)
1311 1314 if expectedcount > 0 and expectedcount < minfilecount:
1312 1315 return
1313 1316
1314 1317 # Windows defaults to a limit of 512 open files. A buffer of 128
1315 1318 # should give us enough headway.
1316 1319 maxqueue = ui.configint('worker', 'backgroundclosemaxqueue', 384)
1317 1320 threadcount = ui.configint('worker', 'backgroundclosethreadcount', 4)
1318 1321
1319 1322 ui.debug('starting %d threads for background file closing\n' %
1320 1323 threadcount)
1321 1324
1322 1325 self._queue = util.queue(maxsize=maxqueue)
1323 1326 self._running = True
1324 1327
1325 1328 for i in range(threadcount):
1326 1329 t = threading.Thread(target=self._worker, name='backgroundcloser')
1327 1330 self._threads.append(t)
1328 1331 t.start()
1329 1332
1330 1333 def __enter__(self):
1331 1334 self._entered = True
1332 1335 return self
1333 1336
1334 1337 def __exit__(self, exc_type, exc_value, exc_tb):
1335 1338 self._running = False
1336 1339
1337 1340 # Wait for threads to finish closing so open files don't linger for
1338 1341 # longer than lifetime of context manager.
1339 1342 for t in self._threads:
1340 1343 t.join()
1341 1344
1342 1345 def _worker(self):
1343 1346 """Main routine for worker thread."""
1344 1347 while True:
1345 1348 try:
1346 1349 fh = self._queue.get(block=True, timeout=0.100)
1347 1350 # Need to catch or the thread will terminate and
1348 1351 # we could orphan file descriptors.
1349 1352 try:
1350 1353 fh.close()
1351 1354 except Exception as e:
1352 1355 # Stash so can re-raise from main thread later.
1353 1356 self._threadexception = e
1354 1357 except util.empty:
1355 1358 if not self._running:
1356 1359 break
1357 1360
1358 1361 def close(self, fh):
1359 1362 """Schedule a file for closing."""
1360 1363 if not self._entered:
1361 1364 raise error.Abort('can only call close() when context manager '
1362 1365 'active')
1363 1366
1364 1367 # If a background thread encountered an exception, raise now so we fail
1365 1368 # fast. Otherwise we may potentially go on for minutes until the error
1366 1369 # is acted on.
1367 1370 if self._threadexception:
1368 1371 e = self._threadexception
1369 1372 self._threadexception = None
1370 1373 raise e
1371 1374
1372 1375 # If we're not actively running, close synchronously.
1373 1376 if not self._running:
1374 1377 fh.close()
1375 1378 return
1376 1379
1377 1380 self._queue.put(fh, block=True, timeout=None)
1378 1381
General Comments 0
You need to be logged in to leave comments. Login now