##// END OF EJS Templates
filecache: unimplement __set__() and __delete__() (API)...
Yuya Nishihara -
r40775:7caf632e default
parent child Browse files
Show More
@@ -1,1504 +1,1504 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 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 collections
11 11 import contextlib
12 12 import errno
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .node import nullid
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 match as matchmod,
22 22 pathutil,
23 23 policy,
24 24 pycompat,
25 25 scmutil,
26 26 txnutil,
27 27 util,
28 28 )
29 29
30 30 parsers = policy.importmod(r'parsers')
31 31
32 32 propertycache = util.propertycache
33 33 filecache = scmutil.filecache
34 34 _rangemask = 0x7fffffff
35 35
36 36 dirstatetuple = parsers.dirstatetuple
37 37
38 38 class repocache(filecache):
39 39 """filecache for files in .hg/"""
40 40 def join(self, obj, fname):
41 41 return obj._opener.join(fname)
42 42
43 43 class rootcache(filecache):
44 44 """filecache for files in the repository root"""
45 45 def join(self, obj, fname):
46 46 return obj._join(fname)
47 47
48 48 def _getfsnow(vfs):
49 49 '''Get "now" timestamp on filesystem'''
50 50 tmpfd, tmpname = vfs.mkstemp()
51 51 try:
52 52 return os.fstat(tmpfd)[stat.ST_MTIME]
53 53 finally:
54 54 os.close(tmpfd)
55 55 vfs.unlink(tmpname)
56 56
57 57 class dirstate(object):
58 58
59 59 def __init__(self, opener, ui, root, validate, sparsematchfn):
60 60 '''Create a new dirstate object.
61 61
62 62 opener is an open()-like callable that can be used to open the
63 63 dirstate file; root is the root of the directory tracked by
64 64 the dirstate.
65 65 '''
66 66 self._opener = opener
67 67 self._validate = validate
68 68 self._root = root
69 69 self._sparsematchfn = sparsematchfn
70 70 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
71 71 # UNC path pointing to root share (issue4557)
72 72 self._rootdir = pathutil.normasprefix(root)
73 73 self._dirty = False
74 74 self._lastnormaltime = 0
75 75 self._ui = ui
76 76 self._filecache = {}
77 77 self._parentwriters = 0
78 78 self._filename = 'dirstate'
79 79 self._pendingfilename = '%s.pending' % self._filename
80 80 self._plchangecallbacks = {}
81 81 self._origpl = None
82 82 self._updatedfiles = set()
83 83 self._mapcls = dirstatemap
84 84
85 85 @contextlib.contextmanager
86 86 def parentchange(self):
87 87 '''Context manager for handling dirstate parents.
88 88
89 89 If an exception occurs in the scope of the context manager,
90 90 the incoherent dirstate won't be written when wlock is
91 91 released.
92 92 '''
93 93 self._parentwriters += 1
94 94 yield
95 95 # Typically we want the "undo" step of a context manager in a
96 96 # finally block so it happens even when an exception
97 97 # occurs. In this case, however, we only want to decrement
98 98 # parentwriters if the code in the with statement exits
99 99 # normally, so we don't have a try/finally here on purpose.
100 100 self._parentwriters -= 1
101 101
102 102 def pendingparentchange(self):
103 103 '''Returns true if the dirstate is in the middle of a set of changes
104 104 that modify the dirstate parent.
105 105 '''
106 106 return self._parentwriters > 0
107 107
108 108 @propertycache
109 109 def _map(self):
110 110 """Return the dirstate contents (see documentation for dirstatemap)."""
111 111 self._map = self._mapcls(self._ui, self._opener, self._root)
112 112 return self._map
113 113
114 114 @property
115 115 def _sparsematcher(self):
116 116 """The matcher for the sparse checkout.
117 117
118 118 The working directory may not include every file from a manifest. The
119 119 matcher obtained by this property will match a path if it is to be
120 120 included in the working directory.
121 121 """
122 122 # TODO there is potential to cache this property. For now, the matcher
123 123 # is resolved on every access. (But the called function does use a
124 124 # cache to keep the lookup fast.)
125 125 return self._sparsematchfn()
126 126
127 127 @repocache('branch')
128 128 def _branch(self):
129 129 try:
130 130 return self._opener.read("branch").strip() or "default"
131 131 except IOError as inst:
132 132 if inst.errno != errno.ENOENT:
133 133 raise
134 134 return "default"
135 135
136 136 @property
137 137 def _pl(self):
138 138 return self._map.parents()
139 139
140 140 def hasdir(self, d):
141 141 return self._map.hastrackeddir(d)
142 142
143 143 @rootcache('.hgignore')
144 144 def _ignore(self):
145 145 files = self._ignorefiles()
146 146 if not files:
147 147 return matchmod.never(self._root, '')
148 148
149 149 pats = ['include:%s' % f for f in files]
150 150 return matchmod.match(self._root, '', [], pats, warn=self._ui.warn)
151 151
152 152 @propertycache
153 153 def _slash(self):
154 154 return self._ui.configbool('ui', 'slash') and pycompat.ossep != '/'
155 155
156 156 @propertycache
157 157 def _checklink(self):
158 158 return util.checklink(self._root)
159 159
160 160 @propertycache
161 161 def _checkexec(self):
162 162 return util.checkexec(self._root)
163 163
164 164 @propertycache
165 165 def _checkcase(self):
166 166 return not util.fscasesensitive(self._join('.hg'))
167 167
168 168 def _join(self, f):
169 169 # much faster than os.path.join()
170 170 # it's safe because f is always a relative path
171 171 return self._rootdir + f
172 172
173 173 def flagfunc(self, buildfallback):
174 174 if self._checklink and self._checkexec:
175 175 def f(x):
176 176 try:
177 177 st = os.lstat(self._join(x))
178 178 if util.statislink(st):
179 179 return 'l'
180 180 if util.statisexec(st):
181 181 return 'x'
182 182 except OSError:
183 183 pass
184 184 return ''
185 185 return f
186 186
187 187 fallback = buildfallback()
188 188 if self._checklink:
189 189 def f(x):
190 190 if os.path.islink(self._join(x)):
191 191 return 'l'
192 192 if 'x' in fallback(x):
193 193 return 'x'
194 194 return ''
195 195 return f
196 196 if self._checkexec:
197 197 def f(x):
198 198 if 'l' in fallback(x):
199 199 return 'l'
200 200 if util.isexec(self._join(x)):
201 201 return 'x'
202 202 return ''
203 203 return f
204 204 else:
205 205 return fallback
206 206
207 207 @propertycache
208 208 def _cwd(self):
209 209 # internal config: ui.forcecwd
210 210 forcecwd = self._ui.config('ui', 'forcecwd')
211 211 if forcecwd:
212 212 return forcecwd
213 213 return encoding.getcwd()
214 214
215 215 def getcwd(self):
216 216 '''Return the path from which a canonical path is calculated.
217 217
218 218 This path should be used to resolve file patterns or to convert
219 219 canonical paths back to file paths for display. It shouldn't be
220 220 used to get real file paths. Use vfs functions instead.
221 221 '''
222 222 cwd = self._cwd
223 223 if cwd == self._root:
224 224 return ''
225 225 # self._root ends with a path separator if self._root is '/' or 'C:\'
226 226 rootsep = self._root
227 227 if not util.endswithsep(rootsep):
228 228 rootsep += pycompat.ossep
229 229 if cwd.startswith(rootsep):
230 230 return cwd[len(rootsep):]
231 231 else:
232 232 # we're outside the repo. return an absolute path.
233 233 return cwd
234 234
235 235 def pathto(self, f, cwd=None):
236 236 if cwd is None:
237 237 cwd = self.getcwd()
238 238 path = util.pathto(self._root, cwd, f)
239 239 if self._slash:
240 240 return util.pconvert(path)
241 241 return path
242 242
243 243 def __getitem__(self, key):
244 244 '''Return the current state of key (a filename) in the dirstate.
245 245
246 246 States are:
247 247 n normal
248 248 m needs merging
249 249 r marked for removal
250 250 a marked for addition
251 251 ? not tracked
252 252 '''
253 253 return self._map.get(key, ("?",))[0]
254 254
255 255 def __contains__(self, key):
256 256 return key in self._map
257 257
258 258 def __iter__(self):
259 259 return iter(sorted(self._map))
260 260
261 261 def items(self):
262 262 return self._map.iteritems()
263 263
264 264 iteritems = items
265 265
266 266 def parents(self):
267 267 return [self._validate(p) for p in self._pl]
268 268
269 269 def p1(self):
270 270 return self._validate(self._pl[0])
271 271
272 272 def p2(self):
273 273 return self._validate(self._pl[1])
274 274
275 275 def branch(self):
276 276 return encoding.tolocal(self._branch)
277 277
278 278 def setparents(self, p1, p2=nullid):
279 279 """Set dirstate parents to p1 and p2.
280 280
281 281 When moving from two parents to one, 'm' merged entries a
282 282 adjusted to normal and previous copy records discarded and
283 283 returned by the call.
284 284
285 285 See localrepo.setparents()
286 286 """
287 287 if self._parentwriters == 0:
288 288 raise ValueError("cannot set dirstate parent without "
289 289 "calling dirstate.beginparentchange")
290 290
291 291 self._dirty = True
292 292 oldp2 = self._pl[1]
293 293 if self._origpl is None:
294 294 self._origpl = self._pl
295 295 self._map.setparents(p1, p2)
296 296 copies = {}
297 297 if oldp2 != nullid and p2 == nullid:
298 298 candidatefiles = self._map.nonnormalset.union(
299 299 self._map.otherparentset)
300 300 for f in candidatefiles:
301 301 s = self._map.get(f)
302 302 if s is None:
303 303 continue
304 304
305 305 # Discard 'm' markers when moving away from a merge state
306 306 if s[0] == 'm':
307 307 source = self._map.copymap.get(f)
308 308 if source:
309 309 copies[f] = source
310 310 self.normallookup(f)
311 311 # Also fix up otherparent markers
312 312 elif s[0] == 'n' and s[2] == -2:
313 313 source = self._map.copymap.get(f)
314 314 if source:
315 315 copies[f] = source
316 316 self.add(f)
317 317 return copies
318 318
319 319 def setbranch(self, branch):
320 self._branch = encoding.fromlocal(branch)
320 self.__class__._branch.set(self, encoding.fromlocal(branch))
321 321 f = self._opener('branch', 'w', atomictemp=True, checkambig=True)
322 322 try:
323 323 f.write(self._branch + '\n')
324 324 f.close()
325 325
326 326 # make sure filecache has the correct stat info for _branch after
327 327 # replacing the underlying file
328 328 ce = self._filecache['_branch']
329 329 if ce:
330 330 ce.refresh()
331 331 except: # re-raises
332 332 f.discard()
333 333 raise
334 334
335 335 def invalidate(self):
336 336 '''Causes the next access to reread the dirstate.
337 337
338 338 This is different from localrepo.invalidatedirstate() because it always
339 339 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
340 340 check whether the dirstate has changed before rereading it.'''
341 341
342 342 for a in (r"_map", r"_branch", r"_ignore"):
343 343 if a in self.__dict__:
344 344 delattr(self, a)
345 345 self._lastnormaltime = 0
346 346 self._dirty = False
347 347 self._updatedfiles.clear()
348 348 self._parentwriters = 0
349 349 self._origpl = None
350 350
351 351 def copy(self, source, dest):
352 352 """Mark dest as a copy of source. Unmark dest if source is None."""
353 353 if source == dest:
354 354 return
355 355 self._dirty = True
356 356 if source is not None:
357 357 self._map.copymap[dest] = source
358 358 self._updatedfiles.add(source)
359 359 self._updatedfiles.add(dest)
360 360 elif self._map.copymap.pop(dest, None):
361 361 self._updatedfiles.add(dest)
362 362
363 363 def copied(self, file):
364 364 return self._map.copymap.get(file, None)
365 365
366 366 def copies(self):
367 367 return self._map.copymap
368 368
369 369 def _addpath(self, f, state, mode, size, mtime):
370 370 oldstate = self[f]
371 371 if state == 'a' or oldstate == 'r':
372 372 scmutil.checkfilename(f)
373 373 if self._map.hastrackeddir(f):
374 374 raise error.Abort(_('directory %r already in dirstate') %
375 375 pycompat.bytestr(f))
376 376 # shadows
377 377 for d in util.finddirs(f):
378 378 if self._map.hastrackeddir(d):
379 379 break
380 380 entry = self._map.get(d)
381 381 if entry is not None and entry[0] != 'r':
382 382 raise error.Abort(
383 383 _('file %r in dirstate clashes with %r') %
384 384 (pycompat.bytestr(d), pycompat.bytestr(f)))
385 385 self._dirty = True
386 386 self._updatedfiles.add(f)
387 387 self._map.addfile(f, oldstate, state, mode, size, mtime)
388 388
389 389 def normal(self, f):
390 390 '''Mark a file normal and clean.'''
391 391 s = os.lstat(self._join(f))
392 392 mtime = s[stat.ST_MTIME]
393 393 self._addpath(f, 'n', s.st_mode,
394 394 s.st_size & _rangemask, mtime & _rangemask)
395 395 self._map.copymap.pop(f, None)
396 396 if f in self._map.nonnormalset:
397 397 self._map.nonnormalset.remove(f)
398 398 if mtime > self._lastnormaltime:
399 399 # Remember the most recent modification timeslot for status(),
400 400 # to make sure we won't miss future size-preserving file content
401 401 # modifications that happen within the same timeslot.
402 402 self._lastnormaltime = mtime
403 403
404 404 def normallookup(self, f):
405 405 '''Mark a file normal, but possibly dirty.'''
406 406 if self._pl[1] != nullid:
407 407 # if there is a merge going on and the file was either
408 408 # in state 'm' (-1) or coming from other parent (-2) before
409 409 # being removed, restore that state.
410 410 entry = self._map.get(f)
411 411 if entry is not None:
412 412 if entry[0] == 'r' and entry[2] in (-1, -2):
413 413 source = self._map.copymap.get(f)
414 414 if entry[2] == -1:
415 415 self.merge(f)
416 416 elif entry[2] == -2:
417 417 self.otherparent(f)
418 418 if source:
419 419 self.copy(source, f)
420 420 return
421 421 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
422 422 return
423 423 self._addpath(f, 'n', 0, -1, -1)
424 424 self._map.copymap.pop(f, None)
425 425
426 426 def otherparent(self, f):
427 427 '''Mark as coming from the other parent, always dirty.'''
428 428 if self._pl[1] == nullid:
429 429 raise error.Abort(_("setting %r to other parent "
430 430 "only allowed in merges") % f)
431 431 if f in self and self[f] == 'n':
432 432 # merge-like
433 433 self._addpath(f, 'm', 0, -2, -1)
434 434 else:
435 435 # add-like
436 436 self._addpath(f, 'n', 0, -2, -1)
437 437 self._map.copymap.pop(f, None)
438 438
439 439 def add(self, f):
440 440 '''Mark a file added.'''
441 441 self._addpath(f, 'a', 0, -1, -1)
442 442 self._map.copymap.pop(f, None)
443 443
444 444 def remove(self, f):
445 445 '''Mark a file removed.'''
446 446 self._dirty = True
447 447 oldstate = self[f]
448 448 size = 0
449 449 if self._pl[1] != nullid:
450 450 entry = self._map.get(f)
451 451 if entry is not None:
452 452 # backup the previous state
453 453 if entry[0] == 'm': # merge
454 454 size = -1
455 455 elif entry[0] == 'n' and entry[2] == -2: # other parent
456 456 size = -2
457 457 self._map.otherparentset.add(f)
458 458 self._updatedfiles.add(f)
459 459 self._map.removefile(f, oldstate, size)
460 460 if size == 0:
461 461 self._map.copymap.pop(f, None)
462 462
463 463 def merge(self, f):
464 464 '''Mark a file merged.'''
465 465 if self._pl[1] == nullid:
466 466 return self.normallookup(f)
467 467 return self.otherparent(f)
468 468
469 469 def drop(self, f):
470 470 '''Drop a file from the dirstate'''
471 471 oldstate = self[f]
472 472 if self._map.dropfile(f, oldstate):
473 473 self._dirty = True
474 474 self._updatedfiles.add(f)
475 475 self._map.copymap.pop(f, None)
476 476
477 477 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
478 478 if exists is None:
479 479 exists = os.path.lexists(os.path.join(self._root, path))
480 480 if not exists:
481 481 # Maybe a path component exists
482 482 if not ignoremissing and '/' in path:
483 483 d, f = path.rsplit('/', 1)
484 484 d = self._normalize(d, False, ignoremissing, None)
485 485 folded = d + "/" + f
486 486 else:
487 487 # No path components, preserve original case
488 488 folded = path
489 489 else:
490 490 # recursively normalize leading directory components
491 491 # against dirstate
492 492 if '/' in normed:
493 493 d, f = normed.rsplit('/', 1)
494 494 d = self._normalize(d, False, ignoremissing, True)
495 495 r = self._root + "/" + d
496 496 folded = d + "/" + util.fspath(f, r)
497 497 else:
498 498 folded = util.fspath(normed, self._root)
499 499 storemap[normed] = folded
500 500
501 501 return folded
502 502
503 503 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
504 504 normed = util.normcase(path)
505 505 folded = self._map.filefoldmap.get(normed, None)
506 506 if folded is None:
507 507 if isknown:
508 508 folded = path
509 509 else:
510 510 folded = self._discoverpath(path, normed, ignoremissing, exists,
511 511 self._map.filefoldmap)
512 512 return folded
513 513
514 514 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
515 515 normed = util.normcase(path)
516 516 folded = self._map.filefoldmap.get(normed, None)
517 517 if folded is None:
518 518 folded = self._map.dirfoldmap.get(normed, None)
519 519 if folded is None:
520 520 if isknown:
521 521 folded = path
522 522 else:
523 523 # store discovered result in dirfoldmap so that future
524 524 # normalizefile calls don't start matching directories
525 525 folded = self._discoverpath(path, normed, ignoremissing, exists,
526 526 self._map.dirfoldmap)
527 527 return folded
528 528
529 529 def normalize(self, path, isknown=False, ignoremissing=False):
530 530 '''
531 531 normalize the case of a pathname when on a casefolding filesystem
532 532
533 533 isknown specifies whether the filename came from walking the
534 534 disk, to avoid extra filesystem access.
535 535
536 536 If ignoremissing is True, missing path are returned
537 537 unchanged. Otherwise, we try harder to normalize possibly
538 538 existing path components.
539 539
540 540 The normalized case is determined based on the following precedence:
541 541
542 542 - version of name already stored in the dirstate
543 543 - version of name stored on disk
544 544 - version provided via command arguments
545 545 '''
546 546
547 547 if self._checkcase:
548 548 return self._normalize(path, isknown, ignoremissing)
549 549 return path
550 550
551 551 def clear(self):
552 552 self._map.clear()
553 553 self._lastnormaltime = 0
554 554 self._updatedfiles.clear()
555 555 self._dirty = True
556 556
557 557 def rebuild(self, parent, allfiles, changedfiles=None):
558 558 if changedfiles is None:
559 559 # Rebuild entire dirstate
560 560 changedfiles = allfiles
561 561 lastnormaltime = self._lastnormaltime
562 562 self.clear()
563 563 self._lastnormaltime = lastnormaltime
564 564
565 565 if self._origpl is None:
566 566 self._origpl = self._pl
567 567 self._map.setparents(parent, nullid)
568 568 for f in changedfiles:
569 569 if f in allfiles:
570 570 self.normallookup(f)
571 571 else:
572 572 self.drop(f)
573 573
574 574 self._dirty = True
575 575
576 576 def identity(self):
577 577 '''Return identity of dirstate itself to detect changing in storage
578 578
579 579 If identity of previous dirstate is equal to this, writing
580 580 changes based on the former dirstate out can keep consistency.
581 581 '''
582 582 return self._map.identity
583 583
584 584 def write(self, tr):
585 585 if not self._dirty:
586 586 return
587 587
588 588 filename = self._filename
589 589 if tr:
590 590 # 'dirstate.write()' is not only for writing in-memory
591 591 # changes out, but also for dropping ambiguous timestamp.
592 592 # delayed writing re-raise "ambiguous timestamp issue".
593 593 # See also the wiki page below for detail:
594 594 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
595 595
596 596 # emulate dropping timestamp in 'parsers.pack_dirstate'
597 597 now = _getfsnow(self._opener)
598 598 self._map.clearambiguoustimes(self._updatedfiles, now)
599 599
600 600 # emulate that all 'dirstate.normal' results are written out
601 601 self._lastnormaltime = 0
602 602 self._updatedfiles.clear()
603 603
604 604 # delay writing in-memory changes out
605 605 tr.addfilegenerator('dirstate', (self._filename,),
606 606 self._writedirstate, location='plain')
607 607 return
608 608
609 609 st = self._opener(filename, "w", atomictemp=True, checkambig=True)
610 610 self._writedirstate(st)
611 611
612 612 def addparentchangecallback(self, category, callback):
613 613 """add a callback to be called when the wd parents are changed
614 614
615 615 Callback will be called with the following arguments:
616 616 dirstate, (oldp1, oldp2), (newp1, newp2)
617 617
618 618 Category is a unique identifier to allow overwriting an old callback
619 619 with a newer callback.
620 620 """
621 621 self._plchangecallbacks[category] = callback
622 622
623 623 def _writedirstate(self, st):
624 624 # notify callbacks about parents change
625 625 if self._origpl is not None and self._origpl != self._pl:
626 626 for c, callback in sorted(self._plchangecallbacks.iteritems()):
627 627 callback(self, self._origpl, self._pl)
628 628 self._origpl = None
629 629 # use the modification time of the newly created temporary file as the
630 630 # filesystem's notion of 'now'
631 631 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
632 632
633 633 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
634 634 # timestamp of each entries in dirstate, because of 'now > mtime'
635 635 delaywrite = self._ui.configint('debug', 'dirstate.delaywrite')
636 636 if delaywrite > 0:
637 637 # do we have any files to delay for?
638 638 for f, e in self._map.iteritems():
639 639 if e[0] == 'n' and e[3] == now:
640 640 import time # to avoid useless import
641 641 # rather than sleep n seconds, sleep until the next
642 642 # multiple of n seconds
643 643 clock = time.time()
644 644 start = int(clock) - (int(clock) % delaywrite)
645 645 end = start + delaywrite
646 646 time.sleep(end - clock)
647 647 now = end # trust our estimate that the end is near now
648 648 break
649 649
650 650 self._map.write(st, now)
651 651 self._lastnormaltime = 0
652 652 self._dirty = False
653 653
654 654 def _dirignore(self, f):
655 655 if f == '.':
656 656 return False
657 657 if self._ignore(f):
658 658 return True
659 659 for p in util.finddirs(f):
660 660 if self._ignore(p):
661 661 return True
662 662 return False
663 663
664 664 def _ignorefiles(self):
665 665 files = []
666 666 if os.path.exists(self._join('.hgignore')):
667 667 files.append(self._join('.hgignore'))
668 668 for name, path in self._ui.configitems("ui"):
669 669 if name == 'ignore' or name.startswith('ignore.'):
670 670 # we need to use os.path.join here rather than self._join
671 671 # because path is arbitrary and user-specified
672 672 files.append(os.path.join(self._rootdir, util.expandpath(path)))
673 673 return files
674 674
675 675 def _ignorefileandline(self, f):
676 676 files = collections.deque(self._ignorefiles())
677 677 visited = set()
678 678 while files:
679 679 i = files.popleft()
680 680 patterns = matchmod.readpatternfile(i, self._ui.warn,
681 681 sourceinfo=True)
682 682 for pattern, lineno, line in patterns:
683 683 kind, p = matchmod._patsplit(pattern, 'glob')
684 684 if kind == "subinclude":
685 685 if p not in visited:
686 686 files.append(p)
687 687 continue
688 688 m = matchmod.match(self._root, '', [], [pattern],
689 689 warn=self._ui.warn)
690 690 if m(f):
691 691 return (i, lineno, line)
692 692 visited.add(i)
693 693 return (None, -1, "")
694 694
695 695 def _walkexplicit(self, match, subrepos):
696 696 '''Get stat data about the files explicitly specified by match.
697 697
698 698 Return a triple (results, dirsfound, dirsnotfound).
699 699 - results is a mapping from filename to stat result. It also contains
700 700 listings mapping subrepos and .hg to None.
701 701 - dirsfound is a list of files found to be directories.
702 702 - dirsnotfound is a list of files that the dirstate thinks are
703 703 directories and that were not found.'''
704 704
705 705 def badtype(mode):
706 706 kind = _('unknown')
707 707 if stat.S_ISCHR(mode):
708 708 kind = _('character device')
709 709 elif stat.S_ISBLK(mode):
710 710 kind = _('block device')
711 711 elif stat.S_ISFIFO(mode):
712 712 kind = _('fifo')
713 713 elif stat.S_ISSOCK(mode):
714 714 kind = _('socket')
715 715 elif stat.S_ISDIR(mode):
716 716 kind = _('directory')
717 717 return _('unsupported file type (type is %s)') % kind
718 718
719 719 matchedir = match.explicitdir
720 720 badfn = match.bad
721 721 dmap = self._map
722 722 lstat = os.lstat
723 723 getkind = stat.S_IFMT
724 724 dirkind = stat.S_IFDIR
725 725 regkind = stat.S_IFREG
726 726 lnkkind = stat.S_IFLNK
727 727 join = self._join
728 728 dirsfound = []
729 729 foundadd = dirsfound.append
730 730 dirsnotfound = []
731 731 notfoundadd = dirsnotfound.append
732 732
733 733 if not match.isexact() and self._checkcase:
734 734 normalize = self._normalize
735 735 else:
736 736 normalize = None
737 737
738 738 files = sorted(match.files())
739 739 subrepos.sort()
740 740 i, j = 0, 0
741 741 while i < len(files) and j < len(subrepos):
742 742 subpath = subrepos[j] + "/"
743 743 if files[i] < subpath:
744 744 i += 1
745 745 continue
746 746 while i < len(files) and files[i].startswith(subpath):
747 747 del files[i]
748 748 j += 1
749 749
750 750 if not files or '.' in files:
751 751 files = ['.']
752 752 results = dict.fromkeys(subrepos)
753 753 results['.hg'] = None
754 754
755 755 for ff in files:
756 756 # constructing the foldmap is expensive, so don't do it for the
757 757 # common case where files is ['.']
758 758 if normalize and ff != '.':
759 759 nf = normalize(ff, False, True)
760 760 else:
761 761 nf = ff
762 762 if nf in results:
763 763 continue
764 764
765 765 try:
766 766 st = lstat(join(nf))
767 767 kind = getkind(st.st_mode)
768 768 if kind == dirkind:
769 769 if nf in dmap:
770 770 # file replaced by dir on disk but still in dirstate
771 771 results[nf] = None
772 772 if matchedir:
773 773 matchedir(nf)
774 774 foundadd((nf, ff))
775 775 elif kind == regkind or kind == lnkkind:
776 776 results[nf] = st
777 777 else:
778 778 badfn(ff, badtype(kind))
779 779 if nf in dmap:
780 780 results[nf] = None
781 781 except OSError as inst: # nf not found on disk - it is dirstate only
782 782 if nf in dmap: # does it exactly match a missing file?
783 783 results[nf] = None
784 784 else: # does it match a missing directory?
785 785 if self._map.hasdir(nf):
786 786 if matchedir:
787 787 matchedir(nf)
788 788 notfoundadd(nf)
789 789 else:
790 790 badfn(ff, encoding.strtolocal(inst.strerror))
791 791
792 792 # match.files() may contain explicitly-specified paths that shouldn't
793 793 # be taken; drop them from the list of files found. dirsfound/notfound
794 794 # aren't filtered here because they will be tested later.
795 795 if match.anypats():
796 796 for f in list(results):
797 797 if f == '.hg' or f in subrepos:
798 798 # keep sentinel to disable further out-of-repo walks
799 799 continue
800 800 if not match(f):
801 801 del results[f]
802 802
803 803 # Case insensitive filesystems cannot rely on lstat() failing to detect
804 804 # a case-only rename. Prune the stat object for any file that does not
805 805 # match the case in the filesystem, if there are multiple files that
806 806 # normalize to the same path.
807 807 if match.isexact() and self._checkcase:
808 808 normed = {}
809 809
810 810 for f, st in results.iteritems():
811 811 if st is None:
812 812 continue
813 813
814 814 nc = util.normcase(f)
815 815 paths = normed.get(nc)
816 816
817 817 if paths is None:
818 818 paths = set()
819 819 normed[nc] = paths
820 820
821 821 paths.add(f)
822 822
823 823 for norm, paths in normed.iteritems():
824 824 if len(paths) > 1:
825 825 for path in paths:
826 826 folded = self._discoverpath(path, norm, True, None,
827 827 self._map.dirfoldmap)
828 828 if path != folded:
829 829 results[path] = None
830 830
831 831 return results, dirsfound, dirsnotfound
832 832
833 833 def walk(self, match, subrepos, unknown, ignored, full=True):
834 834 '''
835 835 Walk recursively through the directory tree, finding all files
836 836 matched by match.
837 837
838 838 If full is False, maybe skip some known-clean files.
839 839
840 840 Return a dict mapping filename to stat-like object (either
841 841 mercurial.osutil.stat instance or return value of os.stat()).
842 842
843 843 '''
844 844 # full is a flag that extensions that hook into walk can use -- this
845 845 # implementation doesn't use it at all. This satisfies the contract
846 846 # because we only guarantee a "maybe".
847 847
848 848 if ignored:
849 849 ignore = util.never
850 850 dirignore = util.never
851 851 elif unknown:
852 852 ignore = self._ignore
853 853 dirignore = self._dirignore
854 854 else:
855 855 # if not unknown and not ignored, drop dir recursion and step 2
856 856 ignore = util.always
857 857 dirignore = util.always
858 858
859 859 matchfn = match.matchfn
860 860 matchalways = match.always()
861 861 matchtdir = match.traversedir
862 862 dmap = self._map
863 863 listdir = util.listdir
864 864 lstat = os.lstat
865 865 dirkind = stat.S_IFDIR
866 866 regkind = stat.S_IFREG
867 867 lnkkind = stat.S_IFLNK
868 868 join = self._join
869 869
870 870 exact = skipstep3 = False
871 871 if match.isexact(): # match.exact
872 872 exact = True
873 873 dirignore = util.always # skip step 2
874 874 elif match.prefix(): # match.match, no patterns
875 875 skipstep3 = True
876 876
877 877 if not exact and self._checkcase:
878 878 normalize = self._normalize
879 879 normalizefile = self._normalizefile
880 880 skipstep3 = False
881 881 else:
882 882 normalize = self._normalize
883 883 normalizefile = None
884 884
885 885 # step 1: find all explicit files
886 886 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
887 887
888 888 skipstep3 = skipstep3 and not (work or dirsnotfound)
889 889 work = [d for d in work if not dirignore(d[0])]
890 890
891 891 # step 2: visit subdirectories
892 892 def traverse(work, alreadynormed):
893 893 wadd = work.append
894 894 while work:
895 895 nd = work.pop()
896 896 visitentries = match.visitchildrenset(nd)
897 897 if not visitentries:
898 898 continue
899 899 if visitentries == 'this' or visitentries == 'all':
900 900 visitentries = None
901 901 skip = None
902 902 if nd == '.':
903 903 nd = ''
904 904 else:
905 905 skip = '.hg'
906 906 try:
907 907 entries = listdir(join(nd), stat=True, skip=skip)
908 908 except OSError as inst:
909 909 if inst.errno in (errno.EACCES, errno.ENOENT):
910 910 match.bad(self.pathto(nd),
911 911 encoding.strtolocal(inst.strerror))
912 912 continue
913 913 raise
914 914 for f, kind, st in entries:
915 915 # Some matchers may return files in the visitentries set,
916 916 # instead of 'this', if the matcher explicitly mentions them
917 917 # and is not an exactmatcher. This is acceptable; we do not
918 918 # make any hard assumptions about file-or-directory below
919 919 # based on the presence of `f` in visitentries. If
920 920 # visitchildrenset returned a set, we can always skip the
921 921 # entries *not* in the set it provided regardless of whether
922 922 # they're actually a file or a directory.
923 923 if visitentries and f not in visitentries:
924 924 continue
925 925 if normalizefile:
926 926 # even though f might be a directory, we're only
927 927 # interested in comparing it to files currently in the
928 928 # dmap -- therefore normalizefile is enough
929 929 nf = normalizefile(nd and (nd + "/" + f) or f, True,
930 930 True)
931 931 else:
932 932 nf = nd and (nd + "/" + f) or f
933 933 if nf not in results:
934 934 if kind == dirkind:
935 935 if not ignore(nf):
936 936 if matchtdir:
937 937 matchtdir(nf)
938 938 wadd(nf)
939 939 if nf in dmap and (matchalways or matchfn(nf)):
940 940 results[nf] = None
941 941 elif kind == regkind or kind == lnkkind:
942 942 if nf in dmap:
943 943 if matchalways or matchfn(nf):
944 944 results[nf] = st
945 945 elif ((matchalways or matchfn(nf))
946 946 and not ignore(nf)):
947 947 # unknown file -- normalize if necessary
948 948 if not alreadynormed:
949 949 nf = normalize(nf, False, True)
950 950 results[nf] = st
951 951 elif nf in dmap and (matchalways or matchfn(nf)):
952 952 results[nf] = None
953 953
954 954 for nd, d in work:
955 955 # alreadynormed means that processwork doesn't have to do any
956 956 # expensive directory normalization
957 957 alreadynormed = not normalize or nd == d
958 958 traverse([d], alreadynormed)
959 959
960 960 for s in subrepos:
961 961 del results[s]
962 962 del results['.hg']
963 963
964 964 # step 3: visit remaining files from dmap
965 965 if not skipstep3 and not exact:
966 966 # If a dmap file is not in results yet, it was either
967 967 # a) not matching matchfn b) ignored, c) missing, or d) under a
968 968 # symlink directory.
969 969 if not results and matchalways:
970 970 visit = [f for f in dmap]
971 971 else:
972 972 visit = [f for f in dmap if f not in results and matchfn(f)]
973 973 visit.sort()
974 974
975 975 if unknown:
976 976 # unknown == True means we walked all dirs under the roots
977 977 # that wasn't ignored, and everything that matched was stat'ed
978 978 # and is already in results.
979 979 # The rest must thus be ignored or under a symlink.
980 980 audit_path = pathutil.pathauditor(self._root, cached=True)
981 981
982 982 for nf in iter(visit):
983 983 # If a stat for the same file was already added with a
984 984 # different case, don't add one for this, since that would
985 985 # make it appear as if the file exists under both names
986 986 # on disk.
987 987 if (normalizefile and
988 988 normalizefile(nf, True, True) in results):
989 989 results[nf] = None
990 990 # Report ignored items in the dmap as long as they are not
991 991 # under a symlink directory.
992 992 elif audit_path.check(nf):
993 993 try:
994 994 results[nf] = lstat(join(nf))
995 995 # file was just ignored, no links, and exists
996 996 except OSError:
997 997 # file doesn't exist
998 998 results[nf] = None
999 999 else:
1000 1000 # It's either missing or under a symlink directory
1001 1001 # which we in this case report as missing
1002 1002 results[nf] = None
1003 1003 else:
1004 1004 # We may not have walked the full directory tree above,
1005 1005 # so stat and check everything we missed.
1006 1006 iv = iter(visit)
1007 1007 for st in util.statfiles([join(i) for i in visit]):
1008 1008 results[next(iv)] = st
1009 1009 return results
1010 1010
1011 1011 def status(self, match, subrepos, ignored, clean, unknown):
1012 1012 '''Determine the status of the working copy relative to the
1013 1013 dirstate and return a pair of (unsure, status), where status is of type
1014 1014 scmutil.status and:
1015 1015
1016 1016 unsure:
1017 1017 files that might have been modified since the dirstate was
1018 1018 written, but need to be read to be sure (size is the same
1019 1019 but mtime differs)
1020 1020 status.modified:
1021 1021 files that have definitely been modified since the dirstate
1022 1022 was written (different size or mode)
1023 1023 status.clean:
1024 1024 files that have definitely not been modified since the
1025 1025 dirstate was written
1026 1026 '''
1027 1027 listignored, listclean, listunknown = ignored, clean, unknown
1028 1028 lookup, modified, added, unknown, ignored = [], [], [], [], []
1029 1029 removed, deleted, clean = [], [], []
1030 1030
1031 1031 dmap = self._map
1032 1032 dmap.preload()
1033 1033 dcontains = dmap.__contains__
1034 1034 dget = dmap.__getitem__
1035 1035 ladd = lookup.append # aka "unsure"
1036 1036 madd = modified.append
1037 1037 aadd = added.append
1038 1038 uadd = unknown.append
1039 1039 iadd = ignored.append
1040 1040 radd = removed.append
1041 1041 dadd = deleted.append
1042 1042 cadd = clean.append
1043 1043 mexact = match.exact
1044 1044 dirignore = self._dirignore
1045 1045 checkexec = self._checkexec
1046 1046 copymap = self._map.copymap
1047 1047 lastnormaltime = self._lastnormaltime
1048 1048
1049 1049 # We need to do full walks when either
1050 1050 # - we're listing all clean files, or
1051 1051 # - match.traversedir does something, because match.traversedir should
1052 1052 # be called for every dir in the working dir
1053 1053 full = listclean or match.traversedir is not None
1054 1054 for fn, st in self.walk(match, subrepos, listunknown, listignored,
1055 1055 full=full).iteritems():
1056 1056 if not dcontains(fn):
1057 1057 if (listignored or mexact(fn)) and dirignore(fn):
1058 1058 if listignored:
1059 1059 iadd(fn)
1060 1060 else:
1061 1061 uadd(fn)
1062 1062 continue
1063 1063
1064 1064 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1065 1065 # written like that for performance reasons. dmap[fn] is not a
1066 1066 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1067 1067 # opcode has fast paths when the value to be unpacked is a tuple or
1068 1068 # a list, but falls back to creating a full-fledged iterator in
1069 1069 # general. That is much slower than simply accessing and storing the
1070 1070 # tuple members one by one.
1071 1071 t = dget(fn)
1072 1072 state = t[0]
1073 1073 mode = t[1]
1074 1074 size = t[2]
1075 1075 time = t[3]
1076 1076
1077 1077 if not st and state in "nma":
1078 1078 dadd(fn)
1079 1079 elif state == 'n':
1080 1080 if (size >= 0 and
1081 1081 ((size != st.st_size and size != st.st_size & _rangemask)
1082 1082 or ((mode ^ st.st_mode) & 0o100 and checkexec))
1083 1083 or size == -2 # other parent
1084 1084 or fn in copymap):
1085 1085 madd(fn)
1086 1086 elif (time != st[stat.ST_MTIME]
1087 1087 and time != st[stat.ST_MTIME] & _rangemask):
1088 1088 ladd(fn)
1089 1089 elif st[stat.ST_MTIME] == lastnormaltime:
1090 1090 # fn may have just been marked as normal and it may have
1091 1091 # changed in the same second without changing its size.
1092 1092 # This can happen if we quickly do multiple commits.
1093 1093 # Force lookup, so we don't miss such a racy file change.
1094 1094 ladd(fn)
1095 1095 elif listclean:
1096 1096 cadd(fn)
1097 1097 elif state == 'm':
1098 1098 madd(fn)
1099 1099 elif state == 'a':
1100 1100 aadd(fn)
1101 1101 elif state == 'r':
1102 1102 radd(fn)
1103 1103
1104 1104 return (lookup, scmutil.status(modified, added, removed, deleted,
1105 1105 unknown, ignored, clean))
1106 1106
1107 1107 def matches(self, match):
1108 1108 '''
1109 1109 return files in the dirstate (in whatever state) filtered by match
1110 1110 '''
1111 1111 dmap = self._map
1112 1112 if match.always():
1113 1113 return dmap.keys()
1114 1114 files = match.files()
1115 1115 if match.isexact():
1116 1116 # fast path -- filter the other way around, since typically files is
1117 1117 # much smaller than dmap
1118 1118 return [f for f in files if f in dmap]
1119 1119 if match.prefix() and all(fn in dmap for fn in files):
1120 1120 # fast path -- all the values are known to be files, so just return
1121 1121 # that
1122 1122 return list(files)
1123 1123 return [f for f in dmap if match(f)]
1124 1124
1125 1125 def _actualfilename(self, tr):
1126 1126 if tr:
1127 1127 return self._pendingfilename
1128 1128 else:
1129 1129 return self._filename
1130 1130
1131 1131 def savebackup(self, tr, backupname):
1132 1132 '''Save current dirstate into backup file'''
1133 1133 filename = self._actualfilename(tr)
1134 1134 assert backupname != filename
1135 1135
1136 1136 # use '_writedirstate' instead of 'write' to write changes certainly,
1137 1137 # because the latter omits writing out if transaction is running.
1138 1138 # output file will be used to create backup of dirstate at this point.
1139 1139 if self._dirty or not self._opener.exists(filename):
1140 1140 self._writedirstate(self._opener(filename, "w", atomictemp=True,
1141 1141 checkambig=True))
1142 1142
1143 1143 if tr:
1144 1144 # ensure that subsequent tr.writepending returns True for
1145 1145 # changes written out above, even if dirstate is never
1146 1146 # changed after this
1147 1147 tr.addfilegenerator('dirstate', (self._filename,),
1148 1148 self._writedirstate, location='plain')
1149 1149
1150 1150 # ensure that pending file written above is unlinked at
1151 1151 # failure, even if tr.writepending isn't invoked until the
1152 1152 # end of this transaction
1153 1153 tr.registertmp(filename, location='plain')
1154 1154
1155 1155 self._opener.tryunlink(backupname)
1156 1156 # hardlink backup is okay because _writedirstate is always called
1157 1157 # with an "atomictemp=True" file.
1158 1158 util.copyfile(self._opener.join(filename),
1159 1159 self._opener.join(backupname), hardlink=True)
1160 1160
1161 1161 def restorebackup(self, tr, backupname):
1162 1162 '''Restore dirstate by backup file'''
1163 1163 # this "invalidate()" prevents "wlock.release()" from writing
1164 1164 # changes of dirstate out after restoring from backup file
1165 1165 self.invalidate()
1166 1166 filename = self._actualfilename(tr)
1167 1167 o = self._opener
1168 1168 if util.samefile(o.join(backupname), o.join(filename)):
1169 1169 o.unlink(backupname)
1170 1170 else:
1171 1171 o.rename(backupname, filename, checkambig=True)
1172 1172
1173 1173 def clearbackup(self, tr, backupname):
1174 1174 '''Clear backup file'''
1175 1175 self._opener.unlink(backupname)
1176 1176
1177 1177 class dirstatemap(object):
1178 1178 """Map encapsulating the dirstate's contents.
1179 1179
1180 1180 The dirstate contains the following state:
1181 1181
1182 1182 - `identity` is the identity of the dirstate file, which can be used to
1183 1183 detect when changes have occurred to the dirstate file.
1184 1184
1185 1185 - `parents` is a pair containing the parents of the working copy. The
1186 1186 parents are updated by calling `setparents`.
1187 1187
1188 1188 - the state map maps filenames to tuples of (state, mode, size, mtime),
1189 1189 where state is a single character representing 'normal', 'added',
1190 1190 'removed', or 'merged'. It is read by treating the dirstate as a
1191 1191 dict. File state is updated by calling the `addfile`, `removefile` and
1192 1192 `dropfile` methods.
1193 1193
1194 1194 - `copymap` maps destination filenames to their source filename.
1195 1195
1196 1196 The dirstate also provides the following views onto the state:
1197 1197
1198 1198 - `nonnormalset` is a set of the filenames that have state other
1199 1199 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1200 1200
1201 1201 - `otherparentset` is a set of the filenames that are marked as coming
1202 1202 from the second parent when the dirstate is currently being merged.
1203 1203
1204 1204 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1205 1205 form that they appear as in the dirstate.
1206 1206
1207 1207 - `dirfoldmap` is a dict mapping normalized directory names to the
1208 1208 denormalized form that they appear as in the dirstate.
1209 1209 """
1210 1210
1211 1211 def __init__(self, ui, opener, root):
1212 1212 self._ui = ui
1213 1213 self._opener = opener
1214 1214 self._root = root
1215 1215 self._filename = 'dirstate'
1216 1216
1217 1217 self._parents = None
1218 1218 self._dirtyparents = False
1219 1219
1220 1220 # for consistent view between _pl() and _read() invocations
1221 1221 self._pendingmode = None
1222 1222
1223 1223 @propertycache
1224 1224 def _map(self):
1225 1225 self._map = {}
1226 1226 self.read()
1227 1227 return self._map
1228 1228
1229 1229 @propertycache
1230 1230 def copymap(self):
1231 1231 self.copymap = {}
1232 1232 self._map
1233 1233 return self.copymap
1234 1234
1235 1235 def clear(self):
1236 1236 self._map.clear()
1237 1237 self.copymap.clear()
1238 1238 self.setparents(nullid, nullid)
1239 1239 util.clearcachedproperty(self, "_dirs")
1240 1240 util.clearcachedproperty(self, "_alldirs")
1241 1241 util.clearcachedproperty(self, "filefoldmap")
1242 1242 util.clearcachedproperty(self, "dirfoldmap")
1243 1243 util.clearcachedproperty(self, "nonnormalset")
1244 1244 util.clearcachedproperty(self, "otherparentset")
1245 1245
1246 1246 def items(self):
1247 1247 return self._map.iteritems()
1248 1248
1249 1249 # forward for python2,3 compat
1250 1250 iteritems = items
1251 1251
1252 1252 def __len__(self):
1253 1253 return len(self._map)
1254 1254
1255 1255 def __iter__(self):
1256 1256 return iter(self._map)
1257 1257
1258 1258 def get(self, key, default=None):
1259 1259 return self._map.get(key, default)
1260 1260
1261 1261 def __contains__(self, key):
1262 1262 return key in self._map
1263 1263
1264 1264 def __getitem__(self, key):
1265 1265 return self._map[key]
1266 1266
1267 1267 def keys(self):
1268 1268 return self._map.keys()
1269 1269
1270 1270 def preload(self):
1271 1271 """Loads the underlying data, if it's not already loaded"""
1272 1272 self._map
1273 1273
1274 1274 def addfile(self, f, oldstate, state, mode, size, mtime):
1275 1275 """Add a tracked file to the dirstate."""
1276 1276 if oldstate in "?r" and r"_dirs" in self.__dict__:
1277 1277 self._dirs.addpath(f)
1278 1278 if oldstate == "?" and r"_alldirs" in self.__dict__:
1279 1279 self._alldirs.addpath(f)
1280 1280 self._map[f] = dirstatetuple(state, mode, size, mtime)
1281 1281 if state != 'n' or mtime == -1:
1282 1282 self.nonnormalset.add(f)
1283 1283 if size == -2:
1284 1284 self.otherparentset.add(f)
1285 1285
1286 1286 def removefile(self, f, oldstate, size):
1287 1287 """
1288 1288 Mark a file as removed in the dirstate.
1289 1289
1290 1290 The `size` parameter is used to store sentinel values that indicate
1291 1291 the file's previous state. In the future, we should refactor this
1292 1292 to be more explicit about what that state is.
1293 1293 """
1294 1294 if oldstate not in "?r" and r"_dirs" in self.__dict__:
1295 1295 self._dirs.delpath(f)
1296 1296 if oldstate == "?" and r"_alldirs" in self.__dict__:
1297 1297 self._alldirs.addpath(f)
1298 1298 if r"filefoldmap" in self.__dict__:
1299 1299 normed = util.normcase(f)
1300 1300 self.filefoldmap.pop(normed, None)
1301 1301 self._map[f] = dirstatetuple('r', 0, size, 0)
1302 1302 self.nonnormalset.add(f)
1303 1303
1304 1304 def dropfile(self, f, oldstate):
1305 1305 """
1306 1306 Remove a file from the dirstate. Returns True if the file was
1307 1307 previously recorded.
1308 1308 """
1309 1309 exists = self._map.pop(f, None) is not None
1310 1310 if exists:
1311 1311 if oldstate != "r" and r"_dirs" in self.__dict__:
1312 1312 self._dirs.delpath(f)
1313 1313 if r"_alldirs" in self.__dict__:
1314 1314 self._alldirs.delpath(f)
1315 1315 if r"filefoldmap" in self.__dict__:
1316 1316 normed = util.normcase(f)
1317 1317 self.filefoldmap.pop(normed, None)
1318 1318 self.nonnormalset.discard(f)
1319 1319 return exists
1320 1320
1321 1321 def clearambiguoustimes(self, files, now):
1322 1322 for f in files:
1323 1323 e = self.get(f)
1324 1324 if e is not None and e[0] == 'n' and e[3] == now:
1325 1325 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1326 1326 self.nonnormalset.add(f)
1327 1327
1328 1328 def nonnormalentries(self):
1329 1329 '''Compute the nonnormal dirstate entries from the dmap'''
1330 1330 try:
1331 1331 return parsers.nonnormalotherparententries(self._map)
1332 1332 except AttributeError:
1333 1333 nonnorm = set()
1334 1334 otherparent = set()
1335 1335 for fname, e in self._map.iteritems():
1336 1336 if e[0] != 'n' or e[3] == -1:
1337 1337 nonnorm.add(fname)
1338 1338 if e[0] == 'n' and e[2] == -2:
1339 1339 otherparent.add(fname)
1340 1340 return nonnorm, otherparent
1341 1341
1342 1342 @propertycache
1343 1343 def filefoldmap(self):
1344 1344 """Returns a dictionary mapping normalized case paths to their
1345 1345 non-normalized versions.
1346 1346 """
1347 1347 try:
1348 1348 makefilefoldmap = parsers.make_file_foldmap
1349 1349 except AttributeError:
1350 1350 pass
1351 1351 else:
1352 1352 return makefilefoldmap(self._map, util.normcasespec,
1353 1353 util.normcasefallback)
1354 1354
1355 1355 f = {}
1356 1356 normcase = util.normcase
1357 1357 for name, s in self._map.iteritems():
1358 1358 if s[0] != 'r':
1359 1359 f[normcase(name)] = name
1360 1360 f['.'] = '.' # prevents useless util.fspath() invocation
1361 1361 return f
1362 1362
1363 1363 def hastrackeddir(self, d):
1364 1364 """
1365 1365 Returns True if the dirstate contains a tracked (not removed) file
1366 1366 in this directory.
1367 1367 """
1368 1368 return d in self._dirs
1369 1369
1370 1370 def hasdir(self, d):
1371 1371 """
1372 1372 Returns True if the dirstate contains a file (tracked or removed)
1373 1373 in this directory.
1374 1374 """
1375 1375 return d in self._alldirs
1376 1376
1377 1377 @propertycache
1378 1378 def _dirs(self):
1379 1379 return util.dirs(self._map, 'r')
1380 1380
1381 1381 @propertycache
1382 1382 def _alldirs(self):
1383 1383 return util.dirs(self._map)
1384 1384
1385 1385 def _opendirstatefile(self):
1386 1386 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1387 1387 if self._pendingmode is not None and self._pendingmode != mode:
1388 1388 fp.close()
1389 1389 raise error.Abort(_('working directory state may be '
1390 1390 'changed parallelly'))
1391 1391 self._pendingmode = mode
1392 1392 return fp
1393 1393
1394 1394 def parents(self):
1395 1395 if not self._parents:
1396 1396 try:
1397 1397 fp = self._opendirstatefile()
1398 1398 st = fp.read(40)
1399 1399 fp.close()
1400 1400 except IOError as err:
1401 1401 if err.errno != errno.ENOENT:
1402 1402 raise
1403 1403 # File doesn't exist, so the current state is empty
1404 1404 st = ''
1405 1405
1406 1406 l = len(st)
1407 1407 if l == 40:
1408 1408 self._parents = (st[:20], st[20:40])
1409 1409 elif l == 0:
1410 1410 self._parents = (nullid, nullid)
1411 1411 else:
1412 1412 raise error.Abort(_('working directory state appears '
1413 1413 'damaged!'))
1414 1414
1415 1415 return self._parents
1416 1416
1417 1417 def setparents(self, p1, p2):
1418 1418 self._parents = (p1, p2)
1419 1419 self._dirtyparents = True
1420 1420
1421 1421 def read(self):
1422 1422 # ignore HG_PENDING because identity is used only for writing
1423 1423 self.identity = util.filestat.frompath(
1424 1424 self._opener.join(self._filename))
1425 1425
1426 1426 try:
1427 1427 fp = self._opendirstatefile()
1428 1428 try:
1429 1429 st = fp.read()
1430 1430 finally:
1431 1431 fp.close()
1432 1432 except IOError as err:
1433 1433 if err.errno != errno.ENOENT:
1434 1434 raise
1435 1435 return
1436 1436 if not st:
1437 1437 return
1438 1438
1439 1439 if util.safehasattr(parsers, 'dict_new_presized'):
1440 1440 # Make an estimate of the number of files in the dirstate based on
1441 1441 # its size. From a linear regression on a set of real-world repos,
1442 1442 # all over 10,000 files, the size of a dirstate entry is 85
1443 1443 # bytes. The cost of resizing is significantly higher than the cost
1444 1444 # of filling in a larger presized dict, so subtract 20% from the
1445 1445 # size.
1446 1446 #
1447 1447 # This heuristic is imperfect in many ways, so in a future dirstate
1448 1448 # format update it makes sense to just record the number of entries
1449 1449 # on write.
1450 1450 self._map = parsers.dict_new_presized(len(st) // 71)
1451 1451
1452 1452 # Python's garbage collector triggers a GC each time a certain number
1453 1453 # of container objects (the number being defined by
1454 1454 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1455 1455 # for each file in the dirstate. The C version then immediately marks
1456 1456 # them as not to be tracked by the collector. However, this has no
1457 1457 # effect on when GCs are triggered, only on what objects the GC looks
1458 1458 # into. This means that O(number of files) GCs are unavoidable.
1459 1459 # Depending on when in the process's lifetime the dirstate is parsed,
1460 1460 # this can get very expensive. As a workaround, disable GC while
1461 1461 # parsing the dirstate.
1462 1462 #
1463 1463 # (we cannot decorate the function directly since it is in a C module)
1464 1464 parse_dirstate = util.nogc(parsers.parse_dirstate)
1465 1465 p = parse_dirstate(self._map, self.copymap, st)
1466 1466 if not self._dirtyparents:
1467 1467 self.setparents(*p)
1468 1468
1469 1469 # Avoid excess attribute lookups by fast pathing certain checks
1470 1470 self.__contains__ = self._map.__contains__
1471 1471 self.__getitem__ = self._map.__getitem__
1472 1472 self.get = self._map.get
1473 1473
1474 1474 def write(self, st, now):
1475 1475 st.write(parsers.pack_dirstate(self._map, self.copymap,
1476 1476 self.parents(), now))
1477 1477 st.close()
1478 1478 self._dirtyparents = False
1479 1479 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1480 1480
1481 1481 @propertycache
1482 1482 def nonnormalset(self):
1483 1483 nonnorm, otherparents = self.nonnormalentries()
1484 1484 self.otherparentset = otherparents
1485 1485 return nonnorm
1486 1486
1487 1487 @propertycache
1488 1488 def otherparentset(self):
1489 1489 nonnorm, otherparents = self.nonnormalentries()
1490 1490 self.nonnormalset = nonnorm
1491 1491 return otherparents
1492 1492
1493 1493 @propertycache
1494 1494 def identity(self):
1495 1495 self._map
1496 1496 return self.identity
1497 1497
1498 1498 @propertycache
1499 1499 def dirfoldmap(self):
1500 1500 f = {}
1501 1501 normcase = util.normcase
1502 1502 for name in self._dirs:
1503 1503 f[normcase(name)] = name
1504 1504 return f
@@ -1,3050 +1,3049 b''
1 1 # localrepo.py - read/write repository class for mercurial
2 2 #
3 3 # Copyright 2005-2007 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 errno
11 11 import hashlib
12 12 import os
13 13 import random
14 14 import sys
15 15 import time
16 16 import weakref
17 17
18 18 from .i18n import _
19 19 from .node import (
20 20 bin,
21 21 hex,
22 22 nullid,
23 23 nullrev,
24 24 short,
25 25 )
26 26 from . import (
27 27 bookmarks,
28 28 branchmap,
29 29 bundle2,
30 30 changegroup,
31 31 changelog,
32 32 color,
33 33 context,
34 34 dirstate,
35 35 dirstateguard,
36 36 discovery,
37 37 encoding,
38 38 error,
39 39 exchange,
40 40 extensions,
41 41 filelog,
42 42 hook,
43 43 lock as lockmod,
44 44 manifest,
45 45 match as matchmod,
46 46 merge as mergemod,
47 47 mergeutil,
48 48 namespaces,
49 49 narrowspec,
50 50 obsolete,
51 51 pathutil,
52 52 phases,
53 53 pushkey,
54 54 pycompat,
55 55 repository,
56 56 repoview,
57 57 revset,
58 58 revsetlang,
59 59 scmutil,
60 60 sparse,
61 61 store as storemod,
62 62 subrepoutil,
63 63 tags as tagsmod,
64 64 transaction,
65 65 txnutil,
66 66 util,
67 67 vfs as vfsmod,
68 68 )
69 69 from .utils import (
70 70 interfaceutil,
71 71 procutil,
72 72 stringutil,
73 73 )
74 74
75 75 from .revlogutils import (
76 76 constants as revlogconst,
77 77 )
78 78
79 79 release = lockmod.release
80 80 urlerr = util.urlerr
81 81 urlreq = util.urlreq
82 82
83 83 # set of (path, vfs-location) tuples. vfs-location is:
84 84 # - 'plain for vfs relative paths
85 85 # - '' for svfs relative paths
86 86 _cachedfiles = set()
87 87
88 88 class _basefilecache(scmutil.filecache):
89 89 """All filecache usage on repo are done for logic that should be unfiltered
90 90 """
91 91 def __get__(self, repo, type=None):
92 92 if repo is None:
93 93 return self
94 # inlined the fast path as the cost of function call matters
94 # proxy to unfiltered __dict__ since filtered repo has no entry
95 95 unfi = repo.unfiltered()
96 96 try:
97 97 return unfi.__dict__[self.sname]
98 98 except KeyError:
99 99 pass
100 100 return super(_basefilecache, self).__get__(unfi, type)
101 def __set__(self, repo, value):
102 return super(_basefilecache, self).__set__(repo.unfiltered(), value)
103 def __delete__(self, repo):
104 return super(_basefilecache, self).__delete__(repo.unfiltered())
101
102 def set(self, repo, value):
103 return super(_basefilecache, self).set(repo.unfiltered(), value)
105 104
106 105 class repofilecache(_basefilecache):
107 106 """filecache for files in .hg but outside of .hg/store"""
108 107 def __init__(self, *paths):
109 108 super(repofilecache, self).__init__(*paths)
110 109 for path in paths:
111 110 _cachedfiles.add((path, 'plain'))
112 111
113 112 def join(self, obj, fname):
114 113 return obj.vfs.join(fname)
115 114
116 115 class storecache(_basefilecache):
117 116 """filecache for files in the store"""
118 117 def __init__(self, *paths):
119 118 super(storecache, self).__init__(*paths)
120 119 for path in paths:
121 120 _cachedfiles.add((path, ''))
122 121
123 122 def join(self, obj, fname):
124 123 return obj.sjoin(fname)
125 124
126 125 def isfilecached(repo, name):
127 126 """check if a repo has already cached "name" filecache-ed property
128 127
129 128 This returns (cachedobj-or-None, iscached) tuple.
130 129 """
131 130 cacheentry = repo.unfiltered()._filecache.get(name, None)
132 131 if not cacheentry:
133 132 return None, False
134 133 return cacheentry.obj, True
135 134
136 135 class unfilteredpropertycache(util.propertycache):
137 136 """propertycache that apply to unfiltered repo only"""
138 137
139 138 def __get__(self, repo, type=None):
140 139 unfi = repo.unfiltered()
141 140 if unfi is repo:
142 141 return super(unfilteredpropertycache, self).__get__(unfi)
143 142 return getattr(unfi, self.name)
144 143
145 144 class filteredpropertycache(util.propertycache):
146 145 """propertycache that must take filtering in account"""
147 146
148 147 def cachevalue(self, obj, value):
149 148 object.__setattr__(obj, self.name, value)
150 149
151 150
152 151 def hasunfilteredcache(repo, name):
153 152 """check if a repo has an unfilteredpropertycache value for <name>"""
154 153 return name in vars(repo.unfiltered())
155 154
156 155 def unfilteredmethod(orig):
157 156 """decorate method that always need to be run on unfiltered version"""
158 157 def wrapper(repo, *args, **kwargs):
159 158 return orig(repo.unfiltered(), *args, **kwargs)
160 159 return wrapper
161 160
162 161 moderncaps = {'lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
163 162 'unbundle'}
164 163 legacycaps = moderncaps.union({'changegroupsubset'})
165 164
166 165 @interfaceutil.implementer(repository.ipeercommandexecutor)
167 166 class localcommandexecutor(object):
168 167 def __init__(self, peer):
169 168 self._peer = peer
170 169 self._sent = False
171 170 self._closed = False
172 171
173 172 def __enter__(self):
174 173 return self
175 174
176 175 def __exit__(self, exctype, excvalue, exctb):
177 176 self.close()
178 177
179 178 def callcommand(self, command, args):
180 179 if self._sent:
181 180 raise error.ProgrammingError('callcommand() cannot be used after '
182 181 'sendcommands()')
183 182
184 183 if self._closed:
185 184 raise error.ProgrammingError('callcommand() cannot be used after '
186 185 'close()')
187 186
188 187 # We don't need to support anything fancy. Just call the named
189 188 # method on the peer and return a resolved future.
190 189 fn = getattr(self._peer, pycompat.sysstr(command))
191 190
192 191 f = pycompat.futures.Future()
193 192
194 193 try:
195 194 result = fn(**pycompat.strkwargs(args))
196 195 except Exception:
197 196 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
198 197 else:
199 198 f.set_result(result)
200 199
201 200 return f
202 201
203 202 def sendcommands(self):
204 203 self._sent = True
205 204
206 205 def close(self):
207 206 self._closed = True
208 207
209 208 @interfaceutil.implementer(repository.ipeercommands)
210 209 class localpeer(repository.peer):
211 210 '''peer for a local repo; reflects only the most recent API'''
212 211
213 212 def __init__(self, repo, caps=None):
214 213 super(localpeer, self).__init__()
215 214
216 215 if caps is None:
217 216 caps = moderncaps.copy()
218 217 self._repo = repo.filtered('served')
219 218 self.ui = repo.ui
220 219 self._caps = repo._restrictcapabilities(caps)
221 220
222 221 # Begin of _basepeer interface.
223 222
224 223 def url(self):
225 224 return self._repo.url()
226 225
227 226 def local(self):
228 227 return self._repo
229 228
230 229 def peer(self):
231 230 return self
232 231
233 232 def canpush(self):
234 233 return True
235 234
236 235 def close(self):
237 236 self._repo.close()
238 237
239 238 # End of _basepeer interface.
240 239
241 240 # Begin of _basewirecommands interface.
242 241
243 242 def branchmap(self):
244 243 return self._repo.branchmap()
245 244
246 245 def capabilities(self):
247 246 return self._caps
248 247
249 248 def clonebundles(self):
250 249 return self._repo.tryread('clonebundles.manifest')
251 250
252 251 def debugwireargs(self, one, two, three=None, four=None, five=None):
253 252 """Used to test argument passing over the wire"""
254 253 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
255 254 pycompat.bytestr(four),
256 255 pycompat.bytestr(five))
257 256
258 257 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
259 258 **kwargs):
260 259 chunks = exchange.getbundlechunks(self._repo, source, heads=heads,
261 260 common=common, bundlecaps=bundlecaps,
262 261 **kwargs)[1]
263 262 cb = util.chunkbuffer(chunks)
264 263
265 264 if exchange.bundle2requested(bundlecaps):
266 265 # When requesting a bundle2, getbundle returns a stream to make the
267 266 # wire level function happier. We need to build a proper object
268 267 # from it in local peer.
269 268 return bundle2.getunbundler(self.ui, cb)
270 269 else:
271 270 return changegroup.getunbundler('01', cb, None)
272 271
273 272 def heads(self):
274 273 return self._repo.heads()
275 274
276 275 def known(self, nodes):
277 276 return self._repo.known(nodes)
278 277
279 278 def listkeys(self, namespace):
280 279 return self._repo.listkeys(namespace)
281 280
282 281 def lookup(self, key):
283 282 return self._repo.lookup(key)
284 283
285 284 def pushkey(self, namespace, key, old, new):
286 285 return self._repo.pushkey(namespace, key, old, new)
287 286
288 287 def stream_out(self):
289 288 raise error.Abort(_('cannot perform stream clone against local '
290 289 'peer'))
291 290
292 291 def unbundle(self, bundle, heads, url):
293 292 """apply a bundle on a repo
294 293
295 294 This function handles the repo locking itself."""
296 295 try:
297 296 try:
298 297 bundle = exchange.readbundle(self.ui, bundle, None)
299 298 ret = exchange.unbundle(self._repo, bundle, heads, 'push', url)
300 299 if util.safehasattr(ret, 'getchunks'):
301 300 # This is a bundle20 object, turn it into an unbundler.
302 301 # This little dance should be dropped eventually when the
303 302 # API is finally improved.
304 303 stream = util.chunkbuffer(ret.getchunks())
305 304 ret = bundle2.getunbundler(self.ui, stream)
306 305 return ret
307 306 except Exception as exc:
308 307 # If the exception contains output salvaged from a bundle2
309 308 # reply, we need to make sure it is printed before continuing
310 309 # to fail. So we build a bundle2 with such output and consume
311 310 # it directly.
312 311 #
313 312 # This is not very elegant but allows a "simple" solution for
314 313 # issue4594
315 314 output = getattr(exc, '_bundle2salvagedoutput', ())
316 315 if output:
317 316 bundler = bundle2.bundle20(self._repo.ui)
318 317 for out in output:
319 318 bundler.addpart(out)
320 319 stream = util.chunkbuffer(bundler.getchunks())
321 320 b = bundle2.getunbundler(self.ui, stream)
322 321 bundle2.processbundle(self._repo, b)
323 322 raise
324 323 except error.PushRaced as exc:
325 324 raise error.ResponseError(_('push failed:'),
326 325 stringutil.forcebytestr(exc))
327 326
328 327 # End of _basewirecommands interface.
329 328
330 329 # Begin of peer interface.
331 330
332 331 def commandexecutor(self):
333 332 return localcommandexecutor(self)
334 333
335 334 # End of peer interface.
336 335
337 336 @interfaceutil.implementer(repository.ipeerlegacycommands)
338 337 class locallegacypeer(localpeer):
339 338 '''peer extension which implements legacy methods too; used for tests with
340 339 restricted capabilities'''
341 340
342 341 def __init__(self, repo):
343 342 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
344 343
345 344 # Begin of baselegacywirecommands interface.
346 345
347 346 def between(self, pairs):
348 347 return self._repo.between(pairs)
349 348
350 349 def branches(self, nodes):
351 350 return self._repo.branches(nodes)
352 351
353 352 def changegroup(self, nodes, source):
354 353 outgoing = discovery.outgoing(self._repo, missingroots=nodes,
355 354 missingheads=self._repo.heads())
356 355 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
357 356
358 357 def changegroupsubset(self, bases, heads, source):
359 358 outgoing = discovery.outgoing(self._repo, missingroots=bases,
360 359 missingheads=heads)
361 360 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
362 361
363 362 # End of baselegacywirecommands interface.
364 363
365 364 # Increment the sub-version when the revlog v2 format changes to lock out old
366 365 # clients.
367 366 REVLOGV2_REQUIREMENT = 'exp-revlogv2.0'
368 367
369 368 # A repository with the sparserevlog feature will have delta chains that
370 369 # can spread over a larger span. Sparse reading cuts these large spans into
371 370 # pieces, so that each piece isn't too big.
372 371 # Without the sparserevlog capability, reading from the repository could use
373 372 # huge amounts of memory, because the whole span would be read at once,
374 373 # including all the intermediate revisions that aren't pertinent for the chain.
375 374 # This is why once a repository has enabled sparse-read, it becomes required.
376 375 SPARSEREVLOG_REQUIREMENT = 'sparserevlog'
377 376
378 377 # Functions receiving (ui, features) that extensions can register to impact
379 378 # the ability to load repositories with custom requirements. Only
380 379 # functions defined in loaded extensions are called.
381 380 #
382 381 # The function receives a set of requirement strings that the repository
383 382 # is capable of opening. Functions will typically add elements to the
384 383 # set to reflect that the extension knows how to handle that requirements.
385 384 featuresetupfuncs = set()
386 385
387 386 def makelocalrepository(baseui, path, intents=None):
388 387 """Create a local repository object.
389 388
390 389 Given arguments needed to construct a local repository, this function
391 390 performs various early repository loading functionality (such as
392 391 reading the ``.hg/requires`` and ``.hg/hgrc`` files), validates that
393 392 the repository can be opened, derives a type suitable for representing
394 393 that repository, and returns an instance of it.
395 394
396 395 The returned object conforms to the ``repository.completelocalrepository``
397 396 interface.
398 397
399 398 The repository type is derived by calling a series of factory functions
400 399 for each aspect/interface of the final repository. These are defined by
401 400 ``REPO_INTERFACES``.
402 401
403 402 Each factory function is called to produce a type implementing a specific
404 403 interface. The cumulative list of returned types will be combined into a
405 404 new type and that type will be instantiated to represent the local
406 405 repository.
407 406
408 407 The factory functions each receive various state that may be consulted
409 408 as part of deriving a type.
410 409
411 410 Extensions should wrap these factory functions to customize repository type
412 411 creation. Note that an extension's wrapped function may be called even if
413 412 that extension is not loaded for the repo being constructed. Extensions
414 413 should check if their ``__name__`` appears in the
415 414 ``extensionmodulenames`` set passed to the factory function and no-op if
416 415 not.
417 416 """
418 417 ui = baseui.copy()
419 418 # Prevent copying repo configuration.
420 419 ui.copy = baseui.copy
421 420
422 421 # Working directory VFS rooted at repository root.
423 422 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
424 423
425 424 # Main VFS for .hg/ directory.
426 425 hgpath = wdirvfs.join(b'.hg')
427 426 hgvfs = vfsmod.vfs(hgpath, cacheaudited=True)
428 427
429 428 # The .hg/ path should exist and should be a directory. All other
430 429 # cases are errors.
431 430 if not hgvfs.isdir():
432 431 try:
433 432 hgvfs.stat()
434 433 except OSError as e:
435 434 if e.errno != errno.ENOENT:
436 435 raise
437 436
438 437 raise error.RepoError(_(b'repository %s not found') % path)
439 438
440 439 # .hg/requires file contains a newline-delimited list of
441 440 # features/capabilities the opener (us) must have in order to use
442 441 # the repository. This file was introduced in Mercurial 0.9.2,
443 442 # which means very old repositories may not have one. We assume
444 443 # a missing file translates to no requirements.
445 444 try:
446 445 requirements = set(hgvfs.read(b'requires').splitlines())
447 446 except IOError as e:
448 447 if e.errno != errno.ENOENT:
449 448 raise
450 449 requirements = set()
451 450
452 451 # The .hg/hgrc file may load extensions or contain config options
453 452 # that influence repository construction. Attempt to load it and
454 453 # process any new extensions that it may have pulled in.
455 454 try:
456 455 ui.readconfig(hgvfs.join(b'hgrc'), root=wdirvfs.base)
457 456 # Run this before extensions.loadall() so extensions can be
458 457 # automatically enabled.
459 458 afterhgrcload(ui, wdirvfs, hgvfs, requirements)
460 459 except IOError:
461 460 pass
462 461 else:
463 462 extensions.loadall(ui)
464 463
465 464 # Set of module names of extensions loaded for this repository.
466 465 extensionmodulenames = {m.__name__ for n, m in extensions.extensions(ui)}
467 466
468 467 supportedrequirements = gathersupportedrequirements(ui)
469 468
470 469 # We first validate the requirements are known.
471 470 ensurerequirementsrecognized(requirements, supportedrequirements)
472 471
473 472 # Then we validate that the known set is reasonable to use together.
474 473 ensurerequirementscompatible(ui, requirements)
475 474
476 475 # TODO there are unhandled edge cases related to opening repositories with
477 476 # shared storage. If storage is shared, we should also test for requirements
478 477 # compatibility in the pointed-to repo. This entails loading the .hg/hgrc in
479 478 # that repo, as that repo may load extensions needed to open it. This is a
480 479 # bit complicated because we don't want the other hgrc to overwrite settings
481 480 # in this hgrc.
482 481 #
483 482 # This bug is somewhat mitigated by the fact that we copy the .hg/requires
484 483 # file when sharing repos. But if a requirement is added after the share is
485 484 # performed, thereby introducing a new requirement for the opener, we may
486 485 # will not see that and could encounter a run-time error interacting with
487 486 # that shared store since it has an unknown-to-us requirement.
488 487
489 488 # At this point, we know we should be capable of opening the repository.
490 489 # Now get on with doing that.
491 490
492 491 features = set()
493 492
494 493 # The "store" part of the repository holds versioned data. How it is
495 494 # accessed is determined by various requirements. The ``shared`` or
496 495 # ``relshared`` requirements indicate the store lives in the path contained
497 496 # in the ``.hg/sharedpath`` file. This is an absolute path for
498 497 # ``shared`` and relative to ``.hg/`` for ``relshared``.
499 498 if b'shared' in requirements or b'relshared' in requirements:
500 499 sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n')
501 500 if b'relshared' in requirements:
502 501 sharedpath = hgvfs.join(sharedpath)
503 502
504 503 sharedvfs = vfsmod.vfs(sharedpath, realpath=True)
505 504
506 505 if not sharedvfs.exists():
507 506 raise error.RepoError(_(b'.hg/sharedpath points to nonexistent '
508 507 b'directory %s') % sharedvfs.base)
509 508
510 509 features.add(repository.REPO_FEATURE_SHARED_STORAGE)
511 510
512 511 storebasepath = sharedvfs.base
513 512 cachepath = sharedvfs.join(b'cache')
514 513 else:
515 514 storebasepath = hgvfs.base
516 515 cachepath = hgvfs.join(b'cache')
517 516
518 517 # The store has changed over time and the exact layout is dictated by
519 518 # requirements. The store interface abstracts differences across all
520 519 # of them.
521 520 store = makestore(requirements, storebasepath,
522 521 lambda base: vfsmod.vfs(base, cacheaudited=True))
523 522 hgvfs.createmode = store.createmode
524 523
525 524 storevfs = store.vfs
526 525 storevfs.options = resolvestorevfsoptions(ui, requirements, features)
527 526
528 527 # The cache vfs is used to manage cache files.
529 528 cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
530 529 cachevfs.createmode = store.createmode
531 530
532 531 # Now resolve the type for the repository object. We do this by repeatedly
533 532 # calling a factory function to produces types for specific aspects of the
534 533 # repo's operation. The aggregate returned types are used as base classes
535 534 # for a dynamically-derived type, which will represent our new repository.
536 535
537 536 bases = []
538 537 extrastate = {}
539 538
540 539 for iface, fn in REPO_INTERFACES:
541 540 # We pass all potentially useful state to give extensions tons of
542 541 # flexibility.
543 542 typ = fn()(ui=ui,
544 543 intents=intents,
545 544 requirements=requirements,
546 545 features=features,
547 546 wdirvfs=wdirvfs,
548 547 hgvfs=hgvfs,
549 548 store=store,
550 549 storevfs=storevfs,
551 550 storeoptions=storevfs.options,
552 551 cachevfs=cachevfs,
553 552 extensionmodulenames=extensionmodulenames,
554 553 extrastate=extrastate,
555 554 baseclasses=bases)
556 555
557 556 if not isinstance(typ, type):
558 557 raise error.ProgrammingError('unable to construct type for %s' %
559 558 iface)
560 559
561 560 bases.append(typ)
562 561
563 562 # type() allows you to use characters in type names that wouldn't be
564 563 # recognized as Python symbols in source code. We abuse that to add
565 564 # rich information about our constructed repo.
566 565 name = pycompat.sysstr(b'derivedrepo:%s<%s>' % (
567 566 wdirvfs.base,
568 567 b','.join(sorted(requirements))))
569 568
570 569 cls = type(name, tuple(bases), {})
571 570
572 571 return cls(
573 572 baseui=baseui,
574 573 ui=ui,
575 574 origroot=path,
576 575 wdirvfs=wdirvfs,
577 576 hgvfs=hgvfs,
578 577 requirements=requirements,
579 578 supportedrequirements=supportedrequirements,
580 579 sharedpath=storebasepath,
581 580 store=store,
582 581 cachevfs=cachevfs,
583 582 features=features,
584 583 intents=intents)
585 584
586 585 def afterhgrcload(ui, wdirvfs, hgvfs, requirements):
587 586 """Perform additional actions after .hg/hgrc is loaded.
588 587
589 588 This function is called during repository loading immediately after
590 589 the .hg/hgrc file is loaded and before per-repo extensions are loaded.
591 590
592 591 The function can be used to validate configs, automatically add
593 592 options (including extensions) based on requirements, etc.
594 593 """
595 594
596 595 # Map of requirements to list of extensions to load automatically when
597 596 # requirement is present.
598 597 autoextensions = {
599 598 b'largefiles': [b'largefiles'],
600 599 b'lfs': [b'lfs'],
601 600 }
602 601
603 602 for requirement, names in sorted(autoextensions.items()):
604 603 if requirement not in requirements:
605 604 continue
606 605
607 606 for name in names:
608 607 if not ui.hasconfig(b'extensions', name):
609 608 ui.setconfig(b'extensions', name, b'', source='autoload')
610 609
611 610 def gathersupportedrequirements(ui):
612 611 """Determine the complete set of recognized requirements."""
613 612 # Start with all requirements supported by this file.
614 613 supported = set(localrepository._basesupported)
615 614
616 615 # Execute ``featuresetupfuncs`` entries if they belong to an extension
617 616 # relevant to this ui instance.
618 617 modules = {m.__name__ for n, m in extensions.extensions(ui)}
619 618
620 619 for fn in featuresetupfuncs:
621 620 if fn.__module__ in modules:
622 621 fn(ui, supported)
623 622
624 623 # Add derived requirements from registered compression engines.
625 624 for name in util.compengines:
626 625 engine = util.compengines[name]
627 626 if engine.revlogheader():
628 627 supported.add(b'exp-compression-%s' % name)
629 628
630 629 return supported
631 630
632 631 def ensurerequirementsrecognized(requirements, supported):
633 632 """Validate that a set of local requirements is recognized.
634 633
635 634 Receives a set of requirements. Raises an ``error.RepoError`` if there
636 635 exists any requirement in that set that currently loaded code doesn't
637 636 recognize.
638 637
639 638 Returns a set of supported requirements.
640 639 """
641 640 missing = set()
642 641
643 642 for requirement in requirements:
644 643 if requirement in supported:
645 644 continue
646 645
647 646 if not requirement or not requirement[0:1].isalnum():
648 647 raise error.RequirementError(_(b'.hg/requires file is corrupt'))
649 648
650 649 missing.add(requirement)
651 650
652 651 if missing:
653 652 raise error.RequirementError(
654 653 _(b'repository requires features unknown to this Mercurial: %s') %
655 654 b' '.join(sorted(missing)),
656 655 hint=_(b'see https://mercurial-scm.org/wiki/MissingRequirement '
657 656 b'for more information'))
658 657
659 658 def ensurerequirementscompatible(ui, requirements):
660 659 """Validates that a set of recognized requirements is mutually compatible.
661 660
662 661 Some requirements may not be compatible with others or require
663 662 config options that aren't enabled. This function is called during
664 663 repository opening to ensure that the set of requirements needed
665 664 to open a repository is sane and compatible with config options.
666 665
667 666 Extensions can monkeypatch this function to perform additional
668 667 checking.
669 668
670 669 ``error.RepoError`` should be raised on failure.
671 670 """
672 671 if b'exp-sparse' in requirements and not sparse.enabled:
673 672 raise error.RepoError(_(b'repository is using sparse feature but '
674 673 b'sparse is not enabled; enable the '
675 674 b'"sparse" extensions to access'))
676 675
677 676 def makestore(requirements, path, vfstype):
678 677 """Construct a storage object for a repository."""
679 678 if b'store' in requirements:
680 679 if b'fncache' in requirements:
681 680 return storemod.fncachestore(path, vfstype,
682 681 b'dotencode' in requirements)
683 682
684 683 return storemod.encodedstore(path, vfstype)
685 684
686 685 return storemod.basicstore(path, vfstype)
687 686
688 687 def resolvestorevfsoptions(ui, requirements, features):
689 688 """Resolve the options to pass to the store vfs opener.
690 689
691 690 The returned dict is used to influence behavior of the storage layer.
692 691 """
693 692 options = {}
694 693
695 694 if b'treemanifest' in requirements:
696 695 options[b'treemanifest'] = True
697 696
698 697 # experimental config: format.manifestcachesize
699 698 manifestcachesize = ui.configint(b'format', b'manifestcachesize')
700 699 if manifestcachesize is not None:
701 700 options[b'manifestcachesize'] = manifestcachesize
702 701
703 702 # In the absence of another requirement superseding a revlog-related
704 703 # requirement, we have to assume the repo is using revlog version 0.
705 704 # This revlog format is super old and we don't bother trying to parse
706 705 # opener options for it because those options wouldn't do anything
707 706 # meaningful on such old repos.
708 707 if b'revlogv1' in requirements or REVLOGV2_REQUIREMENT in requirements:
709 708 options.update(resolverevlogstorevfsoptions(ui, requirements, features))
710 709
711 710 return options
712 711
713 712 def resolverevlogstorevfsoptions(ui, requirements, features):
714 713 """Resolve opener options specific to revlogs."""
715 714
716 715 options = {}
717 716 options[b'flagprocessors'] = {}
718 717
719 718 if b'revlogv1' in requirements:
720 719 options[b'revlogv1'] = True
721 720 if REVLOGV2_REQUIREMENT in requirements:
722 721 options[b'revlogv2'] = True
723 722
724 723 if b'generaldelta' in requirements:
725 724 options[b'generaldelta'] = True
726 725
727 726 # experimental config: format.chunkcachesize
728 727 chunkcachesize = ui.configint(b'format', b'chunkcachesize')
729 728 if chunkcachesize is not None:
730 729 options[b'chunkcachesize'] = chunkcachesize
731 730
732 731 deltabothparents = ui.configbool(b'storage',
733 732 b'revlog.optimize-delta-parent-choice')
734 733 options[b'deltabothparents'] = deltabothparents
735 734
736 735 options[b'lazydeltabase'] = not scmutil.gddeltaconfig(ui)
737 736
738 737 chainspan = ui.configbytes(b'experimental', b'maxdeltachainspan')
739 738 if 0 <= chainspan:
740 739 options[b'maxdeltachainspan'] = chainspan
741 740
742 741 mmapindexthreshold = ui.configbytes(b'experimental',
743 742 b'mmapindexthreshold')
744 743 if mmapindexthreshold is not None:
745 744 options[b'mmapindexthreshold'] = mmapindexthreshold
746 745
747 746 withsparseread = ui.configbool(b'experimental', b'sparse-read')
748 747 srdensitythres = float(ui.config(b'experimental',
749 748 b'sparse-read.density-threshold'))
750 749 srmingapsize = ui.configbytes(b'experimental',
751 750 b'sparse-read.min-gap-size')
752 751 options[b'with-sparse-read'] = withsparseread
753 752 options[b'sparse-read-density-threshold'] = srdensitythres
754 753 options[b'sparse-read-min-gap-size'] = srmingapsize
755 754
756 755 sparserevlog = SPARSEREVLOG_REQUIREMENT in requirements
757 756 options[b'sparse-revlog'] = sparserevlog
758 757 if sparserevlog:
759 758 options[b'generaldelta'] = True
760 759
761 760 maxchainlen = None
762 761 if sparserevlog:
763 762 maxchainlen = revlogconst.SPARSE_REVLOG_MAX_CHAIN_LENGTH
764 763 # experimental config: format.maxchainlen
765 764 maxchainlen = ui.configint(b'format', b'maxchainlen', maxchainlen)
766 765 if maxchainlen is not None:
767 766 options[b'maxchainlen'] = maxchainlen
768 767
769 768 for r in requirements:
770 769 if r.startswith(b'exp-compression-'):
771 770 options[b'compengine'] = r[len(b'exp-compression-'):]
772 771
773 772 if repository.NARROW_REQUIREMENT in requirements:
774 773 options[b'enableellipsis'] = True
775 774
776 775 return options
777 776
778 777 def makemain(**kwargs):
779 778 """Produce a type conforming to ``ilocalrepositorymain``."""
780 779 return localrepository
781 780
782 781 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
783 782 class revlogfilestorage(object):
784 783 """File storage when using revlogs."""
785 784
786 785 def file(self, path):
787 786 if path[0] == b'/':
788 787 path = path[1:]
789 788
790 789 return filelog.filelog(self.svfs, path)
791 790
792 791 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
793 792 class revlognarrowfilestorage(object):
794 793 """File storage when using revlogs and narrow files."""
795 794
796 795 def file(self, path):
797 796 if path[0] == b'/':
798 797 path = path[1:]
799 798
800 799 return filelog.narrowfilelog(self.svfs, path, self.narrowmatch())
801 800
802 801 def makefilestorage(requirements, features, **kwargs):
803 802 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
804 803 features.add(repository.REPO_FEATURE_REVLOG_FILE_STORAGE)
805 804 features.add(repository.REPO_FEATURE_STREAM_CLONE)
806 805
807 806 if repository.NARROW_REQUIREMENT in requirements:
808 807 return revlognarrowfilestorage
809 808 else:
810 809 return revlogfilestorage
811 810
812 811 # List of repository interfaces and factory functions for them. Each
813 812 # will be called in order during ``makelocalrepository()`` to iteratively
814 813 # derive the final type for a local repository instance. We capture the
815 814 # function as a lambda so we don't hold a reference and the module-level
816 815 # functions can be wrapped.
817 816 REPO_INTERFACES = [
818 817 (repository.ilocalrepositorymain, lambda: makemain),
819 818 (repository.ilocalrepositoryfilestorage, lambda: makefilestorage),
820 819 ]
821 820
822 821 @interfaceutil.implementer(repository.ilocalrepositorymain)
823 822 class localrepository(object):
824 823 """Main class for representing local repositories.
825 824
826 825 All local repositories are instances of this class.
827 826
828 827 Constructed on its own, instances of this class are not usable as
829 828 repository objects. To obtain a usable repository object, call
830 829 ``hg.repository()``, ``localrepo.instance()``, or
831 830 ``localrepo.makelocalrepository()``. The latter is the lowest-level.
832 831 ``instance()`` adds support for creating new repositories.
833 832 ``hg.repository()`` adds more extension integration, including calling
834 833 ``reposetup()``. Generally speaking, ``hg.repository()`` should be
835 834 used.
836 835 """
837 836
838 837 # obsolete experimental requirements:
839 838 # - manifestv2: An experimental new manifest format that allowed
840 839 # for stem compression of long paths. Experiment ended up not
841 840 # being successful (repository sizes went up due to worse delta
842 841 # chains), and the code was deleted in 4.6.
843 842 supportedformats = {
844 843 'revlogv1',
845 844 'generaldelta',
846 845 'treemanifest',
847 846 REVLOGV2_REQUIREMENT,
848 847 SPARSEREVLOG_REQUIREMENT,
849 848 }
850 849 _basesupported = supportedformats | {
851 850 'store',
852 851 'fncache',
853 852 'shared',
854 853 'relshared',
855 854 'dotencode',
856 855 'exp-sparse',
857 856 'internal-phase'
858 857 }
859 858
860 859 # list of prefix for file which can be written without 'wlock'
861 860 # Extensions should extend this list when needed
862 861 _wlockfreeprefix = {
863 862 # We migh consider requiring 'wlock' for the next
864 863 # two, but pretty much all the existing code assume
865 864 # wlock is not needed so we keep them excluded for
866 865 # now.
867 866 'hgrc',
868 867 'requires',
869 868 # XXX cache is a complicatged business someone
870 869 # should investigate this in depth at some point
871 870 'cache/',
872 871 # XXX shouldn't be dirstate covered by the wlock?
873 872 'dirstate',
874 873 # XXX bisect was still a bit too messy at the time
875 874 # this changeset was introduced. Someone should fix
876 875 # the remainig bit and drop this line
877 876 'bisect.state',
878 877 }
879 878
880 879 def __init__(self, baseui, ui, origroot, wdirvfs, hgvfs, requirements,
881 880 supportedrequirements, sharedpath, store, cachevfs,
882 881 features, intents=None):
883 882 """Create a new local repository instance.
884 883
885 884 Most callers should use ``hg.repository()``, ``localrepo.instance()``,
886 885 or ``localrepo.makelocalrepository()`` for obtaining a new repository
887 886 object.
888 887
889 888 Arguments:
890 889
891 890 baseui
892 891 ``ui.ui`` instance that ``ui`` argument was based off of.
893 892
894 893 ui
895 894 ``ui.ui`` instance for use by the repository.
896 895
897 896 origroot
898 897 ``bytes`` path to working directory root of this repository.
899 898
900 899 wdirvfs
901 900 ``vfs.vfs`` rooted at the working directory.
902 901
903 902 hgvfs
904 903 ``vfs.vfs`` rooted at .hg/
905 904
906 905 requirements
907 906 ``set`` of bytestrings representing repository opening requirements.
908 907
909 908 supportedrequirements
910 909 ``set`` of bytestrings representing repository requirements that we
911 910 know how to open. May be a supetset of ``requirements``.
912 911
913 912 sharedpath
914 913 ``bytes`` Defining path to storage base directory. Points to a
915 914 ``.hg/`` directory somewhere.
916 915
917 916 store
918 917 ``store.basicstore`` (or derived) instance providing access to
919 918 versioned storage.
920 919
921 920 cachevfs
922 921 ``vfs.vfs`` used for cache files.
923 922
924 923 features
925 924 ``set`` of bytestrings defining features/capabilities of this
926 925 instance.
927 926
928 927 intents
929 928 ``set`` of system strings indicating what this repo will be used
930 929 for.
931 930 """
932 931 self.baseui = baseui
933 932 self.ui = ui
934 933 self.origroot = origroot
935 934 # vfs rooted at working directory.
936 935 self.wvfs = wdirvfs
937 936 self.root = wdirvfs.base
938 937 # vfs rooted at .hg/. Used to access most non-store paths.
939 938 self.vfs = hgvfs
940 939 self.path = hgvfs.base
941 940 self.requirements = requirements
942 941 self.supported = supportedrequirements
943 942 self.sharedpath = sharedpath
944 943 self.store = store
945 944 self.cachevfs = cachevfs
946 945 self.features = features
947 946
948 947 self.filtername = None
949 948
950 949 if (self.ui.configbool('devel', 'all-warnings') or
951 950 self.ui.configbool('devel', 'check-locks')):
952 951 self.vfs.audit = self._getvfsward(self.vfs.audit)
953 952 # A list of callback to shape the phase if no data were found.
954 953 # Callback are in the form: func(repo, roots) --> processed root.
955 954 # This list it to be filled by extension during repo setup
956 955 self._phasedefaults = []
957 956
958 957 color.setup(self.ui)
959 958
960 959 self.spath = self.store.path
961 960 self.svfs = self.store.vfs
962 961 self.sjoin = self.store.join
963 962 if (self.ui.configbool('devel', 'all-warnings') or
964 963 self.ui.configbool('devel', 'check-locks')):
965 964 if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs
966 965 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
967 966 else: # standard vfs
968 967 self.svfs.audit = self._getsvfsward(self.svfs.audit)
969 968
970 969 self._dirstatevalidatewarned = False
971 970
972 971 self._branchcaches = {}
973 972 self._revbranchcache = None
974 973 self._filterpats = {}
975 974 self._datafilters = {}
976 975 self._transref = self._lockref = self._wlockref = None
977 976
978 977 # A cache for various files under .hg/ that tracks file changes,
979 978 # (used by the filecache decorator)
980 979 #
981 980 # Maps a property name to its util.filecacheentry
982 981 self._filecache = {}
983 982
984 983 # hold sets of revision to be filtered
985 984 # should be cleared when something might have changed the filter value:
986 985 # - new changesets,
987 986 # - phase change,
988 987 # - new obsolescence marker,
989 988 # - working directory parent change,
990 989 # - bookmark changes
991 990 self.filteredrevcache = {}
992 991
993 992 # post-dirstate-status hooks
994 993 self._postdsstatus = []
995 994
996 995 # generic mapping between names and nodes
997 996 self.names = namespaces.namespaces()
998 997
999 998 # Key to signature value.
1000 999 self._sparsesignaturecache = {}
1001 1000 # Signature to cached matcher instance.
1002 1001 self._sparsematchercache = {}
1003 1002
1004 1003 def _getvfsward(self, origfunc):
1005 1004 """build a ward for self.vfs"""
1006 1005 rref = weakref.ref(self)
1007 1006 def checkvfs(path, mode=None):
1008 1007 ret = origfunc(path, mode=mode)
1009 1008 repo = rref()
1010 1009 if (repo is None
1011 1010 or not util.safehasattr(repo, '_wlockref')
1012 1011 or not util.safehasattr(repo, '_lockref')):
1013 1012 return
1014 1013 if mode in (None, 'r', 'rb'):
1015 1014 return
1016 1015 if path.startswith(repo.path):
1017 1016 # truncate name relative to the repository (.hg)
1018 1017 path = path[len(repo.path) + 1:]
1019 1018 if path.startswith('cache/'):
1020 1019 msg = 'accessing cache with vfs instead of cachevfs: "%s"'
1021 1020 repo.ui.develwarn(msg % path, stacklevel=2, config="cache-vfs")
1022 1021 if path.startswith('journal.'):
1023 1022 # journal is covered by 'lock'
1024 1023 if repo._currentlock(repo._lockref) is None:
1025 1024 repo.ui.develwarn('write with no lock: "%s"' % path,
1026 1025 stacklevel=2, config='check-locks')
1027 1026 elif repo._currentlock(repo._wlockref) is None:
1028 1027 # rest of vfs files are covered by 'wlock'
1029 1028 #
1030 1029 # exclude special files
1031 1030 for prefix in self._wlockfreeprefix:
1032 1031 if path.startswith(prefix):
1033 1032 return
1034 1033 repo.ui.develwarn('write with no wlock: "%s"' % path,
1035 1034 stacklevel=2, config='check-locks')
1036 1035 return ret
1037 1036 return checkvfs
1038 1037
1039 1038 def _getsvfsward(self, origfunc):
1040 1039 """build a ward for self.svfs"""
1041 1040 rref = weakref.ref(self)
1042 1041 def checksvfs(path, mode=None):
1043 1042 ret = origfunc(path, mode=mode)
1044 1043 repo = rref()
1045 1044 if repo is None or not util.safehasattr(repo, '_lockref'):
1046 1045 return
1047 1046 if mode in (None, 'r', 'rb'):
1048 1047 return
1049 1048 if path.startswith(repo.sharedpath):
1050 1049 # truncate name relative to the repository (.hg)
1051 1050 path = path[len(repo.sharedpath) + 1:]
1052 1051 if repo._currentlock(repo._lockref) is None:
1053 1052 repo.ui.develwarn('write with no lock: "%s"' % path,
1054 1053 stacklevel=3)
1055 1054 return ret
1056 1055 return checksvfs
1057 1056
1058 1057 def close(self):
1059 1058 self._writecaches()
1060 1059
1061 1060 def _writecaches(self):
1062 1061 if self._revbranchcache:
1063 1062 self._revbranchcache.write()
1064 1063
1065 1064 def _restrictcapabilities(self, caps):
1066 1065 if self.ui.configbool('experimental', 'bundle2-advertise'):
1067 1066 caps = set(caps)
1068 1067 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self,
1069 1068 role='client'))
1070 1069 caps.add('bundle2=' + urlreq.quote(capsblob))
1071 1070 return caps
1072 1071
1073 1072 def _writerequirements(self):
1074 1073 scmutil.writerequires(self.vfs, self.requirements)
1075 1074
1076 1075 # Don't cache auditor/nofsauditor, or you'll end up with reference cycle:
1077 1076 # self -> auditor -> self._checknested -> self
1078 1077
1079 1078 @property
1080 1079 def auditor(self):
1081 1080 # This is only used by context.workingctx.match in order to
1082 1081 # detect files in subrepos.
1083 1082 return pathutil.pathauditor(self.root, callback=self._checknested)
1084 1083
1085 1084 @property
1086 1085 def nofsauditor(self):
1087 1086 # This is only used by context.basectx.match in order to detect
1088 1087 # files in subrepos.
1089 1088 return pathutil.pathauditor(self.root, callback=self._checknested,
1090 1089 realfs=False, cached=True)
1091 1090
1092 1091 def _checknested(self, path):
1093 1092 """Determine if path is a legal nested repository."""
1094 1093 if not path.startswith(self.root):
1095 1094 return False
1096 1095 subpath = path[len(self.root) + 1:]
1097 1096 normsubpath = util.pconvert(subpath)
1098 1097
1099 1098 # XXX: Checking against the current working copy is wrong in
1100 1099 # the sense that it can reject things like
1101 1100 #
1102 1101 # $ hg cat -r 10 sub/x.txt
1103 1102 #
1104 1103 # if sub/ is no longer a subrepository in the working copy
1105 1104 # parent revision.
1106 1105 #
1107 1106 # However, it can of course also allow things that would have
1108 1107 # been rejected before, such as the above cat command if sub/
1109 1108 # is a subrepository now, but was a normal directory before.
1110 1109 # The old path auditor would have rejected by mistake since it
1111 1110 # panics when it sees sub/.hg/.
1112 1111 #
1113 1112 # All in all, checking against the working copy seems sensible
1114 1113 # since we want to prevent access to nested repositories on
1115 1114 # the filesystem *now*.
1116 1115 ctx = self[None]
1117 1116 parts = util.splitpath(subpath)
1118 1117 while parts:
1119 1118 prefix = '/'.join(parts)
1120 1119 if prefix in ctx.substate:
1121 1120 if prefix == normsubpath:
1122 1121 return True
1123 1122 else:
1124 1123 sub = ctx.sub(prefix)
1125 1124 return sub.checknested(subpath[len(prefix) + 1:])
1126 1125 else:
1127 1126 parts.pop()
1128 1127 return False
1129 1128
1130 1129 def peer(self):
1131 1130 return localpeer(self) # not cached to avoid reference cycle
1132 1131
1133 1132 def unfiltered(self):
1134 1133 """Return unfiltered version of the repository
1135 1134
1136 1135 Intended to be overwritten by filtered repo."""
1137 1136 return self
1138 1137
1139 1138 def filtered(self, name, visibilityexceptions=None):
1140 1139 """Return a filtered version of a repository"""
1141 1140 cls = repoview.newtype(self.unfiltered().__class__)
1142 1141 return cls(self, name, visibilityexceptions)
1143 1142
1144 1143 @repofilecache('bookmarks', 'bookmarks.current')
1145 1144 def _bookmarks(self):
1146 1145 return bookmarks.bmstore(self)
1147 1146
1148 1147 @property
1149 1148 def _activebookmark(self):
1150 1149 return self._bookmarks.active
1151 1150
1152 1151 # _phasesets depend on changelog. what we need is to call
1153 1152 # _phasecache.invalidate() if '00changelog.i' was changed, but it
1154 1153 # can't be easily expressed in filecache mechanism.
1155 1154 @storecache('phaseroots', '00changelog.i')
1156 1155 def _phasecache(self):
1157 1156 return phases.phasecache(self, self._phasedefaults)
1158 1157
1159 1158 @storecache('obsstore')
1160 1159 def obsstore(self):
1161 1160 return obsolete.makestore(self.ui, self)
1162 1161
1163 1162 @storecache('00changelog.i')
1164 1163 def changelog(self):
1165 1164 return changelog.changelog(self.svfs,
1166 1165 trypending=txnutil.mayhavepending(self.root))
1167 1166
1168 1167 @storecache('00manifest.i')
1169 1168 def manifestlog(self):
1170 1169 rootstore = manifest.manifestrevlog(self.svfs)
1171 1170 return manifest.manifestlog(self.svfs, self, rootstore)
1172 1171
1173 1172 @repofilecache('dirstate')
1174 1173 def dirstate(self):
1175 1174 return self._makedirstate()
1176 1175
1177 1176 def _makedirstate(self):
1178 1177 """Extension point for wrapping the dirstate per-repo."""
1179 1178 sparsematchfn = lambda: sparse.matcher(self)
1180 1179
1181 1180 return dirstate.dirstate(self.vfs, self.ui, self.root,
1182 1181 self._dirstatevalidate, sparsematchfn)
1183 1182
1184 1183 def _dirstatevalidate(self, node):
1185 1184 try:
1186 1185 self.changelog.rev(node)
1187 1186 return node
1188 1187 except error.LookupError:
1189 1188 if not self._dirstatevalidatewarned:
1190 1189 self._dirstatevalidatewarned = True
1191 1190 self.ui.warn(_("warning: ignoring unknown"
1192 1191 " working parent %s!\n") % short(node))
1193 1192 return nullid
1194 1193
1195 1194 @storecache(narrowspec.FILENAME)
1196 1195 def narrowpats(self):
1197 1196 """matcher patterns for this repository's narrowspec
1198 1197
1199 1198 A tuple of (includes, excludes).
1200 1199 """
1201 1200 return narrowspec.load(self)
1202 1201
1203 1202 @storecache(narrowspec.FILENAME)
1204 1203 def _narrowmatch(self):
1205 1204 if repository.NARROW_REQUIREMENT not in self.requirements:
1206 1205 return matchmod.always(self.root, '')
1207 1206 include, exclude = self.narrowpats
1208 1207 return narrowspec.match(self.root, include=include, exclude=exclude)
1209 1208
1210 1209 def narrowmatch(self, match=None, includeexact=False):
1211 1210 """matcher corresponding the the repo's narrowspec
1212 1211
1213 1212 If `match` is given, then that will be intersected with the narrow
1214 1213 matcher.
1215 1214
1216 1215 If `includeexact` is True, then any exact matches from `match` will
1217 1216 be included even if they're outside the narrowspec.
1218 1217 """
1219 1218 if match:
1220 1219 if includeexact and not self._narrowmatch.always():
1221 1220 # do not exclude explicitly-specified paths so that they can
1222 1221 # be warned later on
1223 1222 em = matchmod.exact(match._root, match._cwd, match.files())
1224 1223 nm = matchmod.unionmatcher([self._narrowmatch, em])
1225 1224 return matchmod.intersectmatchers(match, nm)
1226 1225 return matchmod.intersectmatchers(match, self._narrowmatch)
1227 1226 return self._narrowmatch
1228 1227
1229 1228 def setnarrowpats(self, newincludes, newexcludes):
1230 1229 narrowspec.save(self, newincludes, newexcludes)
1231 1230 self.invalidate(clearfilecache=True)
1232 1231
1233 1232 def __getitem__(self, changeid):
1234 1233 if changeid is None:
1235 1234 return context.workingctx(self)
1236 1235 if isinstance(changeid, context.basectx):
1237 1236 return changeid
1238 1237 if isinstance(changeid, slice):
1239 1238 # wdirrev isn't contiguous so the slice shouldn't include it
1240 1239 return [self[i]
1241 1240 for i in pycompat.xrange(*changeid.indices(len(self)))
1242 1241 if i not in self.changelog.filteredrevs]
1243 1242 try:
1244 1243 if isinstance(changeid, int):
1245 1244 node = self.changelog.node(changeid)
1246 1245 rev = changeid
1247 1246 elif changeid == 'null':
1248 1247 node = nullid
1249 1248 rev = nullrev
1250 1249 elif changeid == 'tip':
1251 1250 node = self.changelog.tip()
1252 1251 rev = self.changelog.rev(node)
1253 1252 elif changeid == '.':
1254 1253 # this is a hack to delay/avoid loading obsmarkers
1255 1254 # when we know that '.' won't be hidden
1256 1255 node = self.dirstate.p1()
1257 1256 rev = self.unfiltered().changelog.rev(node)
1258 1257 elif len(changeid) == 20:
1259 1258 try:
1260 1259 node = changeid
1261 1260 rev = self.changelog.rev(changeid)
1262 1261 except error.FilteredLookupError:
1263 1262 changeid = hex(changeid) # for the error message
1264 1263 raise
1265 1264 except LookupError:
1266 1265 # check if it might have come from damaged dirstate
1267 1266 #
1268 1267 # XXX we could avoid the unfiltered if we had a recognizable
1269 1268 # exception for filtered changeset access
1270 1269 if (self.local()
1271 1270 and changeid in self.unfiltered().dirstate.parents()):
1272 1271 msg = _("working directory has unknown parent '%s'!")
1273 1272 raise error.Abort(msg % short(changeid))
1274 1273 changeid = hex(changeid) # for the error message
1275 1274 raise
1276 1275
1277 1276 elif len(changeid) == 40:
1278 1277 node = bin(changeid)
1279 1278 rev = self.changelog.rev(node)
1280 1279 else:
1281 1280 raise error.ProgrammingError(
1282 1281 "unsupported changeid '%s' of type %s" %
1283 1282 (changeid, type(changeid)))
1284 1283
1285 1284 return context.changectx(self, rev, node)
1286 1285
1287 1286 except (error.FilteredIndexError, error.FilteredLookupError):
1288 1287 raise error.FilteredRepoLookupError(_("filtered revision '%s'")
1289 1288 % pycompat.bytestr(changeid))
1290 1289 except (IndexError, LookupError):
1291 1290 raise error.RepoLookupError(
1292 1291 _("unknown revision '%s'") % pycompat.bytestr(changeid))
1293 1292 except error.WdirUnsupported:
1294 1293 return context.workingctx(self)
1295 1294
1296 1295 def __contains__(self, changeid):
1297 1296 """True if the given changeid exists
1298 1297
1299 1298 error.AmbiguousPrefixLookupError is raised if an ambiguous node
1300 1299 specified.
1301 1300 """
1302 1301 try:
1303 1302 self[changeid]
1304 1303 return True
1305 1304 except error.RepoLookupError:
1306 1305 return False
1307 1306
1308 1307 def __nonzero__(self):
1309 1308 return True
1310 1309
1311 1310 __bool__ = __nonzero__
1312 1311
1313 1312 def __len__(self):
1314 1313 # no need to pay the cost of repoview.changelog
1315 1314 unfi = self.unfiltered()
1316 1315 return len(unfi.changelog)
1317 1316
1318 1317 def __iter__(self):
1319 1318 return iter(self.changelog)
1320 1319
1321 1320 def revs(self, expr, *args):
1322 1321 '''Find revisions matching a revset.
1323 1322
1324 1323 The revset is specified as a string ``expr`` that may contain
1325 1324 %-formatting to escape certain types. See ``revsetlang.formatspec``.
1326 1325
1327 1326 Revset aliases from the configuration are not expanded. To expand
1328 1327 user aliases, consider calling ``scmutil.revrange()`` or
1329 1328 ``repo.anyrevs([expr], user=True)``.
1330 1329
1331 1330 Returns a revset.abstractsmartset, which is a list-like interface
1332 1331 that contains integer revisions.
1333 1332 '''
1334 1333 expr = revsetlang.formatspec(expr, *args)
1335 1334 m = revset.match(None, expr)
1336 1335 return m(self)
1337 1336
1338 1337 def set(self, expr, *args):
1339 1338 '''Find revisions matching a revset and emit changectx instances.
1340 1339
1341 1340 This is a convenience wrapper around ``revs()`` that iterates the
1342 1341 result and is a generator of changectx instances.
1343 1342
1344 1343 Revset aliases from the configuration are not expanded. To expand
1345 1344 user aliases, consider calling ``scmutil.revrange()``.
1346 1345 '''
1347 1346 for r in self.revs(expr, *args):
1348 1347 yield self[r]
1349 1348
1350 1349 def anyrevs(self, specs, user=False, localalias=None):
1351 1350 '''Find revisions matching one of the given revsets.
1352 1351
1353 1352 Revset aliases from the configuration are not expanded by default. To
1354 1353 expand user aliases, specify ``user=True``. To provide some local
1355 1354 definitions overriding user aliases, set ``localalias`` to
1356 1355 ``{name: definitionstring}``.
1357 1356 '''
1358 1357 if user:
1359 1358 m = revset.matchany(self.ui, specs,
1360 1359 lookup=revset.lookupfn(self),
1361 1360 localalias=localalias)
1362 1361 else:
1363 1362 m = revset.matchany(None, specs, localalias=localalias)
1364 1363 return m(self)
1365 1364
1366 1365 def url(self):
1367 1366 return 'file:' + self.root
1368 1367
1369 1368 def hook(self, name, throw=False, **args):
1370 1369 """Call a hook, passing this repo instance.
1371 1370
1372 1371 This a convenience method to aid invoking hooks. Extensions likely
1373 1372 won't call this unless they have registered a custom hook or are
1374 1373 replacing code that is expected to call a hook.
1375 1374 """
1376 1375 return hook.hook(self.ui, self, name, throw, **args)
1377 1376
1378 1377 @filteredpropertycache
1379 1378 def _tagscache(self):
1380 1379 '''Returns a tagscache object that contains various tags related
1381 1380 caches.'''
1382 1381
1383 1382 # This simplifies its cache management by having one decorated
1384 1383 # function (this one) and the rest simply fetch things from it.
1385 1384 class tagscache(object):
1386 1385 def __init__(self):
1387 1386 # These two define the set of tags for this repository. tags
1388 1387 # maps tag name to node; tagtypes maps tag name to 'global' or
1389 1388 # 'local'. (Global tags are defined by .hgtags across all
1390 1389 # heads, and local tags are defined in .hg/localtags.)
1391 1390 # They constitute the in-memory cache of tags.
1392 1391 self.tags = self.tagtypes = None
1393 1392
1394 1393 self.nodetagscache = self.tagslist = None
1395 1394
1396 1395 cache = tagscache()
1397 1396 cache.tags, cache.tagtypes = self._findtags()
1398 1397
1399 1398 return cache
1400 1399
1401 1400 def tags(self):
1402 1401 '''return a mapping of tag to node'''
1403 1402 t = {}
1404 1403 if self.changelog.filteredrevs:
1405 1404 tags, tt = self._findtags()
1406 1405 else:
1407 1406 tags = self._tagscache.tags
1408 1407 for k, v in tags.iteritems():
1409 1408 try:
1410 1409 # ignore tags to unknown nodes
1411 1410 self.changelog.rev(v)
1412 1411 t[k] = v
1413 1412 except (error.LookupError, ValueError):
1414 1413 pass
1415 1414 return t
1416 1415
1417 1416 def _findtags(self):
1418 1417 '''Do the hard work of finding tags. Return a pair of dicts
1419 1418 (tags, tagtypes) where tags maps tag name to node, and tagtypes
1420 1419 maps tag name to a string like \'global\' or \'local\'.
1421 1420 Subclasses or extensions are free to add their own tags, but
1422 1421 should be aware that the returned dicts will be retained for the
1423 1422 duration of the localrepo object.'''
1424 1423
1425 1424 # XXX what tagtype should subclasses/extensions use? Currently
1426 1425 # mq and bookmarks add tags, but do not set the tagtype at all.
1427 1426 # Should each extension invent its own tag type? Should there
1428 1427 # be one tagtype for all such "virtual" tags? Or is the status
1429 1428 # quo fine?
1430 1429
1431 1430
1432 1431 # map tag name to (node, hist)
1433 1432 alltags = tagsmod.findglobaltags(self.ui, self)
1434 1433 # map tag name to tag type
1435 1434 tagtypes = dict((tag, 'global') for tag in alltags)
1436 1435
1437 1436 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
1438 1437
1439 1438 # Build the return dicts. Have to re-encode tag names because
1440 1439 # the tags module always uses UTF-8 (in order not to lose info
1441 1440 # writing to the cache), but the rest of Mercurial wants them in
1442 1441 # local encoding.
1443 1442 tags = {}
1444 1443 for (name, (node, hist)) in alltags.iteritems():
1445 1444 if node != nullid:
1446 1445 tags[encoding.tolocal(name)] = node
1447 1446 tags['tip'] = self.changelog.tip()
1448 1447 tagtypes = dict([(encoding.tolocal(name), value)
1449 1448 for (name, value) in tagtypes.iteritems()])
1450 1449 return (tags, tagtypes)
1451 1450
1452 1451 def tagtype(self, tagname):
1453 1452 '''
1454 1453 return the type of the given tag. result can be:
1455 1454
1456 1455 'local' : a local tag
1457 1456 'global' : a global tag
1458 1457 None : tag does not exist
1459 1458 '''
1460 1459
1461 1460 return self._tagscache.tagtypes.get(tagname)
1462 1461
1463 1462 def tagslist(self):
1464 1463 '''return a list of tags ordered by revision'''
1465 1464 if not self._tagscache.tagslist:
1466 1465 l = []
1467 1466 for t, n in self.tags().iteritems():
1468 1467 l.append((self.changelog.rev(n), t, n))
1469 1468 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
1470 1469
1471 1470 return self._tagscache.tagslist
1472 1471
1473 1472 def nodetags(self, node):
1474 1473 '''return the tags associated with a node'''
1475 1474 if not self._tagscache.nodetagscache:
1476 1475 nodetagscache = {}
1477 1476 for t, n in self._tagscache.tags.iteritems():
1478 1477 nodetagscache.setdefault(n, []).append(t)
1479 1478 for tags in nodetagscache.itervalues():
1480 1479 tags.sort()
1481 1480 self._tagscache.nodetagscache = nodetagscache
1482 1481 return self._tagscache.nodetagscache.get(node, [])
1483 1482
1484 1483 def nodebookmarks(self, node):
1485 1484 """return the list of bookmarks pointing to the specified node"""
1486 1485 return self._bookmarks.names(node)
1487 1486
1488 1487 def branchmap(self):
1489 1488 '''returns a dictionary {branch: [branchheads]} with branchheads
1490 1489 ordered by increasing revision number'''
1491 1490 branchmap.updatecache(self)
1492 1491 return self._branchcaches[self.filtername]
1493 1492
1494 1493 @unfilteredmethod
1495 1494 def revbranchcache(self):
1496 1495 if not self._revbranchcache:
1497 1496 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
1498 1497 return self._revbranchcache
1499 1498
1500 1499 def branchtip(self, branch, ignoremissing=False):
1501 1500 '''return the tip node for a given branch
1502 1501
1503 1502 If ignoremissing is True, then this method will not raise an error.
1504 1503 This is helpful for callers that only expect None for a missing branch
1505 1504 (e.g. namespace).
1506 1505
1507 1506 '''
1508 1507 try:
1509 1508 return self.branchmap().branchtip(branch)
1510 1509 except KeyError:
1511 1510 if not ignoremissing:
1512 1511 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
1513 1512 else:
1514 1513 pass
1515 1514
1516 1515 def lookup(self, key):
1517 1516 return scmutil.revsymbol(self, key).node()
1518 1517
1519 1518 def lookupbranch(self, key):
1520 1519 if key in self.branchmap():
1521 1520 return key
1522 1521
1523 1522 return scmutil.revsymbol(self, key).branch()
1524 1523
1525 1524 def known(self, nodes):
1526 1525 cl = self.changelog
1527 1526 nm = cl.nodemap
1528 1527 filtered = cl.filteredrevs
1529 1528 result = []
1530 1529 for n in nodes:
1531 1530 r = nm.get(n)
1532 1531 resp = not (r is None or r in filtered)
1533 1532 result.append(resp)
1534 1533 return result
1535 1534
1536 1535 def local(self):
1537 1536 return self
1538 1537
1539 1538 def publishing(self):
1540 1539 # it's safe (and desirable) to trust the publish flag unconditionally
1541 1540 # so that we don't finalize changes shared between users via ssh or nfs
1542 1541 return self.ui.configbool('phases', 'publish', untrusted=True)
1543 1542
1544 1543 def cancopy(self):
1545 1544 # so statichttprepo's override of local() works
1546 1545 if not self.local():
1547 1546 return False
1548 1547 if not self.publishing():
1549 1548 return True
1550 1549 # if publishing we can't copy if there is filtered content
1551 1550 return not self.filtered('visible').changelog.filteredrevs
1552 1551
1553 1552 def shared(self):
1554 1553 '''the type of shared repository (None if not shared)'''
1555 1554 if self.sharedpath != self.path:
1556 1555 return 'store'
1557 1556 return None
1558 1557
1559 1558 def wjoin(self, f, *insidef):
1560 1559 return self.vfs.reljoin(self.root, f, *insidef)
1561 1560
1562 1561 def setparents(self, p1, p2=nullid):
1563 1562 with self.dirstate.parentchange():
1564 1563 copies = self.dirstate.setparents(p1, p2)
1565 1564 pctx = self[p1]
1566 1565 if copies:
1567 1566 # Adjust copy records, the dirstate cannot do it, it
1568 1567 # requires access to parents manifests. Preserve them
1569 1568 # only for entries added to first parent.
1570 1569 for f in copies:
1571 1570 if f not in pctx and copies[f] in pctx:
1572 1571 self.dirstate.copy(copies[f], f)
1573 1572 if p2 == nullid:
1574 1573 for f, s in sorted(self.dirstate.copies().items()):
1575 1574 if f not in pctx and s not in pctx:
1576 1575 self.dirstate.copy(None, f)
1577 1576
1578 1577 def filectx(self, path, changeid=None, fileid=None, changectx=None):
1579 1578 """changeid can be a changeset revision, node, or tag.
1580 1579 fileid can be a file revision or node."""
1581 1580 return context.filectx(self, path, changeid, fileid,
1582 1581 changectx=changectx)
1583 1582
1584 1583 def getcwd(self):
1585 1584 return self.dirstate.getcwd()
1586 1585
1587 1586 def pathto(self, f, cwd=None):
1588 1587 return self.dirstate.pathto(f, cwd)
1589 1588
1590 1589 def _loadfilter(self, filter):
1591 1590 if filter not in self._filterpats:
1592 1591 l = []
1593 1592 for pat, cmd in self.ui.configitems(filter):
1594 1593 if cmd == '!':
1595 1594 continue
1596 1595 mf = matchmod.match(self.root, '', [pat])
1597 1596 fn = None
1598 1597 params = cmd
1599 1598 for name, filterfn in self._datafilters.iteritems():
1600 1599 if cmd.startswith(name):
1601 1600 fn = filterfn
1602 1601 params = cmd[len(name):].lstrip()
1603 1602 break
1604 1603 if not fn:
1605 1604 fn = lambda s, c, **kwargs: procutil.filter(s, c)
1606 1605 # Wrap old filters not supporting keyword arguments
1607 1606 if not pycompat.getargspec(fn)[2]:
1608 1607 oldfn = fn
1609 1608 fn = lambda s, c, **kwargs: oldfn(s, c)
1610 1609 l.append((mf, fn, params))
1611 1610 self._filterpats[filter] = l
1612 1611 return self._filterpats[filter]
1613 1612
1614 1613 def _filter(self, filterpats, filename, data):
1615 1614 for mf, fn, cmd in filterpats:
1616 1615 if mf(filename):
1617 1616 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
1618 1617 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1619 1618 break
1620 1619
1621 1620 return data
1622 1621
1623 1622 @unfilteredpropertycache
1624 1623 def _encodefilterpats(self):
1625 1624 return self._loadfilter('encode')
1626 1625
1627 1626 @unfilteredpropertycache
1628 1627 def _decodefilterpats(self):
1629 1628 return self._loadfilter('decode')
1630 1629
1631 1630 def adddatafilter(self, name, filter):
1632 1631 self._datafilters[name] = filter
1633 1632
1634 1633 def wread(self, filename):
1635 1634 if self.wvfs.islink(filename):
1636 1635 data = self.wvfs.readlink(filename)
1637 1636 else:
1638 1637 data = self.wvfs.read(filename)
1639 1638 return self._filter(self._encodefilterpats, filename, data)
1640 1639
1641 1640 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
1642 1641 """write ``data`` into ``filename`` in the working directory
1643 1642
1644 1643 This returns length of written (maybe decoded) data.
1645 1644 """
1646 1645 data = self._filter(self._decodefilterpats, filename, data)
1647 1646 if 'l' in flags:
1648 1647 self.wvfs.symlink(data, filename)
1649 1648 else:
1650 1649 self.wvfs.write(filename, data, backgroundclose=backgroundclose,
1651 1650 **kwargs)
1652 1651 if 'x' in flags:
1653 1652 self.wvfs.setflags(filename, False, True)
1654 1653 else:
1655 1654 self.wvfs.setflags(filename, False, False)
1656 1655 return len(data)
1657 1656
1658 1657 def wwritedata(self, filename, data):
1659 1658 return self._filter(self._decodefilterpats, filename, data)
1660 1659
1661 1660 def currenttransaction(self):
1662 1661 """return the current transaction or None if non exists"""
1663 1662 if self._transref:
1664 1663 tr = self._transref()
1665 1664 else:
1666 1665 tr = None
1667 1666
1668 1667 if tr and tr.running():
1669 1668 return tr
1670 1669 return None
1671 1670
1672 1671 def transaction(self, desc, report=None):
1673 1672 if (self.ui.configbool('devel', 'all-warnings')
1674 1673 or self.ui.configbool('devel', 'check-locks')):
1675 1674 if self._currentlock(self._lockref) is None:
1676 1675 raise error.ProgrammingError('transaction requires locking')
1677 1676 tr = self.currenttransaction()
1678 1677 if tr is not None:
1679 1678 return tr.nest(name=desc)
1680 1679
1681 1680 # abort here if the journal already exists
1682 1681 if self.svfs.exists("journal"):
1683 1682 raise error.RepoError(
1684 1683 _("abandoned transaction found"),
1685 1684 hint=_("run 'hg recover' to clean up transaction"))
1686 1685
1687 1686 idbase = "%.40f#%f" % (random.random(), time.time())
1688 1687 ha = hex(hashlib.sha1(idbase).digest())
1689 1688 txnid = 'TXN:' + ha
1690 1689 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1691 1690
1692 1691 self._writejournal(desc)
1693 1692 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1694 1693 if report:
1695 1694 rp = report
1696 1695 else:
1697 1696 rp = self.ui.warn
1698 1697 vfsmap = {'plain': self.vfs, 'store': self.svfs} # root of .hg/
1699 1698 # we must avoid cyclic reference between repo and transaction.
1700 1699 reporef = weakref.ref(self)
1701 1700 # Code to track tag movement
1702 1701 #
1703 1702 # Since tags are all handled as file content, it is actually quite hard
1704 1703 # to track these movement from a code perspective. So we fallback to a
1705 1704 # tracking at the repository level. One could envision to track changes
1706 1705 # to the '.hgtags' file through changegroup apply but that fails to
1707 1706 # cope with case where transaction expose new heads without changegroup
1708 1707 # being involved (eg: phase movement).
1709 1708 #
1710 1709 # For now, We gate the feature behind a flag since this likely comes
1711 1710 # with performance impacts. The current code run more often than needed
1712 1711 # and do not use caches as much as it could. The current focus is on
1713 1712 # the behavior of the feature so we disable it by default. The flag
1714 1713 # will be removed when we are happy with the performance impact.
1715 1714 #
1716 1715 # Once this feature is no longer experimental move the following
1717 1716 # documentation to the appropriate help section:
1718 1717 #
1719 1718 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
1720 1719 # tags (new or changed or deleted tags). In addition the details of
1721 1720 # these changes are made available in a file at:
1722 1721 # ``REPOROOT/.hg/changes/tags.changes``.
1723 1722 # Make sure you check for HG_TAG_MOVED before reading that file as it
1724 1723 # might exist from a previous transaction even if no tag were touched
1725 1724 # in this one. Changes are recorded in a line base format::
1726 1725 #
1727 1726 # <action> <hex-node> <tag-name>\n
1728 1727 #
1729 1728 # Actions are defined as follow:
1730 1729 # "-R": tag is removed,
1731 1730 # "+A": tag is added,
1732 1731 # "-M": tag is moved (old value),
1733 1732 # "+M": tag is moved (new value),
1734 1733 tracktags = lambda x: None
1735 1734 # experimental config: experimental.hook-track-tags
1736 1735 shouldtracktags = self.ui.configbool('experimental', 'hook-track-tags')
1737 1736 if desc != 'strip' and shouldtracktags:
1738 1737 oldheads = self.changelog.headrevs()
1739 1738 def tracktags(tr2):
1740 1739 repo = reporef()
1741 1740 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
1742 1741 newheads = repo.changelog.headrevs()
1743 1742 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
1744 1743 # notes: we compare lists here.
1745 1744 # As we do it only once buiding set would not be cheaper
1746 1745 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
1747 1746 if changes:
1748 1747 tr2.hookargs['tag_moved'] = '1'
1749 1748 with repo.vfs('changes/tags.changes', 'w',
1750 1749 atomictemp=True) as changesfile:
1751 1750 # note: we do not register the file to the transaction
1752 1751 # because we needs it to still exist on the transaction
1753 1752 # is close (for txnclose hooks)
1754 1753 tagsmod.writediff(changesfile, changes)
1755 1754 def validate(tr2):
1756 1755 """will run pre-closing hooks"""
1757 1756 # XXX the transaction API is a bit lacking here so we take a hacky
1758 1757 # path for now
1759 1758 #
1760 1759 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
1761 1760 # dict is copied before these run. In addition we needs the data
1762 1761 # available to in memory hooks too.
1763 1762 #
1764 1763 # Moreover, we also need to make sure this runs before txnclose
1765 1764 # hooks and there is no "pending" mechanism that would execute
1766 1765 # logic only if hooks are about to run.
1767 1766 #
1768 1767 # Fixing this limitation of the transaction is also needed to track
1769 1768 # other families of changes (bookmarks, phases, obsolescence).
1770 1769 #
1771 1770 # This will have to be fixed before we remove the experimental
1772 1771 # gating.
1773 1772 tracktags(tr2)
1774 1773 repo = reporef()
1775 1774 if repo.ui.configbool('experimental', 'single-head-per-branch'):
1776 1775 scmutil.enforcesinglehead(repo, tr2, desc)
1777 1776 if hook.hashook(repo.ui, 'pretxnclose-bookmark'):
1778 1777 for name, (old, new) in sorted(tr.changes['bookmarks'].items()):
1779 1778 args = tr.hookargs.copy()
1780 1779 args.update(bookmarks.preparehookargs(name, old, new))
1781 1780 repo.hook('pretxnclose-bookmark', throw=True,
1782 1781 txnname=desc,
1783 1782 **pycompat.strkwargs(args))
1784 1783 if hook.hashook(repo.ui, 'pretxnclose-phase'):
1785 1784 cl = repo.unfiltered().changelog
1786 1785 for rev, (old, new) in tr.changes['phases'].items():
1787 1786 args = tr.hookargs.copy()
1788 1787 node = hex(cl.node(rev))
1789 1788 args.update(phases.preparehookargs(node, old, new))
1790 1789 repo.hook('pretxnclose-phase', throw=True, txnname=desc,
1791 1790 **pycompat.strkwargs(args))
1792 1791
1793 1792 repo.hook('pretxnclose', throw=True,
1794 1793 txnname=desc, **pycompat.strkwargs(tr.hookargs))
1795 1794 def releasefn(tr, success):
1796 1795 repo = reporef()
1797 1796 if success:
1798 1797 # this should be explicitly invoked here, because
1799 1798 # in-memory changes aren't written out at closing
1800 1799 # transaction, if tr.addfilegenerator (via
1801 1800 # dirstate.write or so) isn't invoked while
1802 1801 # transaction running
1803 1802 repo.dirstate.write(None)
1804 1803 else:
1805 1804 # discard all changes (including ones already written
1806 1805 # out) in this transaction
1807 1806 narrowspec.restorebackup(self, 'journal.narrowspec')
1808 1807 repo.dirstate.restorebackup(None, 'journal.dirstate')
1809 1808
1810 1809 repo.invalidate(clearfilecache=True)
1811 1810
1812 1811 tr = transaction.transaction(rp, self.svfs, vfsmap,
1813 1812 "journal",
1814 1813 "undo",
1815 1814 aftertrans(renames),
1816 1815 self.store.createmode,
1817 1816 validator=validate,
1818 1817 releasefn=releasefn,
1819 1818 checkambigfiles=_cachedfiles,
1820 1819 name=desc)
1821 1820 tr.changes['origrepolen'] = len(self)
1822 1821 tr.changes['obsmarkers'] = set()
1823 1822 tr.changes['phases'] = {}
1824 1823 tr.changes['bookmarks'] = {}
1825 1824
1826 1825 tr.hookargs['txnid'] = txnid
1827 1826 # note: writing the fncache only during finalize mean that the file is
1828 1827 # outdated when running hooks. As fncache is used for streaming clone,
1829 1828 # this is not expected to break anything that happen during the hooks.
1830 1829 tr.addfinalize('flush-fncache', self.store.write)
1831 1830 def txnclosehook(tr2):
1832 1831 """To be run if transaction is successful, will schedule a hook run
1833 1832 """
1834 1833 # Don't reference tr2 in hook() so we don't hold a reference.
1835 1834 # This reduces memory consumption when there are multiple
1836 1835 # transactions per lock. This can likely go away if issue5045
1837 1836 # fixes the function accumulation.
1838 1837 hookargs = tr2.hookargs
1839 1838
1840 1839 def hookfunc():
1841 1840 repo = reporef()
1842 1841 if hook.hashook(repo.ui, 'txnclose-bookmark'):
1843 1842 bmchanges = sorted(tr.changes['bookmarks'].items())
1844 1843 for name, (old, new) in bmchanges:
1845 1844 args = tr.hookargs.copy()
1846 1845 args.update(bookmarks.preparehookargs(name, old, new))
1847 1846 repo.hook('txnclose-bookmark', throw=False,
1848 1847 txnname=desc, **pycompat.strkwargs(args))
1849 1848
1850 1849 if hook.hashook(repo.ui, 'txnclose-phase'):
1851 1850 cl = repo.unfiltered().changelog
1852 1851 phasemv = sorted(tr.changes['phases'].items())
1853 1852 for rev, (old, new) in phasemv:
1854 1853 args = tr.hookargs.copy()
1855 1854 node = hex(cl.node(rev))
1856 1855 args.update(phases.preparehookargs(node, old, new))
1857 1856 repo.hook('txnclose-phase', throw=False, txnname=desc,
1858 1857 **pycompat.strkwargs(args))
1859 1858
1860 1859 repo.hook('txnclose', throw=False, txnname=desc,
1861 1860 **pycompat.strkwargs(hookargs))
1862 1861 reporef()._afterlock(hookfunc)
1863 1862 tr.addfinalize('txnclose-hook', txnclosehook)
1864 1863 # Include a leading "-" to make it happen before the transaction summary
1865 1864 # reports registered via scmutil.registersummarycallback() whose names
1866 1865 # are 00-txnreport etc. That way, the caches will be warm when the
1867 1866 # callbacks run.
1868 1867 tr.addpostclose('-warm-cache', self._buildcacheupdater(tr))
1869 1868 def txnaborthook(tr2):
1870 1869 """To be run if transaction is aborted
1871 1870 """
1872 1871 reporef().hook('txnabort', throw=False, txnname=desc,
1873 1872 **pycompat.strkwargs(tr2.hookargs))
1874 1873 tr.addabort('txnabort-hook', txnaborthook)
1875 1874 # avoid eager cache invalidation. in-memory data should be identical
1876 1875 # to stored data if transaction has no error.
1877 1876 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
1878 1877 self._transref = weakref.ref(tr)
1879 1878 scmutil.registersummarycallback(self, tr, desc)
1880 1879 return tr
1881 1880
1882 1881 def _journalfiles(self):
1883 1882 return ((self.svfs, 'journal'),
1884 1883 (self.vfs, 'journal.dirstate'),
1885 1884 (self.vfs, 'journal.branch'),
1886 1885 (self.vfs, 'journal.desc'),
1887 1886 (self.vfs, 'journal.bookmarks'),
1888 1887 (self.svfs, 'journal.phaseroots'))
1889 1888
1890 1889 def undofiles(self):
1891 1890 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
1892 1891
1893 1892 @unfilteredmethod
1894 1893 def _writejournal(self, desc):
1895 1894 self.dirstate.savebackup(None, 'journal.dirstate')
1896 1895 narrowspec.savebackup(self, 'journal.narrowspec')
1897 1896 self.vfs.write("journal.branch",
1898 1897 encoding.fromlocal(self.dirstate.branch()))
1899 1898 self.vfs.write("journal.desc",
1900 1899 "%d\n%s\n" % (len(self), desc))
1901 1900 self.vfs.write("journal.bookmarks",
1902 1901 self.vfs.tryread("bookmarks"))
1903 1902 self.svfs.write("journal.phaseroots",
1904 1903 self.svfs.tryread("phaseroots"))
1905 1904
1906 1905 def recover(self):
1907 1906 with self.lock():
1908 1907 if self.svfs.exists("journal"):
1909 1908 self.ui.status(_("rolling back interrupted transaction\n"))
1910 1909 vfsmap = {'': self.svfs,
1911 1910 'plain': self.vfs,}
1912 1911 transaction.rollback(self.svfs, vfsmap, "journal",
1913 1912 self.ui.warn,
1914 1913 checkambigfiles=_cachedfiles)
1915 1914 self.invalidate()
1916 1915 return True
1917 1916 else:
1918 1917 self.ui.warn(_("no interrupted transaction available\n"))
1919 1918 return False
1920 1919
1921 1920 def rollback(self, dryrun=False, force=False):
1922 1921 wlock = lock = dsguard = None
1923 1922 try:
1924 1923 wlock = self.wlock()
1925 1924 lock = self.lock()
1926 1925 if self.svfs.exists("undo"):
1927 1926 dsguard = dirstateguard.dirstateguard(self, 'rollback')
1928 1927
1929 1928 return self._rollback(dryrun, force, dsguard)
1930 1929 else:
1931 1930 self.ui.warn(_("no rollback information available\n"))
1932 1931 return 1
1933 1932 finally:
1934 1933 release(dsguard, lock, wlock)
1935 1934
1936 1935 @unfilteredmethod # Until we get smarter cache management
1937 1936 def _rollback(self, dryrun, force, dsguard):
1938 1937 ui = self.ui
1939 1938 try:
1940 1939 args = self.vfs.read('undo.desc').splitlines()
1941 1940 (oldlen, desc, detail) = (int(args[0]), args[1], None)
1942 1941 if len(args) >= 3:
1943 1942 detail = args[2]
1944 1943 oldtip = oldlen - 1
1945 1944
1946 1945 if detail and ui.verbose:
1947 1946 msg = (_('repository tip rolled back to revision %d'
1948 1947 ' (undo %s: %s)\n')
1949 1948 % (oldtip, desc, detail))
1950 1949 else:
1951 1950 msg = (_('repository tip rolled back to revision %d'
1952 1951 ' (undo %s)\n')
1953 1952 % (oldtip, desc))
1954 1953 except IOError:
1955 1954 msg = _('rolling back unknown transaction\n')
1956 1955 desc = None
1957 1956
1958 1957 if not force and self['.'] != self['tip'] and desc == 'commit':
1959 1958 raise error.Abort(
1960 1959 _('rollback of last commit while not checked out '
1961 1960 'may lose data'), hint=_('use -f to force'))
1962 1961
1963 1962 ui.status(msg)
1964 1963 if dryrun:
1965 1964 return 0
1966 1965
1967 1966 parents = self.dirstate.parents()
1968 1967 self.destroying()
1969 1968 vfsmap = {'plain': self.vfs, '': self.svfs}
1970 1969 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn,
1971 1970 checkambigfiles=_cachedfiles)
1972 1971 if self.vfs.exists('undo.bookmarks'):
1973 1972 self.vfs.rename('undo.bookmarks', 'bookmarks', checkambig=True)
1974 1973 if self.svfs.exists('undo.phaseroots'):
1975 1974 self.svfs.rename('undo.phaseroots', 'phaseroots', checkambig=True)
1976 1975 self.invalidate()
1977 1976
1978 1977 parentgone = (parents[0] not in self.changelog.nodemap or
1979 1978 parents[1] not in self.changelog.nodemap)
1980 1979 if parentgone:
1981 1980 # prevent dirstateguard from overwriting already restored one
1982 1981 dsguard.close()
1983 1982
1984 1983 narrowspec.restorebackup(self, 'undo.narrowspec')
1985 1984 self.dirstate.restorebackup(None, 'undo.dirstate')
1986 1985 try:
1987 1986 branch = self.vfs.read('undo.branch')
1988 1987 self.dirstate.setbranch(encoding.tolocal(branch))
1989 1988 except IOError:
1990 1989 ui.warn(_('named branch could not be reset: '
1991 1990 'current branch is still \'%s\'\n')
1992 1991 % self.dirstate.branch())
1993 1992
1994 1993 parents = tuple([p.rev() for p in self[None].parents()])
1995 1994 if len(parents) > 1:
1996 1995 ui.status(_('working directory now based on '
1997 1996 'revisions %d and %d\n') % parents)
1998 1997 else:
1999 1998 ui.status(_('working directory now based on '
2000 1999 'revision %d\n') % parents)
2001 2000 mergemod.mergestate.clean(self, self['.'].node())
2002 2001
2003 2002 # TODO: if we know which new heads may result from this rollback, pass
2004 2003 # them to destroy(), which will prevent the branchhead cache from being
2005 2004 # invalidated.
2006 2005 self.destroyed()
2007 2006 return 0
2008 2007
2009 2008 def _buildcacheupdater(self, newtransaction):
2010 2009 """called during transaction to build the callback updating cache
2011 2010
2012 2011 Lives on the repository to help extension who might want to augment
2013 2012 this logic. For this purpose, the created transaction is passed to the
2014 2013 method.
2015 2014 """
2016 2015 # we must avoid cyclic reference between repo and transaction.
2017 2016 reporef = weakref.ref(self)
2018 2017 def updater(tr):
2019 2018 repo = reporef()
2020 2019 repo.updatecaches(tr)
2021 2020 return updater
2022 2021
2023 2022 @unfilteredmethod
2024 2023 def updatecaches(self, tr=None, full=False):
2025 2024 """warm appropriate caches
2026 2025
2027 2026 If this function is called after a transaction closed. The transaction
2028 2027 will be available in the 'tr' argument. This can be used to selectively
2029 2028 update caches relevant to the changes in that transaction.
2030 2029
2031 2030 If 'full' is set, make sure all caches the function knows about have
2032 2031 up-to-date data. Even the ones usually loaded more lazily.
2033 2032 """
2034 2033 if tr is not None and tr.hookargs.get('source') == 'strip':
2035 2034 # During strip, many caches are invalid but
2036 2035 # later call to `destroyed` will refresh them.
2037 2036 return
2038 2037
2039 2038 if tr is None or tr.changes['origrepolen'] < len(self):
2040 2039 # updating the unfiltered branchmap should refresh all the others,
2041 2040 self.ui.debug('updating the branch cache\n')
2042 2041 branchmap.updatecache(self.filtered('served'))
2043 2042
2044 2043 if full:
2045 2044 rbc = self.revbranchcache()
2046 2045 for r in self.changelog:
2047 2046 rbc.branchinfo(r)
2048 2047 rbc.write()
2049 2048
2050 2049 # ensure the working copy parents are in the manifestfulltextcache
2051 2050 for ctx in self['.'].parents():
2052 2051 ctx.manifest() # accessing the manifest is enough
2053 2052
2054 2053 def invalidatecaches(self):
2055 2054
2056 2055 if r'_tagscache' in vars(self):
2057 2056 # can't use delattr on proxy
2058 2057 del self.__dict__[r'_tagscache']
2059 2058
2060 2059 self.unfiltered()._branchcaches.clear()
2061 2060 self.invalidatevolatilesets()
2062 2061 self._sparsesignaturecache.clear()
2063 2062
2064 2063 def invalidatevolatilesets(self):
2065 2064 self.filteredrevcache.clear()
2066 2065 obsolete.clearobscaches(self)
2067 2066
2068 2067 def invalidatedirstate(self):
2069 2068 '''Invalidates the dirstate, causing the next call to dirstate
2070 2069 to check if it was modified since the last time it was read,
2071 2070 rereading it if it has.
2072 2071
2073 2072 This is different to dirstate.invalidate() that it doesn't always
2074 2073 rereads the dirstate. Use dirstate.invalidate() if you want to
2075 2074 explicitly read the dirstate again (i.e. restoring it to a previous
2076 2075 known good state).'''
2077 2076 if hasunfilteredcache(self, r'dirstate'):
2078 2077 for k in self.dirstate._filecache:
2079 2078 try:
2080 2079 delattr(self.dirstate, k)
2081 2080 except AttributeError:
2082 2081 pass
2083 2082 delattr(self.unfiltered(), r'dirstate')
2084 2083
2085 2084 def invalidate(self, clearfilecache=False):
2086 2085 '''Invalidates both store and non-store parts other than dirstate
2087 2086
2088 2087 If a transaction is running, invalidation of store is omitted,
2089 2088 because discarding in-memory changes might cause inconsistency
2090 2089 (e.g. incomplete fncache causes unintentional failure, but
2091 2090 redundant one doesn't).
2092 2091 '''
2093 2092 unfiltered = self.unfiltered() # all file caches are stored unfiltered
2094 2093 for k in list(self._filecache.keys()):
2095 2094 # dirstate is invalidated separately in invalidatedirstate()
2096 2095 if k == 'dirstate':
2097 2096 continue
2098 2097 if (k == 'changelog' and
2099 2098 self.currenttransaction() and
2100 2099 self.changelog._delayed):
2101 2100 # The changelog object may store unwritten revisions. We don't
2102 2101 # want to lose them.
2103 2102 # TODO: Solve the problem instead of working around it.
2104 2103 continue
2105 2104
2106 2105 if clearfilecache:
2107 2106 del self._filecache[k]
2108 2107 try:
2109 2108 delattr(unfiltered, k)
2110 2109 except AttributeError:
2111 2110 pass
2112 2111 self.invalidatecaches()
2113 2112 if not self.currenttransaction():
2114 2113 # TODO: Changing contents of store outside transaction
2115 2114 # causes inconsistency. We should make in-memory store
2116 2115 # changes detectable, and abort if changed.
2117 2116 self.store.invalidatecaches()
2118 2117
2119 2118 def invalidateall(self):
2120 2119 '''Fully invalidates both store and non-store parts, causing the
2121 2120 subsequent operation to reread any outside changes.'''
2122 2121 # extension should hook this to invalidate its caches
2123 2122 self.invalidate()
2124 2123 self.invalidatedirstate()
2125 2124
2126 2125 @unfilteredmethod
2127 2126 def _refreshfilecachestats(self, tr):
2128 2127 """Reload stats of cached files so that they are flagged as valid"""
2129 2128 for k, ce in self._filecache.items():
2130 2129 k = pycompat.sysstr(k)
2131 2130 if k == r'dirstate' or k not in self.__dict__:
2132 2131 continue
2133 2132 ce.refresh()
2134 2133
2135 2134 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
2136 2135 inheritchecker=None, parentenvvar=None):
2137 2136 parentlock = None
2138 2137 # the contents of parentenvvar are used by the underlying lock to
2139 2138 # determine whether it can be inherited
2140 2139 if parentenvvar is not None:
2141 2140 parentlock = encoding.environ.get(parentenvvar)
2142 2141
2143 2142 timeout = 0
2144 2143 warntimeout = 0
2145 2144 if wait:
2146 2145 timeout = self.ui.configint("ui", "timeout")
2147 2146 warntimeout = self.ui.configint("ui", "timeout.warn")
2148 2147 # internal config: ui.signal-safe-lock
2149 2148 signalsafe = self.ui.configbool('ui', 'signal-safe-lock')
2150 2149
2151 2150 l = lockmod.trylock(self.ui, vfs, lockname, timeout, warntimeout,
2152 2151 releasefn=releasefn,
2153 2152 acquirefn=acquirefn, desc=desc,
2154 2153 inheritchecker=inheritchecker,
2155 2154 parentlock=parentlock,
2156 2155 signalsafe=signalsafe)
2157 2156 return l
2158 2157
2159 2158 def _afterlock(self, callback):
2160 2159 """add a callback to be run when the repository is fully unlocked
2161 2160
2162 2161 The callback will be executed when the outermost lock is released
2163 2162 (with wlock being higher level than 'lock')."""
2164 2163 for ref in (self._wlockref, self._lockref):
2165 2164 l = ref and ref()
2166 2165 if l and l.held:
2167 2166 l.postrelease.append(callback)
2168 2167 break
2169 2168 else: # no lock have been found.
2170 2169 callback()
2171 2170
2172 2171 def lock(self, wait=True):
2173 2172 '''Lock the repository store (.hg/store) and return a weak reference
2174 2173 to the lock. Use this before modifying the store (e.g. committing or
2175 2174 stripping). If you are opening a transaction, get a lock as well.)
2176 2175
2177 2176 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2178 2177 'wlock' first to avoid a dead-lock hazard.'''
2179 2178 l = self._currentlock(self._lockref)
2180 2179 if l is not None:
2181 2180 l.lock()
2182 2181 return l
2183 2182
2184 2183 l = self._lock(self.svfs, "lock", wait, None,
2185 2184 self.invalidate, _('repository %s') % self.origroot)
2186 2185 self._lockref = weakref.ref(l)
2187 2186 return l
2188 2187
2189 2188 def _wlockchecktransaction(self):
2190 2189 if self.currenttransaction() is not None:
2191 2190 raise error.LockInheritanceContractViolation(
2192 2191 'wlock cannot be inherited in the middle of a transaction')
2193 2192
2194 2193 def wlock(self, wait=True):
2195 2194 '''Lock the non-store parts of the repository (everything under
2196 2195 .hg except .hg/store) and return a weak reference to the lock.
2197 2196
2198 2197 Use this before modifying files in .hg.
2199 2198
2200 2199 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2201 2200 'wlock' first to avoid a dead-lock hazard.'''
2202 2201 l = self._wlockref and self._wlockref()
2203 2202 if l is not None and l.held:
2204 2203 l.lock()
2205 2204 return l
2206 2205
2207 2206 # We do not need to check for non-waiting lock acquisition. Such
2208 2207 # acquisition would not cause dead-lock as they would just fail.
2209 2208 if wait and (self.ui.configbool('devel', 'all-warnings')
2210 2209 or self.ui.configbool('devel', 'check-locks')):
2211 2210 if self._currentlock(self._lockref) is not None:
2212 2211 self.ui.develwarn('"wlock" acquired after "lock"')
2213 2212
2214 2213 def unlock():
2215 2214 if self.dirstate.pendingparentchange():
2216 2215 self.dirstate.invalidate()
2217 2216 else:
2218 2217 self.dirstate.write(None)
2219 2218
2220 2219 self._filecache['dirstate'].refresh()
2221 2220
2222 2221 l = self._lock(self.vfs, "wlock", wait, unlock,
2223 2222 self.invalidatedirstate, _('working directory of %s') %
2224 2223 self.origroot,
2225 2224 inheritchecker=self._wlockchecktransaction,
2226 2225 parentenvvar='HG_WLOCK_LOCKER')
2227 2226 self._wlockref = weakref.ref(l)
2228 2227 return l
2229 2228
2230 2229 def _currentlock(self, lockref):
2231 2230 """Returns the lock if it's held, or None if it's not."""
2232 2231 if lockref is None:
2233 2232 return None
2234 2233 l = lockref()
2235 2234 if l is None or not l.held:
2236 2235 return None
2237 2236 return l
2238 2237
2239 2238 def currentwlock(self):
2240 2239 """Returns the wlock if it's held, or None if it's not."""
2241 2240 return self._currentlock(self._wlockref)
2242 2241
2243 2242 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
2244 2243 """
2245 2244 commit an individual file as part of a larger transaction
2246 2245 """
2247 2246
2248 2247 fname = fctx.path()
2249 2248 fparent1 = manifest1.get(fname, nullid)
2250 2249 fparent2 = manifest2.get(fname, nullid)
2251 2250 if isinstance(fctx, context.filectx):
2252 2251 node = fctx.filenode()
2253 2252 if node in [fparent1, fparent2]:
2254 2253 self.ui.debug('reusing %s filelog entry\n' % fname)
2255 2254 if manifest1.flags(fname) != fctx.flags():
2256 2255 changelist.append(fname)
2257 2256 return node
2258 2257
2259 2258 flog = self.file(fname)
2260 2259 meta = {}
2261 2260 copy = fctx.renamed()
2262 2261 if copy and copy[0] != fname:
2263 2262 # Mark the new revision of this file as a copy of another
2264 2263 # file. This copy data will effectively act as a parent
2265 2264 # of this new revision. If this is a merge, the first
2266 2265 # parent will be the nullid (meaning "look up the copy data")
2267 2266 # and the second one will be the other parent. For example:
2268 2267 #
2269 2268 # 0 --- 1 --- 3 rev1 changes file foo
2270 2269 # \ / rev2 renames foo to bar and changes it
2271 2270 # \- 2 -/ rev3 should have bar with all changes and
2272 2271 # should record that bar descends from
2273 2272 # bar in rev2 and foo in rev1
2274 2273 #
2275 2274 # this allows this merge to succeed:
2276 2275 #
2277 2276 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
2278 2277 # \ / merging rev3 and rev4 should use bar@rev2
2279 2278 # \- 2 --- 4 as the merge base
2280 2279 #
2281 2280
2282 2281 cfname = copy[0]
2283 2282 crev = manifest1.get(cfname)
2284 2283 newfparent = fparent2
2285 2284
2286 2285 if manifest2: # branch merge
2287 2286 if fparent2 == nullid or crev is None: # copied on remote side
2288 2287 if cfname in manifest2:
2289 2288 crev = manifest2[cfname]
2290 2289 newfparent = fparent1
2291 2290
2292 2291 # Here, we used to search backwards through history to try to find
2293 2292 # where the file copy came from if the source of a copy was not in
2294 2293 # the parent directory. However, this doesn't actually make sense to
2295 2294 # do (what does a copy from something not in your working copy even
2296 2295 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
2297 2296 # the user that copy information was dropped, so if they didn't
2298 2297 # expect this outcome it can be fixed, but this is the correct
2299 2298 # behavior in this circumstance.
2300 2299
2301 2300 if crev:
2302 2301 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
2303 2302 meta["copy"] = cfname
2304 2303 meta["copyrev"] = hex(crev)
2305 2304 fparent1, fparent2 = nullid, newfparent
2306 2305 else:
2307 2306 self.ui.warn(_("warning: can't find ancestor for '%s' "
2308 2307 "copied from '%s'!\n") % (fname, cfname))
2309 2308
2310 2309 elif fparent1 == nullid:
2311 2310 fparent1, fparent2 = fparent2, nullid
2312 2311 elif fparent2 != nullid:
2313 2312 # is one parent an ancestor of the other?
2314 2313 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
2315 2314 if fparent1 in fparentancestors:
2316 2315 fparent1, fparent2 = fparent2, nullid
2317 2316 elif fparent2 in fparentancestors:
2318 2317 fparent2 = nullid
2319 2318
2320 2319 # is the file changed?
2321 2320 text = fctx.data()
2322 2321 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
2323 2322 changelist.append(fname)
2324 2323 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
2325 2324 # are just the flags changed during merge?
2326 2325 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
2327 2326 changelist.append(fname)
2328 2327
2329 2328 return fparent1
2330 2329
2331 2330 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
2332 2331 """check for commit arguments that aren't committable"""
2333 2332 if match.isexact() or match.prefix():
2334 2333 matched = set(status.modified + status.added + status.removed)
2335 2334
2336 2335 for f in match.files():
2337 2336 f = self.dirstate.normalize(f)
2338 2337 if f == '.' or f in matched or f in wctx.substate:
2339 2338 continue
2340 2339 if f in status.deleted:
2341 2340 fail(f, _('file not found!'))
2342 2341 if f in vdirs: # visited directory
2343 2342 d = f + '/'
2344 2343 for mf in matched:
2345 2344 if mf.startswith(d):
2346 2345 break
2347 2346 else:
2348 2347 fail(f, _("no match under directory!"))
2349 2348 elif f not in self.dirstate:
2350 2349 fail(f, _("file not tracked!"))
2351 2350
2352 2351 @unfilteredmethod
2353 2352 def commit(self, text="", user=None, date=None, match=None, force=False,
2354 2353 editor=False, extra=None):
2355 2354 """Add a new revision to current repository.
2356 2355
2357 2356 Revision information is gathered from the working directory,
2358 2357 match can be used to filter the committed files. If editor is
2359 2358 supplied, it is called to get a commit message.
2360 2359 """
2361 2360 if extra is None:
2362 2361 extra = {}
2363 2362
2364 2363 def fail(f, msg):
2365 2364 raise error.Abort('%s: %s' % (f, msg))
2366 2365
2367 2366 if not match:
2368 2367 match = matchmod.always(self.root, '')
2369 2368
2370 2369 if not force:
2371 2370 vdirs = []
2372 2371 match.explicitdir = vdirs.append
2373 2372 match.bad = fail
2374 2373
2375 2374 wlock = lock = tr = None
2376 2375 try:
2377 2376 wlock = self.wlock()
2378 2377 lock = self.lock() # for recent changelog (see issue4368)
2379 2378
2380 2379 wctx = self[None]
2381 2380 merge = len(wctx.parents()) > 1
2382 2381
2383 2382 if not force and merge and not match.always():
2384 2383 raise error.Abort(_('cannot partially commit a merge '
2385 2384 '(do not specify files or patterns)'))
2386 2385
2387 2386 status = self.status(match=match, clean=force)
2388 2387 if force:
2389 2388 status.modified.extend(status.clean) # mq may commit clean files
2390 2389
2391 2390 # check subrepos
2392 2391 subs, commitsubs, newstate = subrepoutil.precommit(
2393 2392 self.ui, wctx, status, match, force=force)
2394 2393
2395 2394 # make sure all explicit patterns are matched
2396 2395 if not force:
2397 2396 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
2398 2397
2399 2398 cctx = context.workingcommitctx(self, status,
2400 2399 text, user, date, extra)
2401 2400
2402 2401 # internal config: ui.allowemptycommit
2403 2402 allowemptycommit = (wctx.branch() != wctx.p1().branch()
2404 2403 or extra.get('close') or merge or cctx.files()
2405 2404 or self.ui.configbool('ui', 'allowemptycommit'))
2406 2405 if not allowemptycommit:
2407 2406 return None
2408 2407
2409 2408 if merge and cctx.deleted():
2410 2409 raise error.Abort(_("cannot commit merge with missing files"))
2411 2410
2412 2411 ms = mergemod.mergestate.read(self)
2413 2412 mergeutil.checkunresolved(ms)
2414 2413
2415 2414 if editor:
2416 2415 cctx._text = editor(self, cctx, subs)
2417 2416 edited = (text != cctx._text)
2418 2417
2419 2418 # Save commit message in case this transaction gets rolled back
2420 2419 # (e.g. by a pretxncommit hook). Leave the content alone on
2421 2420 # the assumption that the user will use the same editor again.
2422 2421 msgfn = self.savecommitmessage(cctx._text)
2423 2422
2424 2423 # commit subs and write new state
2425 2424 if subs:
2426 2425 for s in sorted(commitsubs):
2427 2426 sub = wctx.sub(s)
2428 2427 self.ui.status(_('committing subrepository %s\n') %
2429 2428 subrepoutil.subrelpath(sub))
2430 2429 sr = sub.commit(cctx._text, user, date)
2431 2430 newstate[s] = (newstate[s][0], sr)
2432 2431 subrepoutil.writestate(self, newstate)
2433 2432
2434 2433 p1, p2 = self.dirstate.parents()
2435 2434 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
2436 2435 try:
2437 2436 self.hook("precommit", throw=True, parent1=hookp1,
2438 2437 parent2=hookp2)
2439 2438 tr = self.transaction('commit')
2440 2439 ret = self.commitctx(cctx, True)
2441 2440 except: # re-raises
2442 2441 if edited:
2443 2442 self.ui.write(
2444 2443 _('note: commit message saved in %s\n') % msgfn)
2445 2444 raise
2446 2445 # update bookmarks, dirstate and mergestate
2447 2446 bookmarks.update(self, [p1, p2], ret)
2448 2447 cctx.markcommitted(ret)
2449 2448 ms.reset()
2450 2449 tr.close()
2451 2450
2452 2451 finally:
2453 2452 lockmod.release(tr, lock, wlock)
2454 2453
2455 2454 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
2456 2455 # hack for command that use a temporary commit (eg: histedit)
2457 2456 # temporary commit got stripped before hook release
2458 2457 if self.changelog.hasnode(ret):
2459 2458 self.hook("commit", node=node, parent1=parent1,
2460 2459 parent2=parent2)
2461 2460 self._afterlock(commithook)
2462 2461 return ret
2463 2462
2464 2463 @unfilteredmethod
2465 2464 def commitctx(self, ctx, error=False):
2466 2465 """Add a new revision to current repository.
2467 2466 Revision information is passed via the context argument.
2468 2467
2469 2468 ctx.files() should list all files involved in this commit, i.e.
2470 2469 modified/added/removed files. On merge, it may be wider than the
2471 2470 ctx.files() to be committed, since any file nodes derived directly
2472 2471 from p1 or p2 are excluded from the committed ctx.files().
2473 2472 """
2474 2473
2475 2474 tr = None
2476 2475 p1, p2 = ctx.p1(), ctx.p2()
2477 2476 user = ctx.user()
2478 2477
2479 2478 lock = self.lock()
2480 2479 try:
2481 2480 tr = self.transaction("commit")
2482 2481 trp = weakref.proxy(tr)
2483 2482
2484 2483 if ctx.manifestnode():
2485 2484 # reuse an existing manifest revision
2486 2485 self.ui.debug('reusing known manifest\n')
2487 2486 mn = ctx.manifestnode()
2488 2487 files = ctx.files()
2489 2488 elif ctx.files():
2490 2489 m1ctx = p1.manifestctx()
2491 2490 m2ctx = p2.manifestctx()
2492 2491 mctx = m1ctx.copy()
2493 2492
2494 2493 m = mctx.read()
2495 2494 m1 = m1ctx.read()
2496 2495 m2 = m2ctx.read()
2497 2496
2498 2497 # check in files
2499 2498 added = []
2500 2499 changed = []
2501 2500 removed = list(ctx.removed())
2502 2501 linkrev = len(self)
2503 2502 self.ui.note(_("committing files:\n"))
2504 2503 for f in sorted(ctx.modified() + ctx.added()):
2505 2504 self.ui.note(f + "\n")
2506 2505 try:
2507 2506 fctx = ctx[f]
2508 2507 if fctx is None:
2509 2508 removed.append(f)
2510 2509 else:
2511 2510 added.append(f)
2512 2511 m[f] = self._filecommit(fctx, m1, m2, linkrev,
2513 2512 trp, changed)
2514 2513 m.setflag(f, fctx.flags())
2515 2514 except OSError as inst:
2516 2515 self.ui.warn(_("trouble committing %s!\n") % f)
2517 2516 raise
2518 2517 except IOError as inst:
2519 2518 errcode = getattr(inst, 'errno', errno.ENOENT)
2520 2519 if error or errcode and errcode != errno.ENOENT:
2521 2520 self.ui.warn(_("trouble committing %s!\n") % f)
2522 2521 raise
2523 2522
2524 2523 # update manifest
2525 2524 removed = [f for f in sorted(removed) if f in m1 or f in m2]
2526 2525 drop = [f for f in removed if f in m]
2527 2526 for f in drop:
2528 2527 del m[f]
2529 2528 files = changed + removed
2530 2529 md = None
2531 2530 if not files:
2532 2531 # if no "files" actually changed in terms of the changelog,
2533 2532 # try hard to detect unmodified manifest entry so that the
2534 2533 # exact same commit can be reproduced later on convert.
2535 2534 md = m1.diff(m, scmutil.matchfiles(self, ctx.files()))
2536 2535 if not files and md:
2537 2536 self.ui.debug('not reusing manifest (no file change in '
2538 2537 'changelog, but manifest differs)\n')
2539 2538 if files or md:
2540 2539 self.ui.note(_("committing manifest\n"))
2541 2540 # we're using narrowmatch here since it's already applied at
2542 2541 # other stages (such as dirstate.walk), so we're already
2543 2542 # ignoring things outside of narrowspec in most cases. The
2544 2543 # one case where we might have files outside the narrowspec
2545 2544 # at this point is merges, and we already error out in the
2546 2545 # case where the merge has files outside of the narrowspec,
2547 2546 # so this is safe.
2548 2547 mn = mctx.write(trp, linkrev,
2549 2548 p1.manifestnode(), p2.manifestnode(),
2550 2549 added, drop, match=self.narrowmatch())
2551 2550 else:
2552 2551 self.ui.debug('reusing manifest form p1 (listed files '
2553 2552 'actually unchanged)\n')
2554 2553 mn = p1.manifestnode()
2555 2554 else:
2556 2555 self.ui.debug('reusing manifest from p1 (no file change)\n')
2557 2556 mn = p1.manifestnode()
2558 2557 files = []
2559 2558
2560 2559 # update changelog
2561 2560 self.ui.note(_("committing changelog\n"))
2562 2561 self.changelog.delayupdate(tr)
2563 2562 n = self.changelog.add(mn, files, ctx.description(),
2564 2563 trp, p1.node(), p2.node(),
2565 2564 user, ctx.date(), ctx.extra().copy())
2566 2565 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
2567 2566 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
2568 2567 parent2=xp2)
2569 2568 # set the new commit is proper phase
2570 2569 targetphase = subrepoutil.newcommitphase(self.ui, ctx)
2571 2570 if targetphase:
2572 2571 # retract boundary do not alter parent changeset.
2573 2572 # if a parent have higher the resulting phase will
2574 2573 # be compliant anyway
2575 2574 #
2576 2575 # if minimal phase was 0 we don't need to retract anything
2577 2576 phases.registernew(self, tr, targetphase, [n])
2578 2577 tr.close()
2579 2578 return n
2580 2579 finally:
2581 2580 if tr:
2582 2581 tr.release()
2583 2582 lock.release()
2584 2583
2585 2584 @unfilteredmethod
2586 2585 def destroying(self):
2587 2586 '''Inform the repository that nodes are about to be destroyed.
2588 2587 Intended for use by strip and rollback, so there's a common
2589 2588 place for anything that has to be done before destroying history.
2590 2589
2591 2590 This is mostly useful for saving state that is in memory and waiting
2592 2591 to be flushed when the current lock is released. Because a call to
2593 2592 destroyed is imminent, the repo will be invalidated causing those
2594 2593 changes to stay in memory (waiting for the next unlock), or vanish
2595 2594 completely.
2596 2595 '''
2597 2596 # When using the same lock to commit and strip, the phasecache is left
2598 2597 # dirty after committing. Then when we strip, the repo is invalidated,
2599 2598 # causing those changes to disappear.
2600 2599 if '_phasecache' in vars(self):
2601 2600 self._phasecache.write()
2602 2601
2603 2602 @unfilteredmethod
2604 2603 def destroyed(self):
2605 2604 '''Inform the repository that nodes have been destroyed.
2606 2605 Intended for use by strip and rollback, so there's a common
2607 2606 place for anything that has to be done after destroying history.
2608 2607 '''
2609 2608 # When one tries to:
2610 2609 # 1) destroy nodes thus calling this method (e.g. strip)
2611 2610 # 2) use phasecache somewhere (e.g. commit)
2612 2611 #
2613 2612 # then 2) will fail because the phasecache contains nodes that were
2614 2613 # removed. We can either remove phasecache from the filecache,
2615 2614 # causing it to reload next time it is accessed, or simply filter
2616 2615 # the removed nodes now and write the updated cache.
2617 2616 self._phasecache.filterunknown(self)
2618 2617 self._phasecache.write()
2619 2618
2620 2619 # refresh all repository caches
2621 2620 self.updatecaches()
2622 2621
2623 2622 # Ensure the persistent tag cache is updated. Doing it now
2624 2623 # means that the tag cache only has to worry about destroyed
2625 2624 # heads immediately after a strip/rollback. That in turn
2626 2625 # guarantees that "cachetip == currenttip" (comparing both rev
2627 2626 # and node) always means no nodes have been added or destroyed.
2628 2627
2629 2628 # XXX this is suboptimal when qrefresh'ing: we strip the current
2630 2629 # head, refresh the tag cache, then immediately add a new head.
2631 2630 # But I think doing it this way is necessary for the "instant
2632 2631 # tag cache retrieval" case to work.
2633 2632 self.invalidate()
2634 2633
2635 2634 def status(self, node1='.', node2=None, match=None,
2636 2635 ignored=False, clean=False, unknown=False,
2637 2636 listsubrepos=False):
2638 2637 '''a convenience method that calls node1.status(node2)'''
2639 2638 return self[node1].status(node2, match, ignored, clean, unknown,
2640 2639 listsubrepos)
2641 2640
2642 2641 def addpostdsstatus(self, ps):
2643 2642 """Add a callback to run within the wlock, at the point at which status
2644 2643 fixups happen.
2645 2644
2646 2645 On status completion, callback(wctx, status) will be called with the
2647 2646 wlock held, unless the dirstate has changed from underneath or the wlock
2648 2647 couldn't be grabbed.
2649 2648
2650 2649 Callbacks should not capture and use a cached copy of the dirstate --
2651 2650 it might change in the meanwhile. Instead, they should access the
2652 2651 dirstate via wctx.repo().dirstate.
2653 2652
2654 2653 This list is emptied out after each status run -- extensions should
2655 2654 make sure it adds to this list each time dirstate.status is called.
2656 2655 Extensions should also make sure they don't call this for statuses
2657 2656 that don't involve the dirstate.
2658 2657 """
2659 2658
2660 2659 # The list is located here for uniqueness reasons -- it is actually
2661 2660 # managed by the workingctx, but that isn't unique per-repo.
2662 2661 self._postdsstatus.append(ps)
2663 2662
2664 2663 def postdsstatus(self):
2665 2664 """Used by workingctx to get the list of post-dirstate-status hooks."""
2666 2665 return self._postdsstatus
2667 2666
2668 2667 def clearpostdsstatus(self):
2669 2668 """Used by workingctx to clear post-dirstate-status hooks."""
2670 2669 del self._postdsstatus[:]
2671 2670
2672 2671 def heads(self, start=None):
2673 2672 if start is None:
2674 2673 cl = self.changelog
2675 2674 headrevs = reversed(cl.headrevs())
2676 2675 return [cl.node(rev) for rev in headrevs]
2677 2676
2678 2677 heads = self.changelog.heads(start)
2679 2678 # sort the output in rev descending order
2680 2679 return sorted(heads, key=self.changelog.rev, reverse=True)
2681 2680
2682 2681 def branchheads(self, branch=None, start=None, closed=False):
2683 2682 '''return a (possibly filtered) list of heads for the given branch
2684 2683
2685 2684 Heads are returned in topological order, from newest to oldest.
2686 2685 If branch is None, use the dirstate branch.
2687 2686 If start is not None, return only heads reachable from start.
2688 2687 If closed is True, return heads that are marked as closed as well.
2689 2688 '''
2690 2689 if branch is None:
2691 2690 branch = self[None].branch()
2692 2691 branches = self.branchmap()
2693 2692 if branch not in branches:
2694 2693 return []
2695 2694 # the cache returns heads ordered lowest to highest
2696 2695 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
2697 2696 if start is not None:
2698 2697 # filter out the heads that cannot be reached from startrev
2699 2698 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
2700 2699 bheads = [h for h in bheads if h in fbheads]
2701 2700 return bheads
2702 2701
2703 2702 def branches(self, nodes):
2704 2703 if not nodes:
2705 2704 nodes = [self.changelog.tip()]
2706 2705 b = []
2707 2706 for n in nodes:
2708 2707 t = n
2709 2708 while True:
2710 2709 p = self.changelog.parents(n)
2711 2710 if p[1] != nullid or p[0] == nullid:
2712 2711 b.append((t, n, p[0], p[1]))
2713 2712 break
2714 2713 n = p[0]
2715 2714 return b
2716 2715
2717 2716 def between(self, pairs):
2718 2717 r = []
2719 2718
2720 2719 for top, bottom in pairs:
2721 2720 n, l, i = top, [], 0
2722 2721 f = 1
2723 2722
2724 2723 while n != bottom and n != nullid:
2725 2724 p = self.changelog.parents(n)[0]
2726 2725 if i == f:
2727 2726 l.append(n)
2728 2727 f = f * 2
2729 2728 n = p
2730 2729 i += 1
2731 2730
2732 2731 r.append(l)
2733 2732
2734 2733 return r
2735 2734
2736 2735 def checkpush(self, pushop):
2737 2736 """Extensions can override this function if additional checks have
2738 2737 to be performed before pushing, or call it if they override push
2739 2738 command.
2740 2739 """
2741 2740
2742 2741 @unfilteredpropertycache
2743 2742 def prepushoutgoinghooks(self):
2744 2743 """Return util.hooks consists of a pushop with repo, remote, outgoing
2745 2744 methods, which are called before pushing changesets.
2746 2745 """
2747 2746 return util.hooks()
2748 2747
2749 2748 def pushkey(self, namespace, key, old, new):
2750 2749 try:
2751 2750 tr = self.currenttransaction()
2752 2751 hookargs = {}
2753 2752 if tr is not None:
2754 2753 hookargs.update(tr.hookargs)
2755 2754 hookargs = pycompat.strkwargs(hookargs)
2756 2755 hookargs[r'namespace'] = namespace
2757 2756 hookargs[r'key'] = key
2758 2757 hookargs[r'old'] = old
2759 2758 hookargs[r'new'] = new
2760 2759 self.hook('prepushkey', throw=True, **hookargs)
2761 2760 except error.HookAbort as exc:
2762 2761 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
2763 2762 if exc.hint:
2764 2763 self.ui.write_err(_("(%s)\n") % exc.hint)
2765 2764 return False
2766 2765 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
2767 2766 ret = pushkey.push(self, namespace, key, old, new)
2768 2767 def runhook():
2769 2768 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
2770 2769 ret=ret)
2771 2770 self._afterlock(runhook)
2772 2771 return ret
2773 2772
2774 2773 def listkeys(self, namespace):
2775 2774 self.hook('prelistkeys', throw=True, namespace=namespace)
2776 2775 self.ui.debug('listing keys for "%s"\n' % namespace)
2777 2776 values = pushkey.list(self, namespace)
2778 2777 self.hook('listkeys', namespace=namespace, values=values)
2779 2778 return values
2780 2779
2781 2780 def debugwireargs(self, one, two, three=None, four=None, five=None):
2782 2781 '''used to test argument passing over the wire'''
2783 2782 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
2784 2783 pycompat.bytestr(four),
2785 2784 pycompat.bytestr(five))
2786 2785
2787 2786 def savecommitmessage(self, text):
2788 2787 fp = self.vfs('last-message.txt', 'wb')
2789 2788 try:
2790 2789 fp.write(text)
2791 2790 finally:
2792 2791 fp.close()
2793 2792 return self.pathto(fp.name[len(self.root) + 1:])
2794 2793
2795 2794 # used to avoid circular references so destructors work
2796 2795 def aftertrans(files):
2797 2796 renamefiles = [tuple(t) for t in files]
2798 2797 def a():
2799 2798 for vfs, src, dest in renamefiles:
2800 2799 # if src and dest refer to a same file, vfs.rename is a no-op,
2801 2800 # leaving both src and dest on disk. delete dest to make sure
2802 2801 # the rename couldn't be such a no-op.
2803 2802 vfs.tryunlink(dest)
2804 2803 try:
2805 2804 vfs.rename(src, dest)
2806 2805 except OSError: # journal file does not yet exist
2807 2806 pass
2808 2807 return a
2809 2808
2810 2809 def undoname(fn):
2811 2810 base, name = os.path.split(fn)
2812 2811 assert name.startswith('journal')
2813 2812 return os.path.join(base, name.replace('journal', 'undo', 1))
2814 2813
2815 2814 def instance(ui, path, create, intents=None, createopts=None):
2816 2815 localpath = util.urllocalpath(path)
2817 2816 if create:
2818 2817 createrepository(ui, localpath, createopts=createopts)
2819 2818
2820 2819 return makelocalrepository(ui, localpath, intents=intents)
2821 2820
2822 2821 def islocal(path):
2823 2822 return True
2824 2823
2825 2824 def defaultcreateopts(ui, createopts=None):
2826 2825 """Populate the default creation options for a repository.
2827 2826
2828 2827 A dictionary of explicitly requested creation options can be passed
2829 2828 in. Missing keys will be populated.
2830 2829 """
2831 2830 createopts = dict(createopts or {})
2832 2831
2833 2832 if 'backend' not in createopts:
2834 2833 # experimental config: storage.new-repo-backend
2835 2834 createopts['backend'] = ui.config('storage', 'new-repo-backend')
2836 2835
2837 2836 return createopts
2838 2837
2839 2838 def newreporequirements(ui, createopts):
2840 2839 """Determine the set of requirements for a new local repository.
2841 2840
2842 2841 Extensions can wrap this function to specify custom requirements for
2843 2842 new repositories.
2844 2843 """
2845 2844 # If the repo is being created from a shared repository, we copy
2846 2845 # its requirements.
2847 2846 if 'sharedrepo' in createopts:
2848 2847 requirements = set(createopts['sharedrepo'].requirements)
2849 2848 if createopts.get('sharedrelative'):
2850 2849 requirements.add('relshared')
2851 2850 else:
2852 2851 requirements.add('shared')
2853 2852
2854 2853 return requirements
2855 2854
2856 2855 if 'backend' not in createopts:
2857 2856 raise error.ProgrammingError('backend key not present in createopts; '
2858 2857 'was defaultcreateopts() called?')
2859 2858
2860 2859 if createopts['backend'] != 'revlogv1':
2861 2860 raise error.Abort(_('unable to determine repository requirements for '
2862 2861 'storage backend: %s') % createopts['backend'])
2863 2862
2864 2863 requirements = {'revlogv1'}
2865 2864 if ui.configbool('format', 'usestore'):
2866 2865 requirements.add('store')
2867 2866 if ui.configbool('format', 'usefncache'):
2868 2867 requirements.add('fncache')
2869 2868 if ui.configbool('format', 'dotencode'):
2870 2869 requirements.add('dotencode')
2871 2870
2872 2871 compengine = ui.config('experimental', 'format.compression')
2873 2872 if compengine not in util.compengines:
2874 2873 raise error.Abort(_('compression engine %s defined by '
2875 2874 'experimental.format.compression not available') %
2876 2875 compengine,
2877 2876 hint=_('run "hg debuginstall" to list available '
2878 2877 'compression engines'))
2879 2878
2880 2879 # zlib is the historical default and doesn't need an explicit requirement.
2881 2880 if compengine != 'zlib':
2882 2881 requirements.add('exp-compression-%s' % compengine)
2883 2882
2884 2883 if scmutil.gdinitconfig(ui):
2885 2884 requirements.add('generaldelta')
2886 2885 if ui.configbool('experimental', 'treemanifest'):
2887 2886 requirements.add('treemanifest')
2888 2887 # experimental config: format.sparse-revlog
2889 2888 if ui.configbool('format', 'sparse-revlog'):
2890 2889 requirements.add(SPARSEREVLOG_REQUIREMENT)
2891 2890
2892 2891 revlogv2 = ui.config('experimental', 'revlogv2')
2893 2892 if revlogv2 == 'enable-unstable-format-and-corrupt-my-data':
2894 2893 requirements.remove('revlogv1')
2895 2894 # generaldelta is implied by revlogv2.
2896 2895 requirements.discard('generaldelta')
2897 2896 requirements.add(REVLOGV2_REQUIREMENT)
2898 2897 # experimental config: format.internal-phase
2899 2898 if ui.configbool('format', 'internal-phase'):
2900 2899 requirements.add('internal-phase')
2901 2900
2902 2901 if createopts.get('narrowfiles'):
2903 2902 requirements.add(repository.NARROW_REQUIREMENT)
2904 2903
2905 2904 if createopts.get('lfs'):
2906 2905 requirements.add('lfs')
2907 2906
2908 2907 return requirements
2909 2908
2910 2909 def filterknowncreateopts(ui, createopts):
2911 2910 """Filters a dict of repo creation options against options that are known.
2912 2911
2913 2912 Receives a dict of repo creation options and returns a dict of those
2914 2913 options that we don't know how to handle.
2915 2914
2916 2915 This function is called as part of repository creation. If the
2917 2916 returned dict contains any items, repository creation will not
2918 2917 be allowed, as it means there was a request to create a repository
2919 2918 with options not recognized by loaded code.
2920 2919
2921 2920 Extensions can wrap this function to filter out creation options
2922 2921 they know how to handle.
2923 2922 """
2924 2923 known = {
2925 2924 'backend',
2926 2925 'lfs',
2927 2926 'narrowfiles',
2928 2927 'sharedrepo',
2929 2928 'sharedrelative',
2930 2929 'shareditems',
2931 2930 'shallowfilestore',
2932 2931 }
2933 2932
2934 2933 return {k: v for k, v in createopts.items() if k not in known}
2935 2934
2936 2935 def createrepository(ui, path, createopts=None):
2937 2936 """Create a new repository in a vfs.
2938 2937
2939 2938 ``path`` path to the new repo's working directory.
2940 2939 ``createopts`` options for the new repository.
2941 2940
2942 2941 The following keys for ``createopts`` are recognized:
2943 2942
2944 2943 backend
2945 2944 The storage backend to use.
2946 2945 lfs
2947 2946 Repository will be created with ``lfs`` requirement. The lfs extension
2948 2947 will automatically be loaded when the repository is accessed.
2949 2948 narrowfiles
2950 2949 Set up repository to support narrow file storage.
2951 2950 sharedrepo
2952 2951 Repository object from which storage should be shared.
2953 2952 sharedrelative
2954 2953 Boolean indicating if the path to the shared repo should be
2955 2954 stored as relative. By default, the pointer to the "parent" repo
2956 2955 is stored as an absolute path.
2957 2956 shareditems
2958 2957 Set of items to share to the new repository (in addition to storage).
2959 2958 shallowfilestore
2960 2959 Indicates that storage for files should be shallow (not all ancestor
2961 2960 revisions are known).
2962 2961 """
2963 2962 createopts = defaultcreateopts(ui, createopts=createopts)
2964 2963
2965 2964 unknownopts = filterknowncreateopts(ui, createopts)
2966 2965
2967 2966 if not isinstance(unknownopts, dict):
2968 2967 raise error.ProgrammingError('filterknowncreateopts() did not return '
2969 2968 'a dict')
2970 2969
2971 2970 if unknownopts:
2972 2971 raise error.Abort(_('unable to create repository because of unknown '
2973 2972 'creation option: %s') %
2974 2973 ', '.join(sorted(unknownopts)),
2975 2974 hint=_('is a required extension not loaded?'))
2976 2975
2977 2976 requirements = newreporequirements(ui, createopts=createopts)
2978 2977
2979 2978 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
2980 2979
2981 2980 hgvfs = vfsmod.vfs(wdirvfs.join(b'.hg'))
2982 2981 if hgvfs.exists():
2983 2982 raise error.RepoError(_('repository %s already exists') % path)
2984 2983
2985 2984 if 'sharedrepo' in createopts:
2986 2985 sharedpath = createopts['sharedrepo'].sharedpath
2987 2986
2988 2987 if createopts.get('sharedrelative'):
2989 2988 try:
2990 2989 sharedpath = os.path.relpath(sharedpath, hgvfs.base)
2991 2990 except (IOError, ValueError) as e:
2992 2991 # ValueError is raised on Windows if the drive letters differ
2993 2992 # on each path.
2994 2993 raise error.Abort(_('cannot calculate relative path'),
2995 2994 hint=stringutil.forcebytestr(e))
2996 2995
2997 2996 if not wdirvfs.exists():
2998 2997 wdirvfs.makedirs()
2999 2998
3000 2999 hgvfs.makedir(notindexed=True)
3001 3000
3002 3001 if b'store' in requirements and 'sharedrepo' not in createopts:
3003 3002 hgvfs.mkdir(b'store')
3004 3003
3005 3004 # We create an invalid changelog outside the store so very old
3006 3005 # Mercurial versions (which didn't know about the requirements
3007 3006 # file) encounter an error on reading the changelog. This
3008 3007 # effectively locks out old clients and prevents them from
3009 3008 # mucking with a repo in an unknown format.
3010 3009 #
3011 3010 # The revlog header has version 2, which won't be recognized by
3012 3011 # such old clients.
3013 3012 hgvfs.append(b'00changelog.i',
3014 3013 b'\0\0\0\2 dummy changelog to prevent using the old repo '
3015 3014 b'layout')
3016 3015
3017 3016 scmutil.writerequires(hgvfs, requirements)
3018 3017
3019 3018 # Write out file telling readers where to find the shared store.
3020 3019 if 'sharedrepo' in createopts:
3021 3020 hgvfs.write(b'sharedpath', sharedpath)
3022 3021
3023 3022 if createopts.get('shareditems'):
3024 3023 shared = b'\n'.join(sorted(createopts['shareditems'])) + b'\n'
3025 3024 hgvfs.write(b'shared', shared)
3026 3025
3027 3026 def poisonrepository(repo):
3028 3027 """Poison a repository instance so it can no longer be used."""
3029 3028 # Perform any cleanup on the instance.
3030 3029 repo.close()
3031 3030
3032 3031 # Our strategy is to replace the type of the object with one that
3033 3032 # has all attribute lookups result in error.
3034 3033 #
3035 3034 # But we have to allow the close() method because some constructors
3036 3035 # of repos call close() on repo references.
3037 3036 class poisonedrepository(object):
3038 3037 def __getattribute__(self, item):
3039 3038 if item == r'close':
3040 3039 return object.__getattribute__(self, item)
3041 3040
3042 3041 raise error.ProgrammingError('repo instances should not be used '
3043 3042 'after unshare')
3044 3043
3045 3044 def close(self):
3046 3045 pass
3047 3046
3048 3047 # We may have a repoview, which intercepts __setattr__. So be sure
3049 3048 # we operate at the lowest level possible.
3050 3049 object.__setattr__(repo, r'__class__', poisonedrepository)
@@ -1,1807 +1,1800 b''
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 errno
11 11 import glob
12 12 import hashlib
13 13 import os
14 14 import re
15 15 import socket
16 16 import subprocess
17 17 import weakref
18 18
19 19 from .i18n import _
20 20 from .node import (
21 21 bin,
22 22 hex,
23 23 nullid,
24 24 nullrev,
25 25 short,
26 26 wdirid,
27 27 wdirrev,
28 28 )
29 29
30 30 from . import (
31 31 encoding,
32 32 error,
33 33 match as matchmod,
34 34 obsolete,
35 35 obsutil,
36 36 pathutil,
37 37 phases,
38 38 policy,
39 39 pycompat,
40 40 revsetlang,
41 41 similar,
42 42 smartset,
43 43 url,
44 44 util,
45 45 vfs,
46 46 )
47 47
48 48 from .utils import (
49 49 procutil,
50 50 stringutil,
51 51 )
52 52
53 53 if pycompat.iswindows:
54 54 from . import scmwindows as scmplatform
55 55 else:
56 56 from . import scmposix as scmplatform
57 57
58 58 parsers = policy.importmod(r'parsers')
59 59
60 60 termsize = scmplatform.termsize
61 61
62 62 class status(tuple):
63 63 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
64 64 and 'ignored' properties are only relevant to the working copy.
65 65 '''
66 66
67 67 __slots__ = ()
68 68
69 69 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
70 70 clean):
71 71 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
72 72 ignored, clean))
73 73
74 74 @property
75 75 def modified(self):
76 76 '''files that have been modified'''
77 77 return self[0]
78 78
79 79 @property
80 80 def added(self):
81 81 '''files that have been added'''
82 82 return self[1]
83 83
84 84 @property
85 85 def removed(self):
86 86 '''files that have been removed'''
87 87 return self[2]
88 88
89 89 @property
90 90 def deleted(self):
91 91 '''files that are in the dirstate, but have been deleted from the
92 92 working copy (aka "missing")
93 93 '''
94 94 return self[3]
95 95
96 96 @property
97 97 def unknown(self):
98 98 '''files not in the dirstate that are not ignored'''
99 99 return self[4]
100 100
101 101 @property
102 102 def ignored(self):
103 103 '''files not in the dirstate that are ignored (by _dirignore())'''
104 104 return self[5]
105 105
106 106 @property
107 107 def clean(self):
108 108 '''files that have not been modified'''
109 109 return self[6]
110 110
111 111 def __repr__(self, *args, **kwargs):
112 112 return ((r'<status modified=%s, added=%s, removed=%s, deleted=%s, '
113 113 r'unknown=%s, ignored=%s, clean=%s>') %
114 114 tuple(pycompat.sysstr(stringutil.pprint(v)) for v in self))
115 115
116 116 def itersubrepos(ctx1, ctx2):
117 117 """find subrepos in ctx1 or ctx2"""
118 118 # Create a (subpath, ctx) mapping where we prefer subpaths from
119 119 # ctx1. The subpaths from ctx2 are important when the .hgsub file
120 120 # has been modified (in ctx2) but not yet committed (in ctx1).
121 121 subpaths = dict.fromkeys(ctx2.substate, ctx2)
122 122 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
123 123
124 124 missing = set()
125 125
126 126 for subpath in ctx2.substate:
127 127 if subpath not in ctx1.substate:
128 128 del subpaths[subpath]
129 129 missing.add(subpath)
130 130
131 131 for subpath, ctx in sorted(subpaths.iteritems()):
132 132 yield subpath, ctx.sub(subpath)
133 133
134 134 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
135 135 # status and diff will have an accurate result when it does
136 136 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
137 137 # against itself.
138 138 for subpath in missing:
139 139 yield subpath, ctx2.nullsub(subpath, ctx1)
140 140
141 141 def nochangesfound(ui, repo, excluded=None):
142 142 '''Report no changes for push/pull, excluded is None or a list of
143 143 nodes excluded from the push/pull.
144 144 '''
145 145 secretlist = []
146 146 if excluded:
147 147 for n in excluded:
148 148 ctx = repo[n]
149 149 if ctx.phase() >= phases.secret and not ctx.extinct():
150 150 secretlist.append(n)
151 151
152 152 if secretlist:
153 153 ui.status(_("no changes found (ignored %d secret changesets)\n")
154 154 % len(secretlist))
155 155 else:
156 156 ui.status(_("no changes found\n"))
157 157
158 158 def callcatch(ui, func):
159 159 """call func() with global exception handling
160 160
161 161 return func() if no exception happens. otherwise do some error handling
162 162 and return an exit code accordingly. does not handle all exceptions.
163 163 """
164 164 try:
165 165 try:
166 166 return func()
167 167 except: # re-raises
168 168 ui.traceback()
169 169 raise
170 170 # Global exception handling, alphabetically
171 171 # Mercurial-specific first, followed by built-in and library exceptions
172 172 except error.LockHeld as inst:
173 173 if inst.errno == errno.ETIMEDOUT:
174 174 reason = _('timed out waiting for lock held by %r') % (
175 175 pycompat.bytestr(inst.locker))
176 176 else:
177 177 reason = _('lock held by %r') % inst.locker
178 178 ui.error(_("abort: %s: %s\n") % (
179 179 inst.desc or stringutil.forcebytestr(inst.filename), reason))
180 180 if not inst.locker:
181 181 ui.error(_("(lock might be very busy)\n"))
182 182 except error.LockUnavailable as inst:
183 183 ui.error(_("abort: could not lock %s: %s\n") %
184 184 (inst.desc or stringutil.forcebytestr(inst.filename),
185 185 encoding.strtolocal(inst.strerror)))
186 186 except error.OutOfBandError as inst:
187 187 if inst.args:
188 188 msg = _("abort: remote error:\n")
189 189 else:
190 190 msg = _("abort: remote error\n")
191 191 ui.error(msg)
192 192 if inst.args:
193 193 ui.error(''.join(inst.args))
194 194 if inst.hint:
195 195 ui.error('(%s)\n' % inst.hint)
196 196 except error.RepoError as inst:
197 197 ui.error(_("abort: %s!\n") % inst)
198 198 if inst.hint:
199 199 ui.error(_("(%s)\n") % inst.hint)
200 200 except error.ResponseError as inst:
201 201 ui.error(_("abort: %s") % inst.args[0])
202 202 msg = inst.args[1]
203 203 if isinstance(msg, type(u'')):
204 204 msg = pycompat.sysbytes(msg)
205 205 if not isinstance(msg, bytes):
206 206 ui.error(" %r\n" % (msg,))
207 207 elif not msg:
208 208 ui.error(_(" empty string\n"))
209 209 else:
210 210 ui.error("\n%r\n" % pycompat.bytestr(stringutil.ellipsis(msg)))
211 211 except error.CensoredNodeError as inst:
212 212 ui.error(_("abort: file censored %s!\n") % inst)
213 213 except error.StorageError as inst:
214 214 ui.error(_("abort: %s!\n") % inst)
215 215 except error.InterventionRequired as inst:
216 216 ui.error("%s\n" % inst)
217 217 if inst.hint:
218 218 ui.error(_("(%s)\n") % inst.hint)
219 219 return 1
220 220 except error.WdirUnsupported:
221 221 ui.error(_("abort: working directory revision cannot be specified\n"))
222 222 except error.Abort as inst:
223 223 ui.error(_("abort: %s\n") % inst)
224 224 if inst.hint:
225 225 ui.error(_("(%s)\n") % inst.hint)
226 226 except ImportError as inst:
227 227 ui.error(_("abort: %s!\n") % stringutil.forcebytestr(inst))
228 228 m = stringutil.forcebytestr(inst).split()[-1]
229 229 if m in "mpatch bdiff".split():
230 230 ui.error(_("(did you forget to compile extensions?)\n"))
231 231 elif m in "zlib".split():
232 232 ui.error(_("(is your Python install correct?)\n"))
233 233 except IOError as inst:
234 234 if util.safehasattr(inst, "code"):
235 235 ui.error(_("abort: %s\n") % stringutil.forcebytestr(inst))
236 236 elif util.safehasattr(inst, "reason"):
237 237 try: # usually it is in the form (errno, strerror)
238 238 reason = inst.reason.args[1]
239 239 except (AttributeError, IndexError):
240 240 # it might be anything, for example a string
241 241 reason = inst.reason
242 242 if isinstance(reason, pycompat.unicode):
243 243 # SSLError of Python 2.7.9 contains a unicode
244 244 reason = encoding.unitolocal(reason)
245 245 ui.error(_("abort: error: %s\n") % reason)
246 246 elif (util.safehasattr(inst, "args")
247 247 and inst.args and inst.args[0] == errno.EPIPE):
248 248 pass
249 249 elif getattr(inst, "strerror", None):
250 250 if getattr(inst, "filename", None):
251 251 ui.error(_("abort: %s: %s\n") % (
252 252 encoding.strtolocal(inst.strerror),
253 253 stringutil.forcebytestr(inst.filename)))
254 254 else:
255 255 ui.error(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
256 256 else:
257 257 raise
258 258 except OSError as inst:
259 259 if getattr(inst, "filename", None) is not None:
260 260 ui.error(_("abort: %s: '%s'\n") % (
261 261 encoding.strtolocal(inst.strerror),
262 262 stringutil.forcebytestr(inst.filename)))
263 263 else:
264 264 ui.error(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
265 265 except MemoryError:
266 266 ui.error(_("abort: out of memory\n"))
267 267 except SystemExit as inst:
268 268 # Commands shouldn't sys.exit directly, but give a return code.
269 269 # Just in case catch this and and pass exit code to caller.
270 270 return inst.code
271 271 except socket.error as inst:
272 272 ui.error(_("abort: %s\n") % stringutil.forcebytestr(inst.args[-1]))
273 273
274 274 return -1
275 275
276 276 def checknewlabel(repo, lbl, kind):
277 277 # Do not use the "kind" parameter in ui output.
278 278 # It makes strings difficult to translate.
279 279 if lbl in ['tip', '.', 'null']:
280 280 raise error.Abort(_("the name '%s' is reserved") % lbl)
281 281 for c in (':', '\0', '\n', '\r'):
282 282 if c in lbl:
283 283 raise error.Abort(
284 284 _("%r cannot be used in a name") % pycompat.bytestr(c))
285 285 try:
286 286 int(lbl)
287 287 raise error.Abort(_("cannot use an integer as a name"))
288 288 except ValueError:
289 289 pass
290 290 if lbl.strip() != lbl:
291 291 raise error.Abort(_("leading or trailing whitespace in name %r") % lbl)
292 292
293 293 def checkfilename(f):
294 294 '''Check that the filename f is an acceptable filename for a tracked file'''
295 295 if '\r' in f or '\n' in f:
296 296 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r")
297 297 % pycompat.bytestr(f))
298 298
299 299 def checkportable(ui, f):
300 300 '''Check if filename f is portable and warn or abort depending on config'''
301 301 checkfilename(f)
302 302 abort, warn = checkportabilityalert(ui)
303 303 if abort or warn:
304 304 msg = util.checkwinfilename(f)
305 305 if msg:
306 306 msg = "%s: %s" % (msg, procutil.shellquote(f))
307 307 if abort:
308 308 raise error.Abort(msg)
309 309 ui.warn(_("warning: %s\n") % msg)
310 310
311 311 def checkportabilityalert(ui):
312 312 '''check if the user's config requests nothing, a warning, or abort for
313 313 non-portable filenames'''
314 314 val = ui.config('ui', 'portablefilenames')
315 315 lval = val.lower()
316 316 bval = stringutil.parsebool(val)
317 317 abort = pycompat.iswindows or lval == 'abort'
318 318 warn = bval or lval == 'warn'
319 319 if bval is None and not (warn or abort or lval == 'ignore'):
320 320 raise error.ConfigError(
321 321 _("ui.portablefilenames value is invalid ('%s')") % val)
322 322 return abort, warn
323 323
324 324 class casecollisionauditor(object):
325 325 def __init__(self, ui, abort, dirstate):
326 326 self._ui = ui
327 327 self._abort = abort
328 328 allfiles = '\0'.join(dirstate._map)
329 329 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
330 330 self._dirstate = dirstate
331 331 # The purpose of _newfiles is so that we don't complain about
332 332 # case collisions if someone were to call this object with the
333 333 # same filename twice.
334 334 self._newfiles = set()
335 335
336 336 def __call__(self, f):
337 337 if f in self._newfiles:
338 338 return
339 339 fl = encoding.lower(f)
340 340 if fl in self._loweredfiles and f not in self._dirstate:
341 341 msg = _('possible case-folding collision for %s') % f
342 342 if self._abort:
343 343 raise error.Abort(msg)
344 344 self._ui.warn(_("warning: %s\n") % msg)
345 345 self._loweredfiles.add(fl)
346 346 self._newfiles.add(f)
347 347
348 348 def filteredhash(repo, maxrev):
349 349 """build hash of filtered revisions in the current repoview.
350 350
351 351 Multiple caches perform up-to-date validation by checking that the
352 352 tiprev and tipnode stored in the cache file match the current repository.
353 353 However, this is not sufficient for validating repoviews because the set
354 354 of revisions in the view may change without the repository tiprev and
355 355 tipnode changing.
356 356
357 357 This function hashes all the revs filtered from the view and returns
358 358 that SHA-1 digest.
359 359 """
360 360 cl = repo.changelog
361 361 if not cl.filteredrevs:
362 362 return None
363 363 key = None
364 364 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
365 365 if revs:
366 366 s = hashlib.sha1()
367 367 for rev in revs:
368 368 s.update('%d;' % rev)
369 369 key = s.digest()
370 370 return key
371 371
372 372 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
373 373 '''yield every hg repository under path, always recursively.
374 374 The recurse flag will only control recursion into repo working dirs'''
375 375 def errhandler(err):
376 376 if err.filename == path:
377 377 raise err
378 378 samestat = getattr(os.path, 'samestat', None)
379 379 if followsym and samestat is not None:
380 380 def adddir(dirlst, dirname):
381 381 dirstat = os.stat(dirname)
382 382 match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
383 383 if not match:
384 384 dirlst.append(dirstat)
385 385 return not match
386 386 else:
387 387 followsym = False
388 388
389 389 if (seen_dirs is None) and followsym:
390 390 seen_dirs = []
391 391 adddir(seen_dirs, path)
392 392 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
393 393 dirs.sort()
394 394 if '.hg' in dirs:
395 395 yield root # found a repository
396 396 qroot = os.path.join(root, '.hg', 'patches')
397 397 if os.path.isdir(os.path.join(qroot, '.hg')):
398 398 yield qroot # we have a patch queue repo here
399 399 if recurse:
400 400 # avoid recursing inside the .hg directory
401 401 dirs.remove('.hg')
402 402 else:
403 403 dirs[:] = [] # don't descend further
404 404 elif followsym:
405 405 newdirs = []
406 406 for d in dirs:
407 407 fname = os.path.join(root, d)
408 408 if adddir(seen_dirs, fname):
409 409 if os.path.islink(fname):
410 410 for hgname in walkrepos(fname, True, seen_dirs):
411 411 yield hgname
412 412 else:
413 413 newdirs.append(d)
414 414 dirs[:] = newdirs
415 415
416 416 def binnode(ctx):
417 417 """Return binary node id for a given basectx"""
418 418 node = ctx.node()
419 419 if node is None:
420 420 return wdirid
421 421 return node
422 422
423 423 def intrev(ctx):
424 424 """Return integer for a given basectx that can be used in comparison or
425 425 arithmetic operation"""
426 426 rev = ctx.rev()
427 427 if rev is None:
428 428 return wdirrev
429 429 return rev
430 430
431 431 def formatchangeid(ctx):
432 432 """Format changectx as '{rev}:{node|formatnode}', which is the default
433 433 template provided by logcmdutil.changesettemplater"""
434 434 repo = ctx.repo()
435 435 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
436 436
437 437 def formatrevnode(ui, rev, node):
438 438 """Format given revision and node depending on the current verbosity"""
439 439 if ui.debugflag:
440 440 hexfunc = hex
441 441 else:
442 442 hexfunc = short
443 443 return '%d:%s' % (rev, hexfunc(node))
444 444
445 445 def resolvehexnodeidprefix(repo, prefix):
446 446 if (prefix.startswith('x') and
447 447 repo.ui.configbool('experimental', 'revisions.prefixhexnode')):
448 448 prefix = prefix[1:]
449 449 try:
450 450 # Uses unfiltered repo because it's faster when prefix is ambiguous/
451 451 # This matches the shortesthexnodeidprefix() function below.
452 452 node = repo.unfiltered().changelog._partialmatch(prefix)
453 453 except error.AmbiguousPrefixLookupError:
454 454 revset = repo.ui.config('experimental', 'revisions.disambiguatewithin')
455 455 if revset:
456 456 # Clear config to avoid infinite recursion
457 457 configoverrides = {('experimental',
458 458 'revisions.disambiguatewithin'): None}
459 459 with repo.ui.configoverride(configoverrides):
460 460 revs = repo.anyrevs([revset], user=True)
461 461 matches = []
462 462 for rev in revs:
463 463 node = repo.changelog.node(rev)
464 464 if hex(node).startswith(prefix):
465 465 matches.append(node)
466 466 if len(matches) == 1:
467 467 return matches[0]
468 468 raise
469 469 if node is None:
470 470 return
471 471 repo.changelog.rev(node) # make sure node isn't filtered
472 472 return node
473 473
474 474 def mayberevnum(repo, prefix):
475 475 """Checks if the given prefix may be mistaken for a revision number"""
476 476 try:
477 477 i = int(prefix)
478 478 # if we are a pure int, then starting with zero will not be
479 479 # confused as a rev; or, obviously, if the int is larger
480 480 # than the value of the tip rev. We still need to disambiguate if
481 481 # prefix == '0', since that *is* a valid revnum.
482 482 if (prefix != b'0' and prefix[0:1] == b'0') or i >= len(repo):
483 483 return False
484 484 return True
485 485 except ValueError:
486 486 return False
487 487
488 488 def shortesthexnodeidprefix(repo, node, minlength=1, cache=None):
489 489 """Find the shortest unambiguous prefix that matches hexnode.
490 490
491 491 If "cache" is not None, it must be a dictionary that can be used for
492 492 caching between calls to this method.
493 493 """
494 494 # _partialmatch() of filtered changelog could take O(len(repo)) time,
495 495 # which would be unacceptably slow. so we look for hash collision in
496 496 # unfiltered space, which means some hashes may be slightly longer.
497 497
498 498 minlength=max(minlength, 1)
499 499
500 500 def disambiguate(prefix):
501 501 """Disambiguate against revnums."""
502 502 if repo.ui.configbool('experimental', 'revisions.prefixhexnode'):
503 503 if mayberevnum(repo, prefix):
504 504 return 'x' + prefix
505 505 else:
506 506 return prefix
507 507
508 508 hexnode = hex(node)
509 509 for length in range(len(prefix), len(hexnode) + 1):
510 510 prefix = hexnode[:length]
511 511 if not mayberevnum(repo, prefix):
512 512 return prefix
513 513
514 514 cl = repo.unfiltered().changelog
515 515 revset = repo.ui.config('experimental', 'revisions.disambiguatewithin')
516 516 if revset:
517 517 revs = None
518 518 if cache is not None:
519 519 revs = cache.get('disambiguationrevset')
520 520 if revs is None:
521 521 revs = repo.anyrevs([revset], user=True)
522 522 if cache is not None:
523 523 cache['disambiguationrevset'] = revs
524 524 if cl.rev(node) in revs:
525 525 hexnode = hex(node)
526 526 nodetree = None
527 527 if cache is not None:
528 528 nodetree = cache.get('disambiguationnodetree')
529 529 if not nodetree:
530 530 try:
531 531 nodetree = parsers.nodetree(cl.index, len(revs))
532 532 except AttributeError:
533 533 # no native nodetree
534 534 pass
535 535 else:
536 536 for r in revs:
537 537 nodetree.insert(r)
538 538 if cache is not None:
539 539 cache['disambiguationnodetree'] = nodetree
540 540 if nodetree is not None:
541 541 length = max(nodetree.shortest(node), minlength)
542 542 prefix = hexnode[:length]
543 543 return disambiguate(prefix)
544 544 for length in range(minlength, len(hexnode) + 1):
545 545 matches = []
546 546 prefix = hexnode[:length]
547 547 for rev in revs:
548 548 otherhexnode = repo[rev].hex()
549 549 if prefix == otherhexnode[:length]:
550 550 matches.append(otherhexnode)
551 551 if len(matches) == 1:
552 552 return disambiguate(prefix)
553 553
554 554 try:
555 555 return disambiguate(cl.shortest(node, minlength))
556 556 except error.LookupError:
557 557 raise error.RepoLookupError()
558 558
559 559 def isrevsymbol(repo, symbol):
560 560 """Checks if a symbol exists in the repo.
561 561
562 562 See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the
563 563 symbol is an ambiguous nodeid prefix.
564 564 """
565 565 try:
566 566 revsymbol(repo, symbol)
567 567 return True
568 568 except error.RepoLookupError:
569 569 return False
570 570
571 571 def revsymbol(repo, symbol):
572 572 """Returns a context given a single revision symbol (as string).
573 573
574 574 This is similar to revsingle(), but accepts only a single revision symbol,
575 575 i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but
576 576 not "max(public())".
577 577 """
578 578 if not isinstance(symbol, bytes):
579 579 msg = ("symbol (%s of type %s) was not a string, did you mean "
580 580 "repo[symbol]?" % (symbol, type(symbol)))
581 581 raise error.ProgrammingError(msg)
582 582 try:
583 583 if symbol in ('.', 'tip', 'null'):
584 584 return repo[symbol]
585 585
586 586 try:
587 587 r = int(symbol)
588 588 if '%d' % r != symbol:
589 589 raise ValueError
590 590 l = len(repo.changelog)
591 591 if r < 0:
592 592 r += l
593 593 if r < 0 or r >= l and r != wdirrev:
594 594 raise ValueError
595 595 return repo[r]
596 596 except error.FilteredIndexError:
597 597 raise
598 598 except (ValueError, OverflowError, IndexError):
599 599 pass
600 600
601 601 if len(symbol) == 40:
602 602 try:
603 603 node = bin(symbol)
604 604 rev = repo.changelog.rev(node)
605 605 return repo[rev]
606 606 except error.FilteredLookupError:
607 607 raise
608 608 except (TypeError, LookupError):
609 609 pass
610 610
611 611 # look up bookmarks through the name interface
612 612 try:
613 613 node = repo.names.singlenode(repo, symbol)
614 614 rev = repo.changelog.rev(node)
615 615 return repo[rev]
616 616 except KeyError:
617 617 pass
618 618
619 619 node = resolvehexnodeidprefix(repo, symbol)
620 620 if node is not None:
621 621 rev = repo.changelog.rev(node)
622 622 return repo[rev]
623 623
624 624 raise error.RepoLookupError(_("unknown revision '%s'") % symbol)
625 625
626 626 except error.WdirUnsupported:
627 627 return repo[None]
628 628 except (error.FilteredIndexError, error.FilteredLookupError,
629 629 error.FilteredRepoLookupError):
630 630 raise _filterederror(repo, symbol)
631 631
632 632 def _filterederror(repo, changeid):
633 633 """build an exception to be raised about a filtered changeid
634 634
635 635 This is extracted in a function to help extensions (eg: evolve) to
636 636 experiment with various message variants."""
637 637 if repo.filtername.startswith('visible'):
638 638
639 639 # Check if the changeset is obsolete
640 640 unfilteredrepo = repo.unfiltered()
641 641 ctx = revsymbol(unfilteredrepo, changeid)
642 642
643 643 # If the changeset is obsolete, enrich the message with the reason
644 644 # that made this changeset not visible
645 645 if ctx.obsolete():
646 646 msg = obsutil._getfilteredreason(repo, changeid, ctx)
647 647 else:
648 648 msg = _("hidden revision '%s'") % changeid
649 649
650 650 hint = _('use --hidden to access hidden revisions')
651 651
652 652 return error.FilteredRepoLookupError(msg, hint=hint)
653 653 msg = _("filtered revision '%s' (not in '%s' subset)")
654 654 msg %= (changeid, repo.filtername)
655 655 return error.FilteredRepoLookupError(msg)
656 656
657 657 def revsingle(repo, revspec, default='.', localalias=None):
658 658 if not revspec and revspec != 0:
659 659 return repo[default]
660 660
661 661 l = revrange(repo, [revspec], localalias=localalias)
662 662 if not l:
663 663 raise error.Abort(_('empty revision set'))
664 664 return repo[l.last()]
665 665
666 666 def _pairspec(revspec):
667 667 tree = revsetlang.parse(revspec)
668 668 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
669 669
670 670 def revpair(repo, revs):
671 671 if not revs:
672 672 return repo['.'], repo[None]
673 673
674 674 l = revrange(repo, revs)
675 675
676 676 if not l:
677 677 first = second = None
678 678 elif l.isascending():
679 679 first = l.min()
680 680 second = l.max()
681 681 elif l.isdescending():
682 682 first = l.max()
683 683 second = l.min()
684 684 else:
685 685 first = l.first()
686 686 second = l.last()
687 687
688 688 if first is None:
689 689 raise error.Abort(_('empty revision range'))
690 690 if (first == second and len(revs) >= 2
691 691 and not all(revrange(repo, [r]) for r in revs)):
692 692 raise error.Abort(_('empty revision on one side of range'))
693 693
694 694 # if top-level is range expression, the result must always be a pair
695 695 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
696 696 return repo[first], repo[None]
697 697
698 698 return repo[first], repo[second]
699 699
700 700 def revrange(repo, specs, localalias=None):
701 701 """Execute 1 to many revsets and return the union.
702 702
703 703 This is the preferred mechanism for executing revsets using user-specified
704 704 config options, such as revset aliases.
705 705
706 706 The revsets specified by ``specs`` will be executed via a chained ``OR``
707 707 expression. If ``specs`` is empty, an empty result is returned.
708 708
709 709 ``specs`` can contain integers, in which case they are assumed to be
710 710 revision numbers.
711 711
712 712 It is assumed the revsets are already formatted. If you have arguments
713 713 that need to be expanded in the revset, call ``revsetlang.formatspec()``
714 714 and pass the result as an element of ``specs``.
715 715
716 716 Specifying a single revset is allowed.
717 717
718 718 Returns a ``revset.abstractsmartset`` which is a list-like interface over
719 719 integer revisions.
720 720 """
721 721 allspecs = []
722 722 for spec in specs:
723 723 if isinstance(spec, int):
724 724 spec = revsetlang.formatspec('rev(%d)', spec)
725 725 allspecs.append(spec)
726 726 return repo.anyrevs(allspecs, user=True, localalias=localalias)
727 727
728 728 def meaningfulparents(repo, ctx):
729 729 """Return list of meaningful (or all if debug) parentrevs for rev.
730 730
731 731 For merges (two non-nullrev revisions) both parents are meaningful.
732 732 Otherwise the first parent revision is considered meaningful if it
733 733 is not the preceding revision.
734 734 """
735 735 parents = ctx.parents()
736 736 if len(parents) > 1:
737 737 return parents
738 738 if repo.ui.debugflag:
739 739 return [parents[0], repo[nullrev]]
740 740 if parents[0].rev() >= intrev(ctx) - 1:
741 741 return []
742 742 return parents
743 743
744 744 def expandpats(pats):
745 745 '''Expand bare globs when running on windows.
746 746 On posix we assume it already has already been done by sh.'''
747 747 if not util.expandglobs:
748 748 return list(pats)
749 749 ret = []
750 750 for kindpat in pats:
751 751 kind, pat = matchmod._patsplit(kindpat, None)
752 752 if kind is None:
753 753 try:
754 754 globbed = glob.glob(pat)
755 755 except re.error:
756 756 globbed = [pat]
757 757 if globbed:
758 758 ret.extend(globbed)
759 759 continue
760 760 ret.append(kindpat)
761 761 return ret
762 762
763 763 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
764 764 badfn=None):
765 765 '''Return a matcher and the patterns that were used.
766 766 The matcher will warn about bad matches, unless an alternate badfn callback
767 767 is provided.'''
768 768 if pats == ("",):
769 769 pats = []
770 770 if opts is None:
771 771 opts = {}
772 772 if not globbed and default == 'relpath':
773 773 pats = expandpats(pats or [])
774 774
775 775 def bad(f, msg):
776 776 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
777 777
778 778 if badfn is None:
779 779 badfn = bad
780 780
781 781 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
782 782 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
783 783
784 784 if m.always():
785 785 pats = []
786 786 return m, pats
787 787
788 788 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
789 789 badfn=None):
790 790 '''Return a matcher that will warn about bad matches.'''
791 791 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
792 792
793 793 def matchall(repo):
794 794 '''Return a matcher that will efficiently match everything.'''
795 795 return matchmod.always(repo.root, repo.getcwd())
796 796
797 797 def matchfiles(repo, files, badfn=None):
798 798 '''Return a matcher that will efficiently match exactly these files.'''
799 799 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
800 800
801 801 def parsefollowlinespattern(repo, rev, pat, msg):
802 802 """Return a file name from `pat` pattern suitable for usage in followlines
803 803 logic.
804 804 """
805 805 if not matchmod.patkind(pat):
806 806 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
807 807 else:
808 808 ctx = repo[rev]
809 809 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
810 810 files = [f for f in ctx if m(f)]
811 811 if len(files) != 1:
812 812 raise error.ParseError(msg)
813 813 return files[0]
814 814
815 815 def origpath(ui, repo, filepath):
816 816 '''customize where .orig files are created
817 817
818 818 Fetch user defined path from config file: [ui] origbackuppath = <path>
819 819 Fall back to default (filepath with .orig suffix) if not specified
820 820 '''
821 821 origbackuppath = ui.config('ui', 'origbackuppath')
822 822 if not origbackuppath:
823 823 return filepath + ".orig"
824 824
825 825 # Convert filepath from an absolute path into a path inside the repo.
826 826 filepathfromroot = util.normpath(os.path.relpath(filepath,
827 827 start=repo.root))
828 828
829 829 origvfs = vfs.vfs(repo.wjoin(origbackuppath))
830 830 origbackupdir = origvfs.dirname(filepathfromroot)
831 831 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
832 832 ui.note(_('creating directory: %s\n') % origvfs.join(origbackupdir))
833 833
834 834 # Remove any files that conflict with the backup file's path
835 835 for f in reversed(list(util.finddirs(filepathfromroot))):
836 836 if origvfs.isfileorlink(f):
837 837 ui.note(_('removing conflicting file: %s\n')
838 838 % origvfs.join(f))
839 839 origvfs.unlink(f)
840 840 break
841 841
842 842 origvfs.makedirs(origbackupdir)
843 843
844 844 if origvfs.isdir(filepathfromroot) and not origvfs.islink(filepathfromroot):
845 845 ui.note(_('removing conflicting directory: %s\n')
846 846 % origvfs.join(filepathfromroot))
847 847 origvfs.rmtree(filepathfromroot, forcibly=True)
848 848
849 849 return origvfs.join(filepathfromroot)
850 850
851 851 class _containsnode(object):
852 852 """proxy __contains__(node) to container.__contains__ which accepts revs"""
853 853
854 854 def __init__(self, repo, revcontainer):
855 855 self._torev = repo.changelog.rev
856 856 self._revcontains = revcontainer.__contains__
857 857
858 858 def __contains__(self, node):
859 859 return self._revcontains(self._torev(node))
860 860
861 861 def cleanupnodes(repo, replacements, operation, moves=None, metadata=None,
862 862 fixphase=False, targetphase=None, backup=True):
863 863 """do common cleanups when old nodes are replaced by new nodes
864 864
865 865 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
866 866 (we might also want to move working directory parent in the future)
867 867
868 868 By default, bookmark moves are calculated automatically from 'replacements',
869 869 but 'moves' can be used to override that. Also, 'moves' may include
870 870 additional bookmark moves that should not have associated obsmarkers.
871 871
872 872 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
873 873 have replacements. operation is a string, like "rebase".
874 874
875 875 metadata is dictionary containing metadata to be stored in obsmarker if
876 876 obsolescence is enabled.
877 877 """
878 878 assert fixphase or targetphase is None
879 879 if not replacements and not moves:
880 880 return
881 881
882 882 # translate mapping's other forms
883 883 if not util.safehasattr(replacements, 'items'):
884 884 replacements = {(n,): () for n in replacements}
885 885 else:
886 886 # upgrading non tuple "source" to tuple ones for BC
887 887 repls = {}
888 888 for key, value in replacements.items():
889 889 if not isinstance(key, tuple):
890 890 key = (key,)
891 891 repls[key] = value
892 892 replacements = repls
893 893
894 894 # Calculate bookmark movements
895 895 if moves is None:
896 896 moves = {}
897 897 # Unfiltered repo is needed since nodes in replacements might be hidden.
898 898 unfi = repo.unfiltered()
899 899 for oldnodes, newnodes in replacements.items():
900 900 for oldnode in oldnodes:
901 901 if oldnode in moves:
902 902 continue
903 903 if len(newnodes) > 1:
904 904 # usually a split, take the one with biggest rev number
905 905 newnode = next(unfi.set('max(%ln)', newnodes)).node()
906 906 elif len(newnodes) == 0:
907 907 # move bookmark backwards
908 908 allreplaced = []
909 909 for rep in replacements:
910 910 allreplaced.extend(rep)
911 911 roots = list(unfi.set('max((::%n) - %ln)', oldnode,
912 912 allreplaced))
913 913 if roots:
914 914 newnode = roots[0].node()
915 915 else:
916 916 newnode = nullid
917 917 else:
918 918 newnode = newnodes[0]
919 919 moves[oldnode] = newnode
920 920
921 921 allnewnodes = [n for ns in replacements.values() for n in ns]
922 922 toretract = {}
923 923 toadvance = {}
924 924 if fixphase:
925 925 precursors = {}
926 926 for oldnodes, newnodes in replacements.items():
927 927 for oldnode in oldnodes:
928 928 for newnode in newnodes:
929 929 precursors.setdefault(newnode, []).append(oldnode)
930 930
931 931 allnewnodes.sort(key=lambda n: unfi[n].rev())
932 932 newphases = {}
933 933 def phase(ctx):
934 934 return newphases.get(ctx.node(), ctx.phase())
935 935 for newnode in allnewnodes:
936 936 ctx = unfi[newnode]
937 937 parentphase = max(phase(p) for p in ctx.parents())
938 938 if targetphase is None:
939 939 oldphase = max(unfi[oldnode].phase()
940 940 for oldnode in precursors[newnode])
941 941 newphase = max(oldphase, parentphase)
942 942 else:
943 943 newphase = max(targetphase, parentphase)
944 944 newphases[newnode] = newphase
945 945 if newphase > ctx.phase():
946 946 toretract.setdefault(newphase, []).append(newnode)
947 947 elif newphase < ctx.phase():
948 948 toadvance.setdefault(newphase, []).append(newnode)
949 949
950 950 with repo.transaction('cleanup') as tr:
951 951 # Move bookmarks
952 952 bmarks = repo._bookmarks
953 953 bmarkchanges = []
954 954 for oldnode, newnode in moves.items():
955 955 oldbmarks = repo.nodebookmarks(oldnode)
956 956 if not oldbmarks:
957 957 continue
958 958 from . import bookmarks # avoid import cycle
959 959 repo.ui.debug('moving bookmarks %r from %s to %s\n' %
960 960 (pycompat.rapply(pycompat.maybebytestr, oldbmarks),
961 961 hex(oldnode), hex(newnode)))
962 962 # Delete divergent bookmarks being parents of related newnodes
963 963 deleterevs = repo.revs('parents(roots(%ln & (::%n))) - parents(%n)',
964 964 allnewnodes, newnode, oldnode)
965 965 deletenodes = _containsnode(repo, deleterevs)
966 966 for name in oldbmarks:
967 967 bmarkchanges.append((name, newnode))
968 968 for b in bookmarks.divergent2delete(repo, deletenodes, name):
969 969 bmarkchanges.append((b, None))
970 970
971 971 if bmarkchanges:
972 972 bmarks.applychanges(repo, tr, bmarkchanges)
973 973
974 974 for phase, nodes in toretract.items():
975 975 phases.retractboundary(repo, tr, phase, nodes)
976 976 for phase, nodes in toadvance.items():
977 977 phases.advanceboundary(repo, tr, phase, nodes)
978 978
979 979 # Obsolete or strip nodes
980 980 if obsolete.isenabled(repo, obsolete.createmarkersopt):
981 981 # If a node is already obsoleted, and we want to obsolete it
982 982 # without a successor, skip that obssolete request since it's
983 983 # unnecessary. That's the "if s or not isobs(n)" check below.
984 984 # Also sort the node in topology order, that might be useful for
985 985 # some obsstore logic.
986 986 # NOTE: the sorting might belong to createmarkers.
987 987 torev = unfi.changelog.rev
988 988 sortfunc = lambda ns: torev(ns[0][0])
989 989 rels = []
990 990 for ns, s in sorted(replacements.items(), key=sortfunc):
991 991 rel = (tuple(unfi[n] for n in ns), tuple(unfi[m] for m in s))
992 992 rels.append(rel)
993 993 if rels:
994 994 obsolete.createmarkers(repo, rels, operation=operation,
995 995 metadata=metadata)
996 996 else:
997 997 from . import repair # avoid import cycle
998 998 tostrip = list(n for ns in replacements for n in ns)
999 999 if tostrip:
1000 1000 repair.delayedstrip(repo.ui, repo, tostrip, operation,
1001 1001 backup=backup)
1002 1002
1003 1003 def addremove(repo, matcher, prefix, opts=None):
1004 1004 if opts is None:
1005 1005 opts = {}
1006 1006 m = matcher
1007 1007 dry_run = opts.get('dry_run')
1008 1008 try:
1009 1009 similarity = float(opts.get('similarity') or 0)
1010 1010 except ValueError:
1011 1011 raise error.Abort(_('similarity must be a number'))
1012 1012 if similarity < 0 or similarity > 100:
1013 1013 raise error.Abort(_('similarity must be between 0 and 100'))
1014 1014 similarity /= 100.0
1015 1015
1016 1016 ret = 0
1017 1017 join = lambda f: os.path.join(prefix, f)
1018 1018
1019 1019 wctx = repo[None]
1020 1020 for subpath in sorted(wctx.substate):
1021 1021 submatch = matchmod.subdirmatcher(subpath, m)
1022 1022 if opts.get('subrepos') or m.exact(subpath) or any(submatch.files()):
1023 1023 sub = wctx.sub(subpath)
1024 1024 try:
1025 1025 if sub.addremove(submatch, prefix, opts):
1026 1026 ret = 1
1027 1027 except error.LookupError:
1028 1028 repo.ui.status(_("skipping missing subrepository: %s\n")
1029 1029 % join(subpath))
1030 1030
1031 1031 rejected = []
1032 1032 def badfn(f, msg):
1033 1033 if f in m.files():
1034 1034 m.bad(f, msg)
1035 1035 rejected.append(f)
1036 1036
1037 1037 badmatch = matchmod.badmatch(m, badfn)
1038 1038 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
1039 1039 badmatch)
1040 1040
1041 1041 unknownset = set(unknown + forgotten)
1042 1042 toprint = unknownset.copy()
1043 1043 toprint.update(deleted)
1044 1044 for abs in sorted(toprint):
1045 1045 if repo.ui.verbose or not m.exact(abs):
1046 1046 if abs in unknownset:
1047 1047 status = _('adding %s\n') % m.uipath(abs)
1048 1048 label = 'ui.addremove.added'
1049 1049 else:
1050 1050 status = _('removing %s\n') % m.uipath(abs)
1051 1051 label = 'ui.addremove.removed'
1052 1052 repo.ui.status(status, label=label)
1053 1053
1054 1054 renames = _findrenames(repo, m, added + unknown, removed + deleted,
1055 1055 similarity)
1056 1056
1057 1057 if not dry_run:
1058 1058 _markchanges(repo, unknown + forgotten, deleted, renames)
1059 1059
1060 1060 for f in rejected:
1061 1061 if f in m.files():
1062 1062 return 1
1063 1063 return ret
1064 1064
1065 1065 def marktouched(repo, files, similarity=0.0):
1066 1066 '''Assert that files have somehow been operated upon. files are relative to
1067 1067 the repo root.'''
1068 1068 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
1069 1069 rejected = []
1070 1070
1071 1071 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
1072 1072
1073 1073 if repo.ui.verbose:
1074 1074 unknownset = set(unknown + forgotten)
1075 1075 toprint = unknownset.copy()
1076 1076 toprint.update(deleted)
1077 1077 for abs in sorted(toprint):
1078 1078 if abs in unknownset:
1079 1079 status = _('adding %s\n') % abs
1080 1080 else:
1081 1081 status = _('removing %s\n') % abs
1082 1082 repo.ui.status(status)
1083 1083
1084 1084 renames = _findrenames(repo, m, added + unknown, removed + deleted,
1085 1085 similarity)
1086 1086
1087 1087 _markchanges(repo, unknown + forgotten, deleted, renames)
1088 1088
1089 1089 for f in rejected:
1090 1090 if f in m.files():
1091 1091 return 1
1092 1092 return 0
1093 1093
1094 1094 def _interestingfiles(repo, matcher):
1095 1095 '''Walk dirstate with matcher, looking for files that addremove would care
1096 1096 about.
1097 1097
1098 1098 This is different from dirstate.status because it doesn't care about
1099 1099 whether files are modified or clean.'''
1100 1100 added, unknown, deleted, removed, forgotten = [], [], [], [], []
1101 1101 audit_path = pathutil.pathauditor(repo.root, cached=True)
1102 1102
1103 1103 ctx = repo[None]
1104 1104 dirstate = repo.dirstate
1105 1105 matcher = repo.narrowmatch(matcher, includeexact=True)
1106 1106 walkresults = dirstate.walk(matcher, subrepos=sorted(ctx.substate),
1107 1107 unknown=True, ignored=False, full=False)
1108 1108 for abs, st in walkresults.iteritems():
1109 1109 dstate = dirstate[abs]
1110 1110 if dstate == '?' and audit_path.check(abs):
1111 1111 unknown.append(abs)
1112 1112 elif dstate != 'r' and not st:
1113 1113 deleted.append(abs)
1114 1114 elif dstate == 'r' and st:
1115 1115 forgotten.append(abs)
1116 1116 # for finding renames
1117 1117 elif dstate == 'r' and not st:
1118 1118 removed.append(abs)
1119 1119 elif dstate == 'a':
1120 1120 added.append(abs)
1121 1121
1122 1122 return added, unknown, deleted, removed, forgotten
1123 1123
1124 1124 def _findrenames(repo, matcher, added, removed, similarity):
1125 1125 '''Find renames from removed files to added ones.'''
1126 1126 renames = {}
1127 1127 if similarity > 0:
1128 1128 for old, new, score in similar.findrenames(repo, added, removed,
1129 1129 similarity):
1130 1130 if (repo.ui.verbose or not matcher.exact(old)
1131 1131 or not matcher.exact(new)):
1132 1132 repo.ui.status(_('recording removal of %s as rename to %s '
1133 1133 '(%d%% similar)\n') %
1134 1134 (matcher.rel(old), matcher.rel(new),
1135 1135 score * 100))
1136 1136 renames[new] = old
1137 1137 return renames
1138 1138
1139 1139 def _markchanges(repo, unknown, deleted, renames):
1140 1140 '''Marks the files in unknown as added, the files in deleted as removed,
1141 1141 and the files in renames as copied.'''
1142 1142 wctx = repo[None]
1143 1143 with repo.wlock():
1144 1144 wctx.forget(deleted)
1145 1145 wctx.add(unknown)
1146 1146 for new, old in renames.iteritems():
1147 1147 wctx.copy(old, new)
1148 1148
1149 1149 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
1150 1150 """Update the dirstate to reflect the intent of copying src to dst. For
1151 1151 different reasons it might not end with dst being marked as copied from src.
1152 1152 """
1153 1153 origsrc = repo.dirstate.copied(src) or src
1154 1154 if dst == origsrc: # copying back a copy?
1155 1155 if repo.dirstate[dst] not in 'mn' and not dryrun:
1156 1156 repo.dirstate.normallookup(dst)
1157 1157 else:
1158 1158 if repo.dirstate[origsrc] == 'a' and origsrc == src:
1159 1159 if not ui.quiet:
1160 1160 ui.warn(_("%s has not been committed yet, so no copy "
1161 1161 "data will be stored for %s.\n")
1162 1162 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
1163 1163 if repo.dirstate[dst] in '?r' and not dryrun:
1164 1164 wctx.add([dst])
1165 1165 elif not dryrun:
1166 1166 wctx.copy(origsrc, dst)
1167 1167
1168 1168 def writerequires(opener, requirements):
1169 1169 with opener('requires', 'w') as fp:
1170 1170 for r in sorted(requirements):
1171 1171 fp.write("%s\n" % r)
1172 1172
1173 1173 class filecachesubentry(object):
1174 1174 def __init__(self, path, stat):
1175 1175 self.path = path
1176 1176 self.cachestat = None
1177 1177 self._cacheable = None
1178 1178
1179 1179 if stat:
1180 1180 self.cachestat = filecachesubentry.stat(self.path)
1181 1181
1182 1182 if self.cachestat:
1183 1183 self._cacheable = self.cachestat.cacheable()
1184 1184 else:
1185 1185 # None means we don't know yet
1186 1186 self._cacheable = None
1187 1187
1188 1188 def refresh(self):
1189 1189 if self.cacheable():
1190 1190 self.cachestat = filecachesubentry.stat(self.path)
1191 1191
1192 1192 def cacheable(self):
1193 1193 if self._cacheable is not None:
1194 1194 return self._cacheable
1195 1195
1196 1196 # we don't know yet, assume it is for now
1197 1197 return True
1198 1198
1199 1199 def changed(self):
1200 1200 # no point in going further if we can't cache it
1201 1201 if not self.cacheable():
1202 1202 return True
1203 1203
1204 1204 newstat = filecachesubentry.stat(self.path)
1205 1205
1206 1206 # we may not know if it's cacheable yet, check again now
1207 1207 if newstat and self._cacheable is None:
1208 1208 self._cacheable = newstat.cacheable()
1209 1209
1210 1210 # check again
1211 1211 if not self._cacheable:
1212 1212 return True
1213 1213
1214 1214 if self.cachestat != newstat:
1215 1215 self.cachestat = newstat
1216 1216 return True
1217 1217 else:
1218 1218 return False
1219 1219
1220 1220 @staticmethod
1221 1221 def stat(path):
1222 1222 try:
1223 1223 return util.cachestat(path)
1224 1224 except OSError as e:
1225 1225 if e.errno != errno.ENOENT:
1226 1226 raise
1227 1227
1228 1228 class filecacheentry(object):
1229 1229 def __init__(self, paths, stat=True):
1230 1230 self._entries = []
1231 1231 for path in paths:
1232 1232 self._entries.append(filecachesubentry(path, stat))
1233 1233
1234 1234 def changed(self):
1235 1235 '''true if any entry has changed'''
1236 1236 for entry in self._entries:
1237 1237 if entry.changed():
1238 1238 return True
1239 1239 return False
1240 1240
1241 1241 def refresh(self):
1242 1242 for entry in self._entries:
1243 1243 entry.refresh()
1244 1244
1245 1245 class filecache(object):
1246 1246 """A property like decorator that tracks files under .hg/ for updates.
1247 1247
1248 1248 On first access, the files defined as arguments are stat()ed and the
1249 1249 results cached. The decorated function is called. The results are stashed
1250 1250 away in a ``_filecache`` dict on the object whose method is decorated.
1251 1251
1252 On subsequent access, the cached result is returned.
1253
1254 On external property set operations, stat() calls are performed and the new
1255 value is cached.
1252 On subsequent access, the cached result is used as it is set to the
1253 instance dictionary.
1256 1254
1257 On property delete operations, cached data is removed.
1255 On external property set/delete operations, the caller must update the
1256 corresponding _filecache entry appropriately. Use __class__.<attr>.set()
1257 instead of directly setting <attr>.
1258 1258
1259 When using the property API, cached data is always returned, if available:
1260 no stat() is performed to check if the file has changed and if the function
1261 needs to be called to reflect file changes.
1259 When using the property API, the cached data is always used if available.
1260 No stat() is performed to check if the file has changed.
1262 1261
1263 1262 Others can muck about with the state of the ``_filecache`` dict. e.g. they
1264 1263 can populate an entry before the property's getter is called. In this case,
1265 1264 entries in ``_filecache`` will be used during property operations,
1266 1265 if available. If the underlying file changes, it is up to external callers
1267 1266 to reflect this by e.g. calling ``delattr(obj, attr)`` to remove the cached
1268 1267 method result as well as possibly calling ``del obj._filecache[attr]`` to
1269 1268 remove the ``filecacheentry``.
1270 1269 """
1271 1270
1272 1271 def __init__(self, *paths):
1273 1272 self.paths = paths
1274 1273
1275 1274 def join(self, obj, fname):
1276 1275 """Used to compute the runtime path of a cached file.
1277 1276
1278 1277 Users should subclass filecache and provide their own version of this
1279 1278 function to call the appropriate join function on 'obj' (an instance
1280 1279 of the class that its member function was decorated).
1281 1280 """
1282 1281 raise NotImplementedError
1283 1282
1284 1283 def __call__(self, func):
1285 1284 self.func = func
1286 1285 self.sname = func.__name__
1287 1286 self.name = pycompat.sysbytes(self.sname)
1288 1287 return self
1289 1288
1290 1289 def __get__(self, obj, type=None):
1291 1290 # if accessed on the class, return the descriptor itself.
1292 1291 if obj is None:
1293 1292 return self
1294 # do we need to check if the file changed?
1295 try:
1296 return obj.__dict__[self.sname]
1297 except KeyError:
1298 pass
1293
1294 assert self.sname not in obj.__dict__
1299 1295
1300 1296 entry = obj._filecache.get(self.name)
1301 1297
1302 1298 if entry:
1303 1299 if entry.changed():
1304 1300 entry.obj = self.func(obj)
1305 1301 else:
1306 1302 paths = [self.join(obj, path) for path in self.paths]
1307 1303
1308 1304 # We stat -before- creating the object so our cache doesn't lie if
1309 1305 # a writer modified between the time we read and stat
1310 1306 entry = filecacheentry(paths, True)
1311 1307 entry.obj = self.func(obj)
1312 1308
1313 1309 obj._filecache[self.name] = entry
1314 1310
1315 1311 obj.__dict__[self.sname] = entry.obj
1316 1312 return entry.obj
1317 1313
1318 def __set__(self, obj, value):
1314 # don't implement __set__(), which would make __dict__ lookup as slow as
1315 # function call.
1316
1317 def set(self, obj, value):
1319 1318 if self.name not in obj._filecache:
1320 1319 # we add an entry for the missing value because X in __dict__
1321 1320 # implies X in _filecache
1322 1321 paths = [self.join(obj, path) for path in self.paths]
1323 1322 ce = filecacheentry(paths, False)
1324 1323 obj._filecache[self.name] = ce
1325 1324 else:
1326 1325 ce = obj._filecache[self.name]
1327 1326
1328 1327 ce.obj = value # update cached copy
1329 1328 obj.__dict__[self.sname] = value # update copy returned by obj.x
1330 1329
1331 def __delete__(self, obj):
1332 try:
1333 del obj.__dict__[self.sname]
1334 except KeyError:
1335 raise AttributeError(self.sname)
1336
1337 1330 def extdatasource(repo, source):
1338 1331 """Gather a map of rev -> value dict from the specified source
1339 1332
1340 1333 A source spec is treated as a URL, with a special case shell: type
1341 1334 for parsing the output from a shell command.
1342 1335
1343 1336 The data is parsed as a series of newline-separated records where
1344 1337 each record is a revision specifier optionally followed by a space
1345 1338 and a freeform string value. If the revision is known locally, it
1346 1339 is converted to a rev, otherwise the record is skipped.
1347 1340
1348 1341 Note that both key and value are treated as UTF-8 and converted to
1349 1342 the local encoding. This allows uniformity between local and
1350 1343 remote data sources.
1351 1344 """
1352 1345
1353 1346 spec = repo.ui.config("extdata", source)
1354 1347 if not spec:
1355 1348 raise error.Abort(_("unknown extdata source '%s'") % source)
1356 1349
1357 1350 data = {}
1358 1351 src = proc = None
1359 1352 try:
1360 1353 if spec.startswith("shell:"):
1361 1354 # external commands should be run relative to the repo root
1362 1355 cmd = spec[6:]
1363 1356 proc = subprocess.Popen(procutil.tonativestr(cmd),
1364 1357 shell=True, bufsize=-1,
1365 1358 close_fds=procutil.closefds,
1366 1359 stdout=subprocess.PIPE,
1367 1360 cwd=procutil.tonativestr(repo.root))
1368 1361 src = proc.stdout
1369 1362 else:
1370 1363 # treat as a URL or file
1371 1364 src = url.open(repo.ui, spec)
1372 1365 for l in src:
1373 1366 if " " in l:
1374 1367 k, v = l.strip().split(" ", 1)
1375 1368 else:
1376 1369 k, v = l.strip(), ""
1377 1370
1378 1371 k = encoding.tolocal(k)
1379 1372 try:
1380 1373 data[revsingle(repo, k).rev()] = encoding.tolocal(v)
1381 1374 except (error.LookupError, error.RepoLookupError):
1382 1375 pass # we ignore data for nodes that don't exist locally
1383 1376 finally:
1384 1377 if proc:
1385 1378 proc.communicate()
1386 1379 if src:
1387 1380 src.close()
1388 1381 if proc and proc.returncode != 0:
1389 1382 raise error.Abort(_("extdata command '%s' failed: %s")
1390 1383 % (cmd, procutil.explainexit(proc.returncode)))
1391 1384
1392 1385 return data
1393 1386
1394 1387 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1395 1388 if lock is None:
1396 1389 raise error.LockInheritanceContractViolation(
1397 1390 'lock can only be inherited while held')
1398 1391 if environ is None:
1399 1392 environ = {}
1400 1393 with lock.inherit() as locker:
1401 1394 environ[envvar] = locker
1402 1395 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1403 1396
1404 1397 def wlocksub(repo, cmd, *args, **kwargs):
1405 1398 """run cmd as a subprocess that allows inheriting repo's wlock
1406 1399
1407 1400 This can only be called while the wlock is held. This takes all the
1408 1401 arguments that ui.system does, and returns the exit code of the
1409 1402 subprocess."""
1410 1403 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
1411 1404 **kwargs)
1412 1405
1413 1406 class progress(object):
1414 1407 def __init__(self, ui, topic, unit="", total=None):
1415 1408 self.ui = ui
1416 1409 self.pos = 0
1417 1410 self.topic = topic
1418 1411 self.unit = unit
1419 1412 self.total = total
1420 1413
1421 1414 def __enter__(self):
1422 1415 return self
1423 1416
1424 1417 def __exit__(self, exc_type, exc_value, exc_tb):
1425 1418 self.complete()
1426 1419
1427 1420 def update(self, pos, item="", total=None):
1428 1421 assert pos is not None
1429 1422 if total:
1430 1423 self.total = total
1431 1424 self.pos = pos
1432 1425 self._print(item)
1433 1426
1434 1427 def increment(self, step=1, item="", total=None):
1435 1428 self.update(self.pos + step, item, total)
1436 1429
1437 1430 def complete(self):
1438 1431 self.ui.progress(self.topic, None)
1439 1432
1440 1433 def _print(self, item):
1441 1434 self.ui.progress(self.topic, self.pos, item, self.unit,
1442 1435 self.total)
1443 1436
1444 1437 def gdinitconfig(ui):
1445 1438 """helper function to know if a repo should be created as general delta
1446 1439 """
1447 1440 # experimental config: format.generaldelta
1448 1441 return (ui.configbool('format', 'generaldelta')
1449 1442 or ui.configbool('format', 'usegeneraldelta')
1450 1443 or ui.configbool('format', 'sparse-revlog'))
1451 1444
1452 1445 def gddeltaconfig(ui):
1453 1446 """helper function to know if incoming delta should be optimised
1454 1447 """
1455 1448 # experimental config: format.generaldelta
1456 1449 return ui.configbool('format', 'generaldelta')
1457 1450
1458 1451 class simplekeyvaluefile(object):
1459 1452 """A simple file with key=value lines
1460 1453
1461 1454 Keys must be alphanumerics and start with a letter, values must not
1462 1455 contain '\n' characters"""
1463 1456 firstlinekey = '__firstline'
1464 1457
1465 1458 def __init__(self, vfs, path, keys=None):
1466 1459 self.vfs = vfs
1467 1460 self.path = path
1468 1461
1469 1462 def read(self, firstlinenonkeyval=False):
1470 1463 """Read the contents of a simple key-value file
1471 1464
1472 1465 'firstlinenonkeyval' indicates whether the first line of file should
1473 1466 be treated as a key-value pair or reuturned fully under the
1474 1467 __firstline key."""
1475 1468 lines = self.vfs.readlines(self.path)
1476 1469 d = {}
1477 1470 if firstlinenonkeyval:
1478 1471 if not lines:
1479 1472 e = _("empty simplekeyvalue file")
1480 1473 raise error.CorruptedState(e)
1481 1474 # we don't want to include '\n' in the __firstline
1482 1475 d[self.firstlinekey] = lines[0][:-1]
1483 1476 del lines[0]
1484 1477
1485 1478 try:
1486 1479 # the 'if line.strip()' part prevents us from failing on empty
1487 1480 # lines which only contain '\n' therefore are not skipped
1488 1481 # by 'if line'
1489 1482 updatedict = dict(line[:-1].split('=', 1) for line in lines
1490 1483 if line.strip())
1491 1484 if self.firstlinekey in updatedict:
1492 1485 e = _("%r can't be used as a key")
1493 1486 raise error.CorruptedState(e % self.firstlinekey)
1494 1487 d.update(updatedict)
1495 1488 except ValueError as e:
1496 1489 raise error.CorruptedState(str(e))
1497 1490 return d
1498 1491
1499 1492 def write(self, data, firstline=None):
1500 1493 """Write key=>value mapping to a file
1501 1494 data is a dict. Keys must be alphanumerical and start with a letter.
1502 1495 Values must not contain newline characters.
1503 1496
1504 1497 If 'firstline' is not None, it is written to file before
1505 1498 everything else, as it is, not in a key=value form"""
1506 1499 lines = []
1507 1500 if firstline is not None:
1508 1501 lines.append('%s\n' % firstline)
1509 1502
1510 1503 for k, v in data.items():
1511 1504 if k == self.firstlinekey:
1512 1505 e = "key name '%s' is reserved" % self.firstlinekey
1513 1506 raise error.ProgrammingError(e)
1514 1507 if not k[0:1].isalpha():
1515 1508 e = "keys must start with a letter in a key-value file"
1516 1509 raise error.ProgrammingError(e)
1517 1510 if not k.isalnum():
1518 1511 e = "invalid key name in a simple key-value file"
1519 1512 raise error.ProgrammingError(e)
1520 1513 if '\n' in v:
1521 1514 e = "invalid value in a simple key-value file"
1522 1515 raise error.ProgrammingError(e)
1523 1516 lines.append("%s=%s\n" % (k, v))
1524 1517 with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
1525 1518 fp.write(''.join(lines))
1526 1519
1527 1520 _reportobsoletedsource = [
1528 1521 'debugobsolete',
1529 1522 'pull',
1530 1523 'push',
1531 1524 'serve',
1532 1525 'unbundle',
1533 1526 ]
1534 1527
1535 1528 _reportnewcssource = [
1536 1529 'pull',
1537 1530 'unbundle',
1538 1531 ]
1539 1532
1540 1533 def prefetchfiles(repo, revs, match):
1541 1534 """Invokes the registered file prefetch functions, allowing extensions to
1542 1535 ensure the corresponding files are available locally, before the command
1543 1536 uses them."""
1544 1537 if match:
1545 1538 # The command itself will complain about files that don't exist, so
1546 1539 # don't duplicate the message.
1547 1540 match = matchmod.badmatch(match, lambda fn, msg: None)
1548 1541 else:
1549 1542 match = matchall(repo)
1550 1543
1551 1544 fileprefetchhooks(repo, revs, match)
1552 1545
1553 1546 # a list of (repo, revs, match) prefetch functions
1554 1547 fileprefetchhooks = util.hooks()
1555 1548
1556 1549 # A marker that tells the evolve extension to suppress its own reporting
1557 1550 _reportstroubledchangesets = True
1558 1551
1559 1552 def registersummarycallback(repo, otr, txnname=''):
1560 1553 """register a callback to issue a summary after the transaction is closed
1561 1554 """
1562 1555 def txmatch(sources):
1563 1556 return any(txnname.startswith(source) for source in sources)
1564 1557
1565 1558 categories = []
1566 1559
1567 1560 def reportsummary(func):
1568 1561 """decorator for report callbacks."""
1569 1562 # The repoview life cycle is shorter than the one of the actual
1570 1563 # underlying repository. So the filtered object can die before the
1571 1564 # weakref is used leading to troubles. We keep a reference to the
1572 1565 # unfiltered object and restore the filtering when retrieving the
1573 1566 # repository through the weakref.
1574 1567 filtername = repo.filtername
1575 1568 reporef = weakref.ref(repo.unfiltered())
1576 1569 def wrapped(tr):
1577 1570 repo = reporef()
1578 1571 if filtername:
1579 1572 repo = repo.filtered(filtername)
1580 1573 func(repo, tr)
1581 1574 newcat = '%02i-txnreport' % len(categories)
1582 1575 otr.addpostclose(newcat, wrapped)
1583 1576 categories.append(newcat)
1584 1577 return wrapped
1585 1578
1586 1579 if txmatch(_reportobsoletedsource):
1587 1580 @reportsummary
1588 1581 def reportobsoleted(repo, tr):
1589 1582 obsoleted = obsutil.getobsoleted(repo, tr)
1590 1583 if obsoleted:
1591 1584 repo.ui.status(_('obsoleted %i changesets\n')
1592 1585 % len(obsoleted))
1593 1586
1594 1587 if (obsolete.isenabled(repo, obsolete.createmarkersopt) and
1595 1588 repo.ui.configbool('experimental', 'evolution.report-instabilities')):
1596 1589 instabilitytypes = [
1597 1590 ('orphan', 'orphan'),
1598 1591 ('phase-divergent', 'phasedivergent'),
1599 1592 ('content-divergent', 'contentdivergent'),
1600 1593 ]
1601 1594
1602 1595 def getinstabilitycounts(repo):
1603 1596 filtered = repo.changelog.filteredrevs
1604 1597 counts = {}
1605 1598 for instability, revset in instabilitytypes:
1606 1599 counts[instability] = len(set(obsolete.getrevs(repo, revset)) -
1607 1600 filtered)
1608 1601 return counts
1609 1602
1610 1603 oldinstabilitycounts = getinstabilitycounts(repo)
1611 1604 @reportsummary
1612 1605 def reportnewinstabilities(repo, tr):
1613 1606 newinstabilitycounts = getinstabilitycounts(repo)
1614 1607 for instability, revset in instabilitytypes:
1615 1608 delta = (newinstabilitycounts[instability] -
1616 1609 oldinstabilitycounts[instability])
1617 1610 msg = getinstabilitymessage(delta, instability)
1618 1611 if msg:
1619 1612 repo.ui.warn(msg)
1620 1613
1621 1614 if txmatch(_reportnewcssource):
1622 1615 @reportsummary
1623 1616 def reportnewcs(repo, tr):
1624 1617 """Report the range of new revisions pulled/unbundled."""
1625 1618 origrepolen = tr.changes.get('origrepolen', len(repo))
1626 1619 unfi = repo.unfiltered()
1627 1620 if origrepolen >= len(unfi):
1628 1621 return
1629 1622
1630 1623 # Compute the bounds of new visible revisions' range.
1631 1624 revs = smartset.spanset(repo, start=origrepolen)
1632 1625 if revs:
1633 1626 minrev, maxrev = repo[revs.min()], repo[revs.max()]
1634 1627
1635 1628 if minrev == maxrev:
1636 1629 revrange = minrev
1637 1630 else:
1638 1631 revrange = '%s:%s' % (minrev, maxrev)
1639 1632 draft = len(repo.revs('%ld and draft()', revs))
1640 1633 secret = len(repo.revs('%ld and secret()', revs))
1641 1634 if not (draft or secret):
1642 1635 msg = _('new changesets %s\n') % revrange
1643 1636 elif draft and secret:
1644 1637 msg = _('new changesets %s (%d drafts, %d secrets)\n')
1645 1638 msg %= (revrange, draft, secret)
1646 1639 elif draft:
1647 1640 msg = _('new changesets %s (%d drafts)\n')
1648 1641 msg %= (revrange, draft)
1649 1642 elif secret:
1650 1643 msg = _('new changesets %s (%d secrets)\n')
1651 1644 msg %= (revrange, secret)
1652 1645 else:
1653 1646 errormsg = 'entered unreachable condition'
1654 1647 raise error.ProgrammingError(errormsg)
1655 1648 repo.ui.status(msg)
1656 1649
1657 1650 # search new changesets directly pulled as obsolete
1658 1651 duplicates = tr.changes.get('revduplicates', ())
1659 1652 obsadded = unfi.revs('(%d: + %ld) and obsolete()',
1660 1653 origrepolen, duplicates)
1661 1654 cl = repo.changelog
1662 1655 extinctadded = [r for r in obsadded if r not in cl]
1663 1656 if extinctadded:
1664 1657 # They are not just obsolete, but obsolete and invisible
1665 1658 # we call them "extinct" internally but the terms have not been
1666 1659 # exposed to users.
1667 1660 msg = '(%d other changesets obsolete on arrival)\n'
1668 1661 repo.ui.status(msg % len(extinctadded))
1669 1662
1670 1663 @reportsummary
1671 1664 def reportphasechanges(repo, tr):
1672 1665 """Report statistics of phase changes for changesets pre-existing
1673 1666 pull/unbundle.
1674 1667 """
1675 1668 origrepolen = tr.changes.get('origrepolen', len(repo))
1676 1669 phasetracking = tr.changes.get('phases', {})
1677 1670 if not phasetracking:
1678 1671 return
1679 1672 published = [
1680 1673 rev for rev, (old, new) in phasetracking.iteritems()
1681 1674 if new == phases.public and rev < origrepolen
1682 1675 ]
1683 1676 if not published:
1684 1677 return
1685 1678 repo.ui.status(_('%d local changesets published\n')
1686 1679 % len(published))
1687 1680
1688 1681 def getinstabilitymessage(delta, instability):
1689 1682 """function to return the message to show warning about new instabilities
1690 1683
1691 1684 exists as a separate function so that extension can wrap to show more
1692 1685 information like how to fix instabilities"""
1693 1686 if delta > 0:
1694 1687 return _('%i new %s changesets\n') % (delta, instability)
1695 1688
1696 1689 def nodesummaries(repo, nodes, maxnumnodes=4):
1697 1690 if len(nodes) <= maxnumnodes or repo.ui.verbose:
1698 1691 return ' '.join(short(h) for h in nodes)
1699 1692 first = ' '.join(short(h) for h in nodes[:maxnumnodes])
1700 1693 return _("%s and %d others") % (first, len(nodes) - maxnumnodes)
1701 1694
1702 1695 def enforcesinglehead(repo, tr, desc):
1703 1696 """check that no named branch has multiple heads"""
1704 1697 if desc in ('strip', 'repair'):
1705 1698 # skip the logic during strip
1706 1699 return
1707 1700 visible = repo.filtered('visible')
1708 1701 # possible improvement: we could restrict the check to affected branch
1709 1702 for name, heads in visible.branchmap().iteritems():
1710 1703 if len(heads) > 1:
1711 1704 msg = _('rejecting multiple heads on branch "%s"')
1712 1705 msg %= name
1713 1706 hint = _('%d heads: %s')
1714 1707 hint %= (len(heads), nodesummaries(repo, heads))
1715 1708 raise error.Abort(msg, hint=hint)
1716 1709
1717 1710 def wrapconvertsink(sink):
1718 1711 """Allow extensions to wrap the sink returned by convcmd.convertsink()
1719 1712 before it is used, whether or not the convert extension was formally loaded.
1720 1713 """
1721 1714 return sink
1722 1715
1723 1716 def unhidehashlikerevs(repo, specs, hiddentype):
1724 1717 """parse the user specs and unhide changesets whose hash or revision number
1725 1718 is passed.
1726 1719
1727 1720 hiddentype can be: 1) 'warn': warn while unhiding changesets
1728 1721 2) 'nowarn': don't warn while unhiding changesets
1729 1722
1730 1723 returns a repo object with the required changesets unhidden
1731 1724 """
1732 1725 if not repo.filtername or not repo.ui.configbool('experimental',
1733 1726 'directaccess'):
1734 1727 return repo
1735 1728
1736 1729 if repo.filtername not in ('visible', 'visible-hidden'):
1737 1730 return repo
1738 1731
1739 1732 symbols = set()
1740 1733 for spec in specs:
1741 1734 try:
1742 1735 tree = revsetlang.parse(spec)
1743 1736 except error.ParseError: # will be reported by scmutil.revrange()
1744 1737 continue
1745 1738
1746 1739 symbols.update(revsetlang.gethashlikesymbols(tree))
1747 1740
1748 1741 if not symbols:
1749 1742 return repo
1750 1743
1751 1744 revs = _getrevsfromsymbols(repo, symbols)
1752 1745
1753 1746 if not revs:
1754 1747 return repo
1755 1748
1756 1749 if hiddentype == 'warn':
1757 1750 unfi = repo.unfiltered()
1758 1751 revstr = ", ".join([pycompat.bytestr(unfi[l]) for l in revs])
1759 1752 repo.ui.warn(_("warning: accessing hidden changesets for write "
1760 1753 "operation: %s\n") % revstr)
1761 1754
1762 1755 # we have to use new filtername to separate branch/tags cache until we can
1763 1756 # disbale these cache when revisions are dynamically pinned.
1764 1757 return repo.filtered('visible-hidden', revs)
1765 1758
1766 1759 def _getrevsfromsymbols(repo, symbols):
1767 1760 """parse the list of symbols and returns a set of revision numbers of hidden
1768 1761 changesets present in symbols"""
1769 1762 revs = set()
1770 1763 unfi = repo.unfiltered()
1771 1764 unficl = unfi.changelog
1772 1765 cl = repo.changelog
1773 1766 tiprev = len(unficl)
1774 1767 allowrevnums = repo.ui.configbool('experimental', 'directaccess.revnums')
1775 1768 for s in symbols:
1776 1769 try:
1777 1770 n = int(s)
1778 1771 if n <= tiprev:
1779 1772 if not allowrevnums:
1780 1773 continue
1781 1774 else:
1782 1775 if n not in cl:
1783 1776 revs.add(n)
1784 1777 continue
1785 1778 except ValueError:
1786 1779 pass
1787 1780
1788 1781 try:
1789 1782 s = resolvehexnodeidprefix(unfi, s)
1790 1783 except (error.LookupError, error.WdirUnsupported):
1791 1784 s = None
1792 1785
1793 1786 if s is not None:
1794 1787 rev = unficl.rev(s)
1795 1788 if rev not in cl:
1796 1789 revs.add(rev)
1797 1790
1798 1791 return revs
1799 1792
1800 1793 def bookmarkrevs(repo, mark):
1801 1794 """
1802 1795 Select revisions reachable by a given bookmark
1803 1796 """
1804 1797 return repo.revs("ancestors(bookmark(%s)) - "
1805 1798 "ancestors(head() and not bookmark(%s)) - "
1806 1799 "ancestors(bookmark() and not bookmark(%s))",
1807 1800 mark, mark, mark)
@@ -1,269 +1,269 b''
1 1 from __future__ import absolute_import, print_function
2 2 import os
3 3 import stat
4 4 import subprocess
5 5 import sys
6 6
7 7 if subprocess.call(['python', '%s/hghave' % os.environ['TESTDIR'],
8 8 'cacheable']):
9 9 sys.exit(80)
10 10
11 11 print_ = print
12 12 def print(*args, **kwargs):
13 13 """print() wrapper that flushes stdout buffers to avoid py3 buffer issues
14 14
15 15 We could also just write directly to sys.stdout.buffer the way the
16 16 ui object will, but this was easier for porting the test.
17 17 """
18 18 print_(*args, **kwargs)
19 19 sys.stdout.flush()
20 20
21 21 from mercurial import (
22 22 extensions,
23 23 hg,
24 24 localrepo,
25 25 pycompat,
26 26 ui as uimod,
27 27 util,
28 28 vfs as vfsmod,
29 29 )
30 30
31 31 if pycompat.ispy3:
32 32 xrange = range
33 33
34 34 class fakerepo(object):
35 35 def __init__(self):
36 36 self._filecache = {}
37 37
38 38 class fakevfs(object):
39 39
40 40 def join(self, p):
41 41 return p
42 42
43 43 vfs = fakevfs()
44 44
45 45 def unfiltered(self):
46 46 return self
47 47
48 48 def sjoin(self, p):
49 49 return p
50 50
51 51 @localrepo.repofilecache('x', 'y')
52 52 def cached(self):
53 53 print('creating')
54 54 return 'string from function'
55 55
56 56 def invalidate(self):
57 57 for k in self._filecache:
58 58 try:
59 59 delattr(self, pycompat.sysstr(k))
60 60 except AttributeError:
61 61 pass
62 62
63 63 def basic(repo):
64 64 print("* neither file exists")
65 65 # calls function
66 66 repo.cached
67 67
68 68 repo.invalidate()
69 69 print("* neither file still exists")
70 70 # uses cache
71 71 repo.cached
72 72
73 73 # create empty file
74 74 f = open('x', 'w')
75 75 f.close()
76 76 repo.invalidate()
77 77 print("* empty file x created")
78 78 # should recreate the object
79 79 repo.cached
80 80
81 81 f = open('x', 'w')
82 82 f.write('a')
83 83 f.close()
84 84 repo.invalidate()
85 85 print("* file x changed size")
86 86 # should recreate the object
87 87 repo.cached
88 88
89 89 repo.invalidate()
90 90 print("* nothing changed with either file")
91 91 # stats file again, reuses object
92 92 repo.cached
93 93
94 94 # atomic replace file, size doesn't change
95 95 # hopefully st_mtime doesn't change as well so this doesn't use the cache
96 96 # because of inode change
97 97 f = vfsmod.vfs(b'.')(b'x', b'w', atomictemp=True)
98 98 f.write(b'b')
99 99 f.close()
100 100
101 101 repo.invalidate()
102 102 print("* file x changed inode")
103 103 repo.cached
104 104
105 105 # create empty file y
106 106 f = open('y', 'w')
107 107 f.close()
108 108 repo.invalidate()
109 109 print("* empty file y created")
110 110 # should recreate the object
111 111 repo.cached
112 112
113 113 f = open('y', 'w')
114 114 f.write('A')
115 115 f.close()
116 116 repo.invalidate()
117 117 print("* file y changed size")
118 118 # should recreate the object
119 119 repo.cached
120 120
121 121 f = vfsmod.vfs(b'.')(b'y', b'w', atomictemp=True)
122 122 f.write(b'B')
123 123 f.close()
124 124
125 125 repo.invalidate()
126 126 print("* file y changed inode")
127 127 repo.cached
128 128
129 129 f = vfsmod.vfs(b'.')(b'x', b'w', atomictemp=True)
130 130 f.write(b'c')
131 131 f.close()
132 132 f = vfsmod.vfs(b'.')(b'y', b'w', atomictemp=True)
133 133 f.write(b'C')
134 134 f.close()
135 135
136 136 repo.invalidate()
137 137 print("* both files changed inode")
138 138 repo.cached
139 139
140 140 def fakeuncacheable():
141 141 def wrapcacheable(orig, *args, **kwargs):
142 142 return False
143 143
144 144 def wrapinit(orig, *args, **kwargs):
145 145 pass
146 146
147 147 originit = extensions.wrapfunction(util.cachestat, '__init__', wrapinit)
148 148 origcacheable = extensions.wrapfunction(util.cachestat, 'cacheable',
149 149 wrapcacheable)
150 150
151 151 for fn in ['x', 'y']:
152 152 try:
153 153 os.remove(fn)
154 154 except OSError:
155 155 pass
156 156
157 157 basic(fakerepo())
158 158
159 159 util.cachestat.cacheable = origcacheable
160 160 util.cachestat.__init__ = originit
161 161
162 162 def test_filecache_synced():
163 163 # test old behavior that caused filecached properties to go out of sync
164 164 os.system('hg init && echo a >> a && hg ci -qAm.')
165 165 repo = hg.repository(uimod.ui.load())
166 166 # first rollback clears the filecache, but changelog to stays in __dict__
167 167 repo.rollback()
168 168 repo.commit(b'.')
169 169 # second rollback comes along and touches the changelog externally
170 170 # (file is moved)
171 171 repo.rollback()
172 172 # but since changelog isn't under the filecache control anymore, we don't
173 173 # see that it changed, and return the old changelog without reconstructing
174 174 # it
175 175 repo.commit(b'.')
176 176
177 177 def setbeforeget(repo):
178 178 os.remove('x')
179 179 os.remove('y')
180 repo.cached = 'string set externally'
180 repo.__class__.cached.set(repo, 'string set externally')
181 181 repo.invalidate()
182 182 print("* neither file exists")
183 183 print(repo.cached)
184 184 repo.invalidate()
185 185 f = open('x', 'w')
186 186 f.write('a')
187 187 f.close()
188 188 print("* file x created")
189 189 print(repo.cached)
190 190
191 repo.cached = 'string 2 set externally'
191 repo.__class__.cached.set(repo, 'string 2 set externally')
192 192 repo.invalidate()
193 193 print("* string set externally again")
194 194 print(repo.cached)
195 195
196 196 repo.invalidate()
197 197 f = open('y', 'w')
198 198 f.write('b')
199 199 f.close()
200 200 print("* file y created")
201 201 print(repo.cached)
202 202
203 203 def antiambiguity():
204 204 filename = 'ambigcheck'
205 205
206 206 # try some times, because reproduction of ambiguity depends on
207 207 # "filesystem time"
208 208 for i in xrange(5):
209 209 fp = open(filename, 'w')
210 210 fp.write('FOO')
211 211 fp.close()
212 212
213 213 oldstat = os.stat(filename)
214 214 if oldstat[stat.ST_CTIME] != oldstat[stat.ST_MTIME]:
215 215 # subsequent changing never causes ambiguity
216 216 continue
217 217
218 218 repetition = 3
219 219
220 220 # repeat changing via checkambigatclosing, to examine whether
221 221 # st_mtime is advanced multiple times as expected
222 222 for i in xrange(repetition):
223 223 # explicit closing
224 224 fp = vfsmod.checkambigatclosing(open(filename, 'a'))
225 225 fp.write('FOO')
226 226 fp.close()
227 227
228 228 # implicit closing by "with" statement
229 229 with vfsmod.checkambigatclosing(open(filename, 'a')) as fp:
230 230 fp.write('BAR')
231 231
232 232 newstat = os.stat(filename)
233 233 if oldstat[stat.ST_CTIME] != newstat[stat.ST_CTIME]:
234 234 # timestamp ambiguity was naturally avoided while repetition
235 235 continue
236 236
237 237 # st_mtime should be advanced "repetition * 2" times, because
238 238 # all changes occurred at same time (in sec)
239 239 expected = (oldstat[stat.ST_MTIME] + repetition * 2) & 0x7fffffff
240 240 if newstat[stat.ST_MTIME] != expected:
241 241 print("'newstat[stat.ST_MTIME] %s is not %s (as %s + %s * 2)" %
242 242 (newstat[stat.ST_MTIME], expected,
243 243 oldstat[stat.ST_MTIME], repetition))
244 244
245 245 # no more examination is needed regardless of result
246 246 break
247 247 else:
248 248 # This platform seems too slow to examine anti-ambiguity
249 249 # of file timestamp (or test happened to be executed at
250 250 # bad timing). Exit silently in this case, because running
251 251 # on other faster platforms can detect problems
252 252 pass
253 253
254 254 print('basic:')
255 255 print()
256 256 basic(fakerepo())
257 257 print()
258 258 print('fakeuncacheable:')
259 259 print()
260 260 fakeuncacheable()
261 261 test_filecache_synced()
262 262 print()
263 263 print('setbeforeget:')
264 264 print()
265 265 setbeforeget(fakerepo())
266 266 print()
267 267 print('antiambiguity:')
268 268 print()
269 269 antiambiguity()
General Comments 0
You need to be logged in to leave comments. Login now