##// END OF EJS Templates
localrepo: push manifestlog and changelog construction code into store...
Augie Fackler -
r43175:3df3b139 default
parent child Browse files
Show More
@@ -1,3316 +1,3311 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 changelog,
32 31 color,
33 32 context,
34 33 dirstate,
35 34 dirstateguard,
36 35 discovery,
37 36 encoding,
38 37 error,
39 38 exchange,
40 39 extensions,
41 40 filelog,
42 41 hook,
43 42 lock as lockmod,
44 manifest,
45 43 match as matchmod,
46 44 merge as mergemod,
47 45 mergeutil,
48 46 namespaces,
49 47 narrowspec,
50 48 obsolete,
51 49 pathutil,
52 50 phases,
53 51 pushkey,
54 52 pycompat,
55 53 repoview,
56 54 revset,
57 55 revsetlang,
58 56 scmutil,
59 57 sparse,
60 58 store as storemod,
61 59 subrepoutil,
62 60 tags as tagsmod,
63 61 transaction,
64 62 txnutil,
65 63 util,
66 64 vfs as vfsmod,
67 65 )
68 66
69 67 from .interfaces import (
70 68 repository,
71 69 util as interfaceutil,
72 70 )
73 71
74 72 from .utils import (
75 73 procutil,
76 74 stringutil,
77 75 )
78 76
79 77 from .revlogutils import (
80 78 constants as revlogconst,
81 79 )
82 80
83 81 release = lockmod.release
84 82 urlerr = util.urlerr
85 83 urlreq = util.urlreq
86 84
87 85 # set of (path, vfs-location) tuples. vfs-location is:
88 86 # - 'plain for vfs relative paths
89 87 # - '' for svfs relative paths
90 88 _cachedfiles = set()
91 89
92 90 class _basefilecache(scmutil.filecache):
93 91 """All filecache usage on repo are done for logic that should be unfiltered
94 92 """
95 93 def __get__(self, repo, type=None):
96 94 if repo is None:
97 95 return self
98 96 # proxy to unfiltered __dict__ since filtered repo has no entry
99 97 unfi = repo.unfiltered()
100 98 try:
101 99 return unfi.__dict__[self.sname]
102 100 except KeyError:
103 101 pass
104 102 return super(_basefilecache, self).__get__(unfi, type)
105 103
106 104 def set(self, repo, value):
107 105 return super(_basefilecache, self).set(repo.unfiltered(), value)
108 106
109 107 class repofilecache(_basefilecache):
110 108 """filecache for files in .hg but outside of .hg/store"""
111 109 def __init__(self, *paths):
112 110 super(repofilecache, self).__init__(*paths)
113 111 for path in paths:
114 112 _cachedfiles.add((path, 'plain'))
115 113
116 114 def join(self, obj, fname):
117 115 return obj.vfs.join(fname)
118 116
119 117 class storecache(_basefilecache):
120 118 """filecache for files in the store"""
121 119 def __init__(self, *paths):
122 120 super(storecache, self).__init__(*paths)
123 121 for path in paths:
124 122 _cachedfiles.add((path, ''))
125 123
126 124 def join(self, obj, fname):
127 125 return obj.sjoin(fname)
128 126
129 127 class mixedrepostorecache(_basefilecache):
130 128 """filecache for a mix files in .hg/store and outside"""
131 129 def __init__(self, *pathsandlocations):
132 130 # scmutil.filecache only uses the path for passing back into our
133 131 # join(), so we can safely pass a list of paths and locations
134 132 super(mixedrepostorecache, self).__init__(*pathsandlocations)
135 133 _cachedfiles.update(pathsandlocations)
136 134
137 135 def join(self, obj, fnameandlocation):
138 136 fname, location = fnameandlocation
139 137 if location == 'plain':
140 138 return obj.vfs.join(fname)
141 139 else:
142 140 if location != '':
143 141 raise error.ProgrammingError('unexpected location: %s' %
144 142 location)
145 143 return obj.sjoin(fname)
146 144
147 145 def isfilecached(repo, name):
148 146 """check if a repo has already cached "name" filecache-ed property
149 147
150 148 This returns (cachedobj-or-None, iscached) tuple.
151 149 """
152 150 cacheentry = repo.unfiltered()._filecache.get(name, None)
153 151 if not cacheentry:
154 152 return None, False
155 153 return cacheentry.obj, True
156 154
157 155 class unfilteredpropertycache(util.propertycache):
158 156 """propertycache that apply to unfiltered repo only"""
159 157
160 158 def __get__(self, repo, type=None):
161 159 unfi = repo.unfiltered()
162 160 if unfi is repo:
163 161 return super(unfilteredpropertycache, self).__get__(unfi)
164 162 return getattr(unfi, self.name)
165 163
166 164 class filteredpropertycache(util.propertycache):
167 165 """propertycache that must take filtering in account"""
168 166
169 167 def cachevalue(self, obj, value):
170 168 object.__setattr__(obj, self.name, value)
171 169
172 170
173 171 def hasunfilteredcache(repo, name):
174 172 """check if a repo has an unfilteredpropertycache value for <name>"""
175 173 return name in vars(repo.unfiltered())
176 174
177 175 def unfilteredmethod(orig):
178 176 """decorate method that always need to be run on unfiltered version"""
179 177 def wrapper(repo, *args, **kwargs):
180 178 return orig(repo.unfiltered(), *args, **kwargs)
181 179 return wrapper
182 180
183 181 moderncaps = {'lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
184 182 'unbundle'}
185 183 legacycaps = moderncaps.union({'changegroupsubset'})
186 184
187 185 @interfaceutil.implementer(repository.ipeercommandexecutor)
188 186 class localcommandexecutor(object):
189 187 def __init__(self, peer):
190 188 self._peer = peer
191 189 self._sent = False
192 190 self._closed = False
193 191
194 192 def __enter__(self):
195 193 return self
196 194
197 195 def __exit__(self, exctype, excvalue, exctb):
198 196 self.close()
199 197
200 198 def callcommand(self, command, args):
201 199 if self._sent:
202 200 raise error.ProgrammingError('callcommand() cannot be used after '
203 201 'sendcommands()')
204 202
205 203 if self._closed:
206 204 raise error.ProgrammingError('callcommand() cannot be used after '
207 205 'close()')
208 206
209 207 # We don't need to support anything fancy. Just call the named
210 208 # method on the peer and return a resolved future.
211 209 fn = getattr(self._peer, pycompat.sysstr(command))
212 210
213 211 f = pycompat.futures.Future()
214 212
215 213 try:
216 214 result = fn(**pycompat.strkwargs(args))
217 215 except Exception:
218 216 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
219 217 else:
220 218 f.set_result(result)
221 219
222 220 return f
223 221
224 222 def sendcommands(self):
225 223 self._sent = True
226 224
227 225 def close(self):
228 226 self._closed = True
229 227
230 228 @interfaceutil.implementer(repository.ipeercommands)
231 229 class localpeer(repository.peer):
232 230 '''peer for a local repo; reflects only the most recent API'''
233 231
234 232 def __init__(self, repo, caps=None):
235 233 super(localpeer, self).__init__()
236 234
237 235 if caps is None:
238 236 caps = moderncaps.copy()
239 237 self._repo = repo.filtered('served')
240 238 self.ui = repo.ui
241 239 self._caps = repo._restrictcapabilities(caps)
242 240
243 241 # Begin of _basepeer interface.
244 242
245 243 def url(self):
246 244 return self._repo.url()
247 245
248 246 def local(self):
249 247 return self._repo
250 248
251 249 def peer(self):
252 250 return self
253 251
254 252 def canpush(self):
255 253 return True
256 254
257 255 def close(self):
258 256 self._repo.close()
259 257
260 258 # End of _basepeer interface.
261 259
262 260 # Begin of _basewirecommands interface.
263 261
264 262 def branchmap(self):
265 263 return self._repo.branchmap()
266 264
267 265 def capabilities(self):
268 266 return self._caps
269 267
270 268 def clonebundles(self):
271 269 return self._repo.tryread('clonebundles.manifest')
272 270
273 271 def debugwireargs(self, one, two, three=None, four=None, five=None):
274 272 """Used to test argument passing over the wire"""
275 273 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
276 274 pycompat.bytestr(four),
277 275 pycompat.bytestr(five))
278 276
279 277 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
280 278 **kwargs):
281 279 chunks = exchange.getbundlechunks(self._repo, source, heads=heads,
282 280 common=common, bundlecaps=bundlecaps,
283 281 **kwargs)[1]
284 282 cb = util.chunkbuffer(chunks)
285 283
286 284 if exchange.bundle2requested(bundlecaps):
287 285 # When requesting a bundle2, getbundle returns a stream to make the
288 286 # wire level function happier. We need to build a proper object
289 287 # from it in local peer.
290 288 return bundle2.getunbundler(self.ui, cb)
291 289 else:
292 290 return changegroup.getunbundler('01', cb, None)
293 291
294 292 def heads(self):
295 293 return self._repo.heads()
296 294
297 295 def known(self, nodes):
298 296 return self._repo.known(nodes)
299 297
300 298 def listkeys(self, namespace):
301 299 return self._repo.listkeys(namespace)
302 300
303 301 def lookup(self, key):
304 302 return self._repo.lookup(key)
305 303
306 304 def pushkey(self, namespace, key, old, new):
307 305 return self._repo.pushkey(namespace, key, old, new)
308 306
309 307 def stream_out(self):
310 308 raise error.Abort(_('cannot perform stream clone against local '
311 309 'peer'))
312 310
313 311 def unbundle(self, bundle, heads, url):
314 312 """apply a bundle on a repo
315 313
316 314 This function handles the repo locking itself."""
317 315 try:
318 316 try:
319 317 bundle = exchange.readbundle(self.ui, bundle, None)
320 318 ret = exchange.unbundle(self._repo, bundle, heads, 'push', url)
321 319 if util.safehasattr(ret, 'getchunks'):
322 320 # This is a bundle20 object, turn it into an unbundler.
323 321 # This little dance should be dropped eventually when the
324 322 # API is finally improved.
325 323 stream = util.chunkbuffer(ret.getchunks())
326 324 ret = bundle2.getunbundler(self.ui, stream)
327 325 return ret
328 326 except Exception as exc:
329 327 # If the exception contains output salvaged from a bundle2
330 328 # reply, we need to make sure it is printed before continuing
331 329 # to fail. So we build a bundle2 with such output and consume
332 330 # it directly.
333 331 #
334 332 # This is not very elegant but allows a "simple" solution for
335 333 # issue4594
336 334 output = getattr(exc, '_bundle2salvagedoutput', ())
337 335 if output:
338 336 bundler = bundle2.bundle20(self._repo.ui)
339 337 for out in output:
340 338 bundler.addpart(out)
341 339 stream = util.chunkbuffer(bundler.getchunks())
342 340 b = bundle2.getunbundler(self.ui, stream)
343 341 bundle2.processbundle(self._repo, b)
344 342 raise
345 343 except error.PushRaced as exc:
346 344 raise error.ResponseError(_('push failed:'),
347 345 stringutil.forcebytestr(exc))
348 346
349 347 # End of _basewirecommands interface.
350 348
351 349 # Begin of peer interface.
352 350
353 351 def commandexecutor(self):
354 352 return localcommandexecutor(self)
355 353
356 354 # End of peer interface.
357 355
358 356 @interfaceutil.implementer(repository.ipeerlegacycommands)
359 357 class locallegacypeer(localpeer):
360 358 '''peer extension which implements legacy methods too; used for tests with
361 359 restricted capabilities'''
362 360
363 361 def __init__(self, repo):
364 362 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
365 363
366 364 # Begin of baselegacywirecommands interface.
367 365
368 366 def between(self, pairs):
369 367 return self._repo.between(pairs)
370 368
371 369 def branches(self, nodes):
372 370 return self._repo.branches(nodes)
373 371
374 372 def changegroup(self, nodes, source):
375 373 outgoing = discovery.outgoing(self._repo, missingroots=nodes,
376 374 missingheads=self._repo.heads())
377 375 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
378 376
379 377 def changegroupsubset(self, bases, heads, source):
380 378 outgoing = discovery.outgoing(self._repo, missingroots=bases,
381 379 missingheads=heads)
382 380 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
383 381
384 382 # End of baselegacywirecommands interface.
385 383
386 384 # Increment the sub-version when the revlog v2 format changes to lock out old
387 385 # clients.
388 386 REVLOGV2_REQUIREMENT = 'exp-revlogv2.1'
389 387
390 388 # A repository with the sparserevlog feature will have delta chains that
391 389 # can spread over a larger span. Sparse reading cuts these large spans into
392 390 # pieces, so that each piece isn't too big.
393 391 # Without the sparserevlog capability, reading from the repository could use
394 392 # huge amounts of memory, because the whole span would be read at once,
395 393 # including all the intermediate revisions that aren't pertinent for the chain.
396 394 # This is why once a repository has enabled sparse-read, it becomes required.
397 395 SPARSEREVLOG_REQUIREMENT = 'sparserevlog'
398 396
399 397 # Functions receiving (ui, features) that extensions can register to impact
400 398 # the ability to load repositories with custom requirements. Only
401 399 # functions defined in loaded extensions are called.
402 400 #
403 401 # The function receives a set of requirement strings that the repository
404 402 # is capable of opening. Functions will typically add elements to the
405 403 # set to reflect that the extension knows how to handle that requirements.
406 404 featuresetupfuncs = set()
407 405
408 406 def makelocalrepository(baseui, path, intents=None):
409 407 """Create a local repository object.
410 408
411 409 Given arguments needed to construct a local repository, this function
412 410 performs various early repository loading functionality (such as
413 411 reading the ``.hg/requires`` and ``.hg/hgrc`` files), validates that
414 412 the repository can be opened, derives a type suitable for representing
415 413 that repository, and returns an instance of it.
416 414
417 415 The returned object conforms to the ``repository.completelocalrepository``
418 416 interface.
419 417
420 418 The repository type is derived by calling a series of factory functions
421 419 for each aspect/interface of the final repository. These are defined by
422 420 ``REPO_INTERFACES``.
423 421
424 422 Each factory function is called to produce a type implementing a specific
425 423 interface. The cumulative list of returned types will be combined into a
426 424 new type and that type will be instantiated to represent the local
427 425 repository.
428 426
429 427 The factory functions each receive various state that may be consulted
430 428 as part of deriving a type.
431 429
432 430 Extensions should wrap these factory functions to customize repository type
433 431 creation. Note that an extension's wrapped function may be called even if
434 432 that extension is not loaded for the repo being constructed. Extensions
435 433 should check if their ``__name__`` appears in the
436 434 ``extensionmodulenames`` set passed to the factory function and no-op if
437 435 not.
438 436 """
439 437 ui = baseui.copy()
440 438 # Prevent copying repo configuration.
441 439 ui.copy = baseui.copy
442 440
443 441 # Working directory VFS rooted at repository root.
444 442 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
445 443
446 444 # Main VFS for .hg/ directory.
447 445 hgpath = wdirvfs.join(b'.hg')
448 446 hgvfs = vfsmod.vfs(hgpath, cacheaudited=True)
449 447
450 448 # The .hg/ path should exist and should be a directory. All other
451 449 # cases are errors.
452 450 if not hgvfs.isdir():
453 451 try:
454 452 hgvfs.stat()
455 453 except OSError as e:
456 454 if e.errno != errno.ENOENT:
457 455 raise
458 456
459 457 raise error.RepoError(_(b'repository %s not found') % path)
460 458
461 459 # .hg/requires file contains a newline-delimited list of
462 460 # features/capabilities the opener (us) must have in order to use
463 461 # the repository. This file was introduced in Mercurial 0.9.2,
464 462 # which means very old repositories may not have one. We assume
465 463 # a missing file translates to no requirements.
466 464 try:
467 465 requirements = set(hgvfs.read(b'requires').splitlines())
468 466 except IOError as e:
469 467 if e.errno != errno.ENOENT:
470 468 raise
471 469 requirements = set()
472 470
473 471 # The .hg/hgrc file may load extensions or contain config options
474 472 # that influence repository construction. Attempt to load it and
475 473 # process any new extensions that it may have pulled in.
476 474 if loadhgrc(ui, wdirvfs, hgvfs, requirements):
477 475 afterhgrcload(ui, wdirvfs, hgvfs, requirements)
478 476 extensions.loadall(ui)
479 477 extensions.populateui(ui)
480 478
481 479 # Set of module names of extensions loaded for this repository.
482 480 extensionmodulenames = {m.__name__ for n, m in extensions.extensions(ui)}
483 481
484 482 supportedrequirements = gathersupportedrequirements(ui)
485 483
486 484 # We first validate the requirements are known.
487 485 ensurerequirementsrecognized(requirements, supportedrequirements)
488 486
489 487 # Then we validate that the known set is reasonable to use together.
490 488 ensurerequirementscompatible(ui, requirements)
491 489
492 490 # TODO there are unhandled edge cases related to opening repositories with
493 491 # shared storage. If storage is shared, we should also test for requirements
494 492 # compatibility in the pointed-to repo. This entails loading the .hg/hgrc in
495 493 # that repo, as that repo may load extensions needed to open it. This is a
496 494 # bit complicated because we don't want the other hgrc to overwrite settings
497 495 # in this hgrc.
498 496 #
499 497 # This bug is somewhat mitigated by the fact that we copy the .hg/requires
500 498 # file when sharing repos. But if a requirement is added after the share is
501 499 # performed, thereby introducing a new requirement for the opener, we may
502 500 # will not see that and could encounter a run-time error interacting with
503 501 # that shared store since it has an unknown-to-us requirement.
504 502
505 503 # At this point, we know we should be capable of opening the repository.
506 504 # Now get on with doing that.
507 505
508 506 features = set()
509 507
510 508 # The "store" part of the repository holds versioned data. How it is
511 509 # accessed is determined by various requirements. The ``shared`` or
512 510 # ``relshared`` requirements indicate the store lives in the path contained
513 511 # in the ``.hg/sharedpath`` file. This is an absolute path for
514 512 # ``shared`` and relative to ``.hg/`` for ``relshared``.
515 513 if b'shared' in requirements or b'relshared' in requirements:
516 514 sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n')
517 515 if b'relshared' in requirements:
518 516 sharedpath = hgvfs.join(sharedpath)
519 517
520 518 sharedvfs = vfsmod.vfs(sharedpath, realpath=True)
521 519
522 520 if not sharedvfs.exists():
523 521 raise error.RepoError(_(b'.hg/sharedpath points to nonexistent '
524 522 b'directory %s') % sharedvfs.base)
525 523
526 524 features.add(repository.REPO_FEATURE_SHARED_STORAGE)
527 525
528 526 storebasepath = sharedvfs.base
529 527 cachepath = sharedvfs.join(b'cache')
530 528 else:
531 529 storebasepath = hgvfs.base
532 530 cachepath = hgvfs.join(b'cache')
533 531 wcachepath = hgvfs.join(b'wcache')
534 532
535 533
536 534 # The store has changed over time and the exact layout is dictated by
537 535 # requirements. The store interface abstracts differences across all
538 536 # of them.
539 537 store = makestore(requirements, storebasepath,
540 538 lambda base: vfsmod.vfs(base, cacheaudited=True))
541 539 hgvfs.createmode = store.createmode
542 540
543 541 storevfs = store.vfs
544 542 storevfs.options = resolvestorevfsoptions(ui, requirements, features)
545 543
546 544 # The cache vfs is used to manage cache files.
547 545 cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
548 546 cachevfs.createmode = store.createmode
549 547 # The cache vfs is used to manage cache files related to the working copy
550 548 wcachevfs = vfsmod.vfs(wcachepath, cacheaudited=True)
551 549 wcachevfs.createmode = store.createmode
552 550
553 551 # Now resolve the type for the repository object. We do this by repeatedly
554 552 # calling a factory function to produces types for specific aspects of the
555 553 # repo's operation. The aggregate returned types are used as base classes
556 554 # for a dynamically-derived type, which will represent our new repository.
557 555
558 556 bases = []
559 557 extrastate = {}
560 558
561 559 for iface, fn in REPO_INTERFACES:
562 560 # We pass all potentially useful state to give extensions tons of
563 561 # flexibility.
564 562 typ = fn()(ui=ui,
565 563 intents=intents,
566 564 requirements=requirements,
567 565 features=features,
568 566 wdirvfs=wdirvfs,
569 567 hgvfs=hgvfs,
570 568 store=store,
571 569 storevfs=storevfs,
572 570 storeoptions=storevfs.options,
573 571 cachevfs=cachevfs,
574 572 wcachevfs=wcachevfs,
575 573 extensionmodulenames=extensionmodulenames,
576 574 extrastate=extrastate,
577 575 baseclasses=bases)
578 576
579 577 if not isinstance(typ, type):
580 578 raise error.ProgrammingError('unable to construct type for %s' %
581 579 iface)
582 580
583 581 bases.append(typ)
584 582
585 583 # type() allows you to use characters in type names that wouldn't be
586 584 # recognized as Python symbols in source code. We abuse that to add
587 585 # rich information about our constructed repo.
588 586 name = pycompat.sysstr(b'derivedrepo:%s<%s>' % (
589 587 wdirvfs.base,
590 588 b','.join(sorted(requirements))))
591 589
592 590 cls = type(name, tuple(bases), {})
593 591
594 592 return cls(
595 593 baseui=baseui,
596 594 ui=ui,
597 595 origroot=path,
598 596 wdirvfs=wdirvfs,
599 597 hgvfs=hgvfs,
600 598 requirements=requirements,
601 599 supportedrequirements=supportedrequirements,
602 600 sharedpath=storebasepath,
603 601 store=store,
604 602 cachevfs=cachevfs,
605 603 wcachevfs=wcachevfs,
606 604 features=features,
607 605 intents=intents)
608 606
609 607 def loadhgrc(ui, wdirvfs, hgvfs, requirements):
610 608 """Load hgrc files/content into a ui instance.
611 609
612 610 This is called during repository opening to load any additional
613 611 config files or settings relevant to the current repository.
614 612
615 613 Returns a bool indicating whether any additional configs were loaded.
616 614
617 615 Extensions should monkeypatch this function to modify how per-repo
618 616 configs are loaded. For example, an extension may wish to pull in
619 617 configs from alternate files or sources.
620 618 """
621 619 try:
622 620 ui.readconfig(hgvfs.join(b'hgrc'), root=wdirvfs.base)
623 621 return True
624 622 except IOError:
625 623 return False
626 624
627 625 def afterhgrcload(ui, wdirvfs, hgvfs, requirements):
628 626 """Perform additional actions after .hg/hgrc is loaded.
629 627
630 628 This function is called during repository loading immediately after
631 629 the .hg/hgrc file is loaded and before per-repo extensions are loaded.
632 630
633 631 The function can be used to validate configs, automatically add
634 632 options (including extensions) based on requirements, etc.
635 633 """
636 634
637 635 # Map of requirements to list of extensions to load automatically when
638 636 # requirement is present.
639 637 autoextensions = {
640 638 b'largefiles': [b'largefiles'],
641 639 b'lfs': [b'lfs'],
642 640 }
643 641
644 642 for requirement, names in sorted(autoextensions.items()):
645 643 if requirement not in requirements:
646 644 continue
647 645
648 646 for name in names:
649 647 if not ui.hasconfig(b'extensions', name):
650 648 ui.setconfig(b'extensions', name, b'', source='autoload')
651 649
652 650 def gathersupportedrequirements(ui):
653 651 """Determine the complete set of recognized requirements."""
654 652 # Start with all requirements supported by this file.
655 653 supported = set(localrepository._basesupported)
656 654
657 655 # Execute ``featuresetupfuncs`` entries if they belong to an extension
658 656 # relevant to this ui instance.
659 657 modules = {m.__name__ for n, m in extensions.extensions(ui)}
660 658
661 659 for fn in featuresetupfuncs:
662 660 if fn.__module__ in modules:
663 661 fn(ui, supported)
664 662
665 663 # Add derived requirements from registered compression engines.
666 664 for name in util.compengines:
667 665 engine = util.compengines[name]
668 666 if engine.available() and engine.revlogheader():
669 667 supported.add(b'exp-compression-%s' % name)
670 668 if engine.name() == 'zstd':
671 669 supported.add(b'revlog-compression-zstd')
672 670
673 671 return supported
674 672
675 673 def ensurerequirementsrecognized(requirements, supported):
676 674 """Validate that a set of local requirements is recognized.
677 675
678 676 Receives a set of requirements. Raises an ``error.RepoError`` if there
679 677 exists any requirement in that set that currently loaded code doesn't
680 678 recognize.
681 679
682 680 Returns a set of supported requirements.
683 681 """
684 682 missing = set()
685 683
686 684 for requirement in requirements:
687 685 if requirement in supported:
688 686 continue
689 687
690 688 if not requirement or not requirement[0:1].isalnum():
691 689 raise error.RequirementError(_(b'.hg/requires file is corrupt'))
692 690
693 691 missing.add(requirement)
694 692
695 693 if missing:
696 694 raise error.RequirementError(
697 695 _(b'repository requires features unknown to this Mercurial: %s') %
698 696 b' '.join(sorted(missing)),
699 697 hint=_(b'see https://mercurial-scm.org/wiki/MissingRequirement '
700 698 b'for more information'))
701 699
702 700 def ensurerequirementscompatible(ui, requirements):
703 701 """Validates that a set of recognized requirements is mutually compatible.
704 702
705 703 Some requirements may not be compatible with others or require
706 704 config options that aren't enabled. This function is called during
707 705 repository opening to ensure that the set of requirements needed
708 706 to open a repository is sane and compatible with config options.
709 707
710 708 Extensions can monkeypatch this function to perform additional
711 709 checking.
712 710
713 711 ``error.RepoError`` should be raised on failure.
714 712 """
715 713 if b'exp-sparse' in requirements and not sparse.enabled:
716 714 raise error.RepoError(_(b'repository is using sparse feature but '
717 715 b'sparse is not enabled; enable the '
718 716 b'"sparse" extensions to access'))
719 717
720 718 def makestore(requirements, path, vfstype):
721 719 """Construct a storage object for a repository."""
722 720 if b'store' in requirements:
723 721 if b'fncache' in requirements:
724 722 return storemod.fncachestore(path, vfstype,
725 723 b'dotencode' in requirements)
726 724
727 725 return storemod.encodedstore(path, vfstype)
728 726
729 727 return storemod.basicstore(path, vfstype)
730 728
731 729 def resolvestorevfsoptions(ui, requirements, features):
732 730 """Resolve the options to pass to the store vfs opener.
733 731
734 732 The returned dict is used to influence behavior of the storage layer.
735 733 """
736 734 options = {}
737 735
738 736 if b'treemanifest' in requirements:
739 737 options[b'treemanifest'] = True
740 738
741 739 # experimental config: format.manifestcachesize
742 740 manifestcachesize = ui.configint(b'format', b'manifestcachesize')
743 741 if manifestcachesize is not None:
744 742 options[b'manifestcachesize'] = manifestcachesize
745 743
746 744 # In the absence of another requirement superseding a revlog-related
747 745 # requirement, we have to assume the repo is using revlog version 0.
748 746 # This revlog format is super old and we don't bother trying to parse
749 747 # opener options for it because those options wouldn't do anything
750 748 # meaningful on such old repos.
751 749 if b'revlogv1' in requirements or REVLOGV2_REQUIREMENT in requirements:
752 750 options.update(resolverevlogstorevfsoptions(ui, requirements, features))
753 751
754 752 return options
755 753
756 754 def resolverevlogstorevfsoptions(ui, requirements, features):
757 755 """Resolve opener options specific to revlogs."""
758 756
759 757 options = {}
760 758 options[b'flagprocessors'] = {}
761 759
762 760 if b'revlogv1' in requirements:
763 761 options[b'revlogv1'] = True
764 762 if REVLOGV2_REQUIREMENT in requirements:
765 763 options[b'revlogv2'] = True
766 764
767 765 if b'generaldelta' in requirements:
768 766 options[b'generaldelta'] = True
769 767
770 768 # experimental config: format.chunkcachesize
771 769 chunkcachesize = ui.configint(b'format', b'chunkcachesize')
772 770 if chunkcachesize is not None:
773 771 options[b'chunkcachesize'] = chunkcachesize
774 772
775 773 deltabothparents = ui.configbool(b'storage',
776 774 b'revlog.optimize-delta-parent-choice')
777 775 options[b'deltabothparents'] = deltabothparents
778 776
779 777 lazydelta = ui.configbool(b'storage', b'revlog.reuse-external-delta')
780 778 lazydeltabase = False
781 779 if lazydelta:
782 780 lazydeltabase = ui.configbool(b'storage',
783 781 b'revlog.reuse-external-delta-parent')
784 782 if lazydeltabase is None:
785 783 lazydeltabase = not scmutil.gddeltaconfig(ui)
786 784 options[b'lazydelta'] = lazydelta
787 785 options[b'lazydeltabase'] = lazydeltabase
788 786
789 787 chainspan = ui.configbytes(b'experimental', b'maxdeltachainspan')
790 788 if 0 <= chainspan:
791 789 options[b'maxdeltachainspan'] = chainspan
792 790
793 791 mmapindexthreshold = ui.configbytes(b'experimental',
794 792 b'mmapindexthreshold')
795 793 if mmapindexthreshold is not None:
796 794 options[b'mmapindexthreshold'] = mmapindexthreshold
797 795
798 796 withsparseread = ui.configbool(b'experimental', b'sparse-read')
799 797 srdensitythres = float(ui.config(b'experimental',
800 798 b'sparse-read.density-threshold'))
801 799 srmingapsize = ui.configbytes(b'experimental',
802 800 b'sparse-read.min-gap-size')
803 801 options[b'with-sparse-read'] = withsparseread
804 802 options[b'sparse-read-density-threshold'] = srdensitythres
805 803 options[b'sparse-read-min-gap-size'] = srmingapsize
806 804
807 805 sparserevlog = SPARSEREVLOG_REQUIREMENT in requirements
808 806 options[b'sparse-revlog'] = sparserevlog
809 807 if sparserevlog:
810 808 options[b'generaldelta'] = True
811 809
812 810 maxchainlen = None
813 811 if sparserevlog:
814 812 maxchainlen = revlogconst.SPARSE_REVLOG_MAX_CHAIN_LENGTH
815 813 # experimental config: format.maxchainlen
816 814 maxchainlen = ui.configint(b'format', b'maxchainlen', maxchainlen)
817 815 if maxchainlen is not None:
818 816 options[b'maxchainlen'] = maxchainlen
819 817
820 818 for r in requirements:
821 819 # we allow multiple compression engine requirement to co-exist because
822 820 # strickly speaking, revlog seems to support mixed compression style.
823 821 #
824 822 # The compression used for new entries will be "the last one"
825 823 prefix = r.startswith
826 824 if prefix('revlog-compression-') or prefix('exp-compression-'):
827 825 options[b'compengine'] = r.split('-', 2)[2]
828 826
829 827 options[b'zlib.level'] = ui.configint(b'storage', b'revlog.zlib.level')
830 828 if options[b'zlib.level'] is not None:
831 829 if not (0 <= options[b'zlib.level'] <= 9):
832 830 msg = _('invalid value for `storage.revlog.zlib.level` config: %d')
833 831 raise error.Abort(msg % options[b'zlib.level'])
834 832 options[b'zstd.level'] = ui.configint(b'storage', b'revlog.zstd.level')
835 833 if options[b'zstd.level'] is not None:
836 834 if not (0 <= options[b'zstd.level'] <= 22):
837 835 msg = _('invalid value for `storage.revlog.zstd.level` config: %d')
838 836 raise error.Abort(msg % options[b'zstd.level'])
839 837
840 838 if repository.NARROW_REQUIREMENT in requirements:
841 839 options[b'enableellipsis'] = True
842 840
843 841 return options
844 842
845 843 def makemain(**kwargs):
846 844 """Produce a type conforming to ``ilocalrepositorymain``."""
847 845 return localrepository
848 846
849 847 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
850 848 class revlogfilestorage(object):
851 849 """File storage when using revlogs."""
852 850
853 851 def file(self, path):
854 852 if path[0] == b'/':
855 853 path = path[1:]
856 854
857 855 return filelog.filelog(self.svfs, path)
858 856
859 857 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
860 858 class revlognarrowfilestorage(object):
861 859 """File storage when using revlogs and narrow files."""
862 860
863 861 def file(self, path):
864 862 if path[0] == b'/':
865 863 path = path[1:]
866 864
867 865 return filelog.narrowfilelog(self.svfs, path, self._storenarrowmatch)
868 866
869 867 def makefilestorage(requirements, features, **kwargs):
870 868 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
871 869 features.add(repository.REPO_FEATURE_REVLOG_FILE_STORAGE)
872 870 features.add(repository.REPO_FEATURE_STREAM_CLONE)
873 871
874 872 if repository.NARROW_REQUIREMENT in requirements:
875 873 return revlognarrowfilestorage
876 874 else:
877 875 return revlogfilestorage
878 876
879 877 # List of repository interfaces and factory functions for them. Each
880 878 # will be called in order during ``makelocalrepository()`` to iteratively
881 879 # derive the final type for a local repository instance. We capture the
882 880 # function as a lambda so we don't hold a reference and the module-level
883 881 # functions can be wrapped.
884 882 REPO_INTERFACES = [
885 883 (repository.ilocalrepositorymain, lambda: makemain),
886 884 (repository.ilocalrepositoryfilestorage, lambda: makefilestorage),
887 885 ]
888 886
889 887 @interfaceutil.implementer(repository.ilocalrepositorymain)
890 888 class localrepository(object):
891 889 """Main class for representing local repositories.
892 890
893 891 All local repositories are instances of this class.
894 892
895 893 Constructed on its own, instances of this class are not usable as
896 894 repository objects. To obtain a usable repository object, call
897 895 ``hg.repository()``, ``localrepo.instance()``, or
898 896 ``localrepo.makelocalrepository()``. The latter is the lowest-level.
899 897 ``instance()`` adds support for creating new repositories.
900 898 ``hg.repository()`` adds more extension integration, including calling
901 899 ``reposetup()``. Generally speaking, ``hg.repository()`` should be
902 900 used.
903 901 """
904 902
905 903 # obsolete experimental requirements:
906 904 # - manifestv2: An experimental new manifest format that allowed
907 905 # for stem compression of long paths. Experiment ended up not
908 906 # being successful (repository sizes went up due to worse delta
909 907 # chains), and the code was deleted in 4.6.
910 908 supportedformats = {
911 909 'revlogv1',
912 910 'generaldelta',
913 911 'treemanifest',
914 912 REVLOGV2_REQUIREMENT,
915 913 SPARSEREVLOG_REQUIREMENT,
916 914 bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT,
917 915 }
918 916 _basesupported = supportedformats | {
919 917 'store',
920 918 'fncache',
921 919 'shared',
922 920 'relshared',
923 921 'dotencode',
924 922 'exp-sparse',
925 923 'internal-phase'
926 924 }
927 925
928 926 # list of prefix for file which can be written without 'wlock'
929 927 # Extensions should extend this list when needed
930 928 _wlockfreeprefix = {
931 929 # We migh consider requiring 'wlock' for the next
932 930 # two, but pretty much all the existing code assume
933 931 # wlock is not needed so we keep them excluded for
934 932 # now.
935 933 'hgrc',
936 934 'requires',
937 935 # XXX cache is a complicatged business someone
938 936 # should investigate this in depth at some point
939 937 'cache/',
940 938 # XXX shouldn't be dirstate covered by the wlock?
941 939 'dirstate',
942 940 # XXX bisect was still a bit too messy at the time
943 941 # this changeset was introduced. Someone should fix
944 942 # the remainig bit and drop this line
945 943 'bisect.state',
946 944 }
947 945
948 946 def __init__(self, baseui, ui, origroot, wdirvfs, hgvfs, requirements,
949 947 supportedrequirements, sharedpath, store, cachevfs, wcachevfs,
950 948 features, intents=None):
951 949 """Create a new local repository instance.
952 950
953 951 Most callers should use ``hg.repository()``, ``localrepo.instance()``,
954 952 or ``localrepo.makelocalrepository()`` for obtaining a new repository
955 953 object.
956 954
957 955 Arguments:
958 956
959 957 baseui
960 958 ``ui.ui`` instance that ``ui`` argument was based off of.
961 959
962 960 ui
963 961 ``ui.ui`` instance for use by the repository.
964 962
965 963 origroot
966 964 ``bytes`` path to working directory root of this repository.
967 965
968 966 wdirvfs
969 967 ``vfs.vfs`` rooted at the working directory.
970 968
971 969 hgvfs
972 970 ``vfs.vfs`` rooted at .hg/
973 971
974 972 requirements
975 973 ``set`` of bytestrings representing repository opening requirements.
976 974
977 975 supportedrequirements
978 976 ``set`` of bytestrings representing repository requirements that we
979 977 know how to open. May be a supetset of ``requirements``.
980 978
981 979 sharedpath
982 980 ``bytes`` Defining path to storage base directory. Points to a
983 981 ``.hg/`` directory somewhere.
984 982
985 983 store
986 984 ``store.basicstore`` (or derived) instance providing access to
987 985 versioned storage.
988 986
989 987 cachevfs
990 988 ``vfs.vfs`` used for cache files.
991 989
992 990 wcachevfs
993 991 ``vfs.vfs`` used for cache files related to the working copy.
994 992
995 993 features
996 994 ``set`` of bytestrings defining features/capabilities of this
997 995 instance.
998 996
999 997 intents
1000 998 ``set`` of system strings indicating what this repo will be used
1001 999 for.
1002 1000 """
1003 1001 self.baseui = baseui
1004 1002 self.ui = ui
1005 1003 self.origroot = origroot
1006 1004 # vfs rooted at working directory.
1007 1005 self.wvfs = wdirvfs
1008 1006 self.root = wdirvfs.base
1009 1007 # vfs rooted at .hg/. Used to access most non-store paths.
1010 1008 self.vfs = hgvfs
1011 1009 self.path = hgvfs.base
1012 1010 self.requirements = requirements
1013 1011 self.supported = supportedrequirements
1014 1012 self.sharedpath = sharedpath
1015 1013 self.store = store
1016 1014 self.cachevfs = cachevfs
1017 1015 self.wcachevfs = wcachevfs
1018 1016 self.features = features
1019 1017
1020 1018 self.filtername = None
1021 1019
1022 1020 if (self.ui.configbool('devel', 'all-warnings') or
1023 1021 self.ui.configbool('devel', 'check-locks')):
1024 1022 self.vfs.audit = self._getvfsward(self.vfs.audit)
1025 1023 # A list of callback to shape the phase if no data were found.
1026 1024 # Callback are in the form: func(repo, roots) --> processed root.
1027 1025 # This list it to be filled by extension during repo setup
1028 1026 self._phasedefaults = []
1029 1027
1030 1028 color.setup(self.ui)
1031 1029
1032 1030 self.spath = self.store.path
1033 1031 self.svfs = self.store.vfs
1034 1032 self.sjoin = self.store.join
1035 1033 if (self.ui.configbool('devel', 'all-warnings') or
1036 1034 self.ui.configbool('devel', 'check-locks')):
1037 1035 if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs
1038 1036 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
1039 1037 else: # standard vfs
1040 1038 self.svfs.audit = self._getsvfsward(self.svfs.audit)
1041 1039
1042 1040 self._dirstatevalidatewarned = False
1043 1041
1044 1042 self._branchcaches = branchmap.BranchMapCache()
1045 1043 self._revbranchcache = None
1046 1044 self._filterpats = {}
1047 1045 self._datafilters = {}
1048 1046 self._transref = self._lockref = self._wlockref = None
1049 1047
1050 1048 # A cache for various files under .hg/ that tracks file changes,
1051 1049 # (used by the filecache decorator)
1052 1050 #
1053 1051 # Maps a property name to its util.filecacheentry
1054 1052 self._filecache = {}
1055 1053
1056 1054 # hold sets of revision to be filtered
1057 1055 # should be cleared when something might have changed the filter value:
1058 1056 # - new changesets,
1059 1057 # - phase change,
1060 1058 # - new obsolescence marker,
1061 1059 # - working directory parent change,
1062 1060 # - bookmark changes
1063 1061 self.filteredrevcache = {}
1064 1062
1065 1063 # post-dirstate-status hooks
1066 1064 self._postdsstatus = []
1067 1065
1068 1066 # generic mapping between names and nodes
1069 1067 self.names = namespaces.namespaces()
1070 1068
1071 1069 # Key to signature value.
1072 1070 self._sparsesignaturecache = {}
1073 1071 # Signature to cached matcher instance.
1074 1072 self._sparsematchercache = {}
1075 1073
1076 1074 self._extrafilterid = repoview.extrafilter(ui)
1077 1075
1078 1076 def _getvfsward(self, origfunc):
1079 1077 """build a ward for self.vfs"""
1080 1078 rref = weakref.ref(self)
1081 1079 def checkvfs(path, mode=None):
1082 1080 ret = origfunc(path, mode=mode)
1083 1081 repo = rref()
1084 1082 if (repo is None
1085 1083 or not util.safehasattr(repo, '_wlockref')
1086 1084 or not util.safehasattr(repo, '_lockref')):
1087 1085 return
1088 1086 if mode in (None, 'r', 'rb'):
1089 1087 return
1090 1088 if path.startswith(repo.path):
1091 1089 # truncate name relative to the repository (.hg)
1092 1090 path = path[len(repo.path) + 1:]
1093 1091 if path.startswith('cache/'):
1094 1092 msg = 'accessing cache with vfs instead of cachevfs: "%s"'
1095 1093 repo.ui.develwarn(msg % path, stacklevel=3, config="cache-vfs")
1096 1094 if path.startswith('journal.') or path.startswith('undo.'):
1097 1095 # journal is covered by 'lock'
1098 1096 if repo._currentlock(repo._lockref) is None:
1099 1097 repo.ui.develwarn('write with no lock: "%s"' % path,
1100 1098 stacklevel=3, config='check-locks')
1101 1099 elif repo._currentlock(repo._wlockref) is None:
1102 1100 # rest of vfs files are covered by 'wlock'
1103 1101 #
1104 1102 # exclude special files
1105 1103 for prefix in self._wlockfreeprefix:
1106 1104 if path.startswith(prefix):
1107 1105 return
1108 1106 repo.ui.develwarn('write with no wlock: "%s"' % path,
1109 1107 stacklevel=3, config='check-locks')
1110 1108 return ret
1111 1109 return checkvfs
1112 1110
1113 1111 def _getsvfsward(self, origfunc):
1114 1112 """build a ward for self.svfs"""
1115 1113 rref = weakref.ref(self)
1116 1114 def checksvfs(path, mode=None):
1117 1115 ret = origfunc(path, mode=mode)
1118 1116 repo = rref()
1119 1117 if repo is None or not util.safehasattr(repo, '_lockref'):
1120 1118 return
1121 1119 if mode in (None, 'r', 'rb'):
1122 1120 return
1123 1121 if path.startswith(repo.sharedpath):
1124 1122 # truncate name relative to the repository (.hg)
1125 1123 path = path[len(repo.sharedpath) + 1:]
1126 1124 if repo._currentlock(repo._lockref) is None:
1127 1125 repo.ui.develwarn('write with no lock: "%s"' % path,
1128 1126 stacklevel=4)
1129 1127 return ret
1130 1128 return checksvfs
1131 1129
1132 1130 def close(self):
1133 1131 self._writecaches()
1134 1132
1135 1133 def _writecaches(self):
1136 1134 if self._revbranchcache:
1137 1135 self._revbranchcache.write()
1138 1136
1139 1137 def _restrictcapabilities(self, caps):
1140 1138 if self.ui.configbool('experimental', 'bundle2-advertise'):
1141 1139 caps = set(caps)
1142 1140 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self,
1143 1141 role='client'))
1144 1142 caps.add('bundle2=' + urlreq.quote(capsblob))
1145 1143 return caps
1146 1144
1147 1145 def _writerequirements(self):
1148 1146 scmutil.writerequires(self.vfs, self.requirements)
1149 1147
1150 1148 # Don't cache auditor/nofsauditor, or you'll end up with reference cycle:
1151 1149 # self -> auditor -> self._checknested -> self
1152 1150
1153 1151 @property
1154 1152 def auditor(self):
1155 1153 # This is only used by context.workingctx.match in order to
1156 1154 # detect files in subrepos.
1157 1155 return pathutil.pathauditor(self.root, callback=self._checknested)
1158 1156
1159 1157 @property
1160 1158 def nofsauditor(self):
1161 1159 # This is only used by context.basectx.match in order to detect
1162 1160 # files in subrepos.
1163 1161 return pathutil.pathauditor(self.root, callback=self._checknested,
1164 1162 realfs=False, cached=True)
1165 1163
1166 1164 def _checknested(self, path):
1167 1165 """Determine if path is a legal nested repository."""
1168 1166 if not path.startswith(self.root):
1169 1167 return False
1170 1168 subpath = path[len(self.root) + 1:]
1171 1169 normsubpath = util.pconvert(subpath)
1172 1170
1173 1171 # XXX: Checking against the current working copy is wrong in
1174 1172 # the sense that it can reject things like
1175 1173 #
1176 1174 # $ hg cat -r 10 sub/x.txt
1177 1175 #
1178 1176 # if sub/ is no longer a subrepository in the working copy
1179 1177 # parent revision.
1180 1178 #
1181 1179 # However, it can of course also allow things that would have
1182 1180 # been rejected before, such as the above cat command if sub/
1183 1181 # is a subrepository now, but was a normal directory before.
1184 1182 # The old path auditor would have rejected by mistake since it
1185 1183 # panics when it sees sub/.hg/.
1186 1184 #
1187 1185 # All in all, checking against the working copy seems sensible
1188 1186 # since we want to prevent access to nested repositories on
1189 1187 # the filesystem *now*.
1190 1188 ctx = self[None]
1191 1189 parts = util.splitpath(subpath)
1192 1190 while parts:
1193 1191 prefix = '/'.join(parts)
1194 1192 if prefix in ctx.substate:
1195 1193 if prefix == normsubpath:
1196 1194 return True
1197 1195 else:
1198 1196 sub = ctx.sub(prefix)
1199 1197 return sub.checknested(subpath[len(prefix) + 1:])
1200 1198 else:
1201 1199 parts.pop()
1202 1200 return False
1203 1201
1204 1202 def peer(self):
1205 1203 return localpeer(self) # not cached to avoid reference cycle
1206 1204
1207 1205 def unfiltered(self):
1208 1206 """Return unfiltered version of the repository
1209 1207
1210 1208 Intended to be overwritten by filtered repo."""
1211 1209 return self
1212 1210
1213 1211 def filtered(self, name, visibilityexceptions=None):
1214 1212 """Return a filtered version of a repository
1215 1213
1216 1214 The `name` parameter is the identifier of the requested view. This
1217 1215 will return a repoview object set "exactly" to the specified view.
1218 1216
1219 1217 This function does not apply recursive filtering to a repository. For
1220 1218 example calling `repo.filtered("served")` will return a repoview using
1221 1219 the "served" view, regardless of the initial view used by `repo`.
1222 1220
1223 1221 In other word, there is always only one level of `repoview` "filtering".
1224 1222 """
1225 1223 if self._extrafilterid is not None and '%' not in name:
1226 1224 name = name + '%' + self._extrafilterid
1227 1225
1228 1226 cls = repoview.newtype(self.unfiltered().__class__)
1229 1227 return cls(self, name, visibilityexceptions)
1230 1228
1231 1229 @mixedrepostorecache(('bookmarks', 'plain'), ('bookmarks.current', 'plain'),
1232 1230 ('bookmarks', ''), ('00changelog.i', ''))
1233 1231 def _bookmarks(self):
1234 1232 # Since the multiple files involved in the transaction cannot be
1235 1233 # written atomically (with current repository format), there is a race
1236 1234 # condition here.
1237 1235 #
1238 1236 # 1) changelog content A is read
1239 1237 # 2) outside transaction update changelog to content B
1240 1238 # 3) outside transaction update bookmark file referring to content B
1241 1239 # 4) bookmarks file content is read and filtered against changelog-A
1242 1240 #
1243 1241 # When this happens, bookmarks against nodes missing from A are dropped.
1244 1242 #
1245 1243 # Having this happening during read is not great, but it become worse
1246 1244 # when this happen during write because the bookmarks to the "unknown"
1247 1245 # nodes will be dropped for good. However, writes happen within locks.
1248 1246 # This locking makes it possible to have a race free consistent read.
1249 1247 # For this purpose data read from disc before locking are
1250 1248 # "invalidated" right after the locks are taken. This invalidations are
1251 1249 # "light", the `filecache` mechanism keep the data in memory and will
1252 1250 # reuse them if the underlying files did not changed. Not parsing the
1253 1251 # same data multiple times helps performances.
1254 1252 #
1255 1253 # Unfortunately in the case describe above, the files tracked by the
1256 1254 # bookmarks file cache might not have changed, but the in-memory
1257 1255 # content is still "wrong" because we used an older changelog content
1258 1256 # to process the on-disk data. So after locking, the changelog would be
1259 1257 # refreshed but `_bookmarks` would be preserved.
1260 1258 # Adding `00changelog.i` to the list of tracked file is not
1261 1259 # enough, because at the time we build the content for `_bookmarks` in
1262 1260 # (4), the changelog file has already diverged from the content used
1263 1261 # for loading `changelog` in (1)
1264 1262 #
1265 1263 # To prevent the issue, we force the changelog to be explicitly
1266 1264 # reloaded while computing `_bookmarks`. The data race can still happen
1267 1265 # without the lock (with a narrower window), but it would no longer go
1268 1266 # undetected during the lock time refresh.
1269 1267 #
1270 1268 # The new schedule is as follow
1271 1269 #
1272 1270 # 1) filecache logic detect that `_bookmarks` needs to be computed
1273 1271 # 2) cachestat for `bookmarks` and `changelog` are captured (for book)
1274 1272 # 3) We force `changelog` filecache to be tested
1275 1273 # 4) cachestat for `changelog` are captured (for changelog)
1276 1274 # 5) `_bookmarks` is computed and cached
1277 1275 #
1278 1276 # The step in (3) ensure we have a changelog at least as recent as the
1279 1277 # cache stat computed in (1). As a result at locking time:
1280 1278 # * if the changelog did not changed since (1) -> we can reuse the data
1281 1279 # * otherwise -> the bookmarks get refreshed.
1282 1280 self._refreshchangelog()
1283 1281 return bookmarks.bmstore(self)
1284 1282
1285 1283 def _refreshchangelog(self):
1286 1284 """make sure the in memory changelog match the on-disk one"""
1287 1285 if ('changelog' in vars(self) and self.currenttransaction() is None):
1288 1286 del self.changelog
1289 1287
1290 1288 @property
1291 1289 def _activebookmark(self):
1292 1290 return self._bookmarks.active
1293 1291
1294 1292 # _phasesets depend on changelog. what we need is to call
1295 1293 # _phasecache.invalidate() if '00changelog.i' was changed, but it
1296 1294 # can't be easily expressed in filecache mechanism.
1297 1295 @storecache('phaseroots', '00changelog.i')
1298 1296 def _phasecache(self):
1299 1297 return phases.phasecache(self, self._phasedefaults)
1300 1298
1301 1299 @storecache('obsstore')
1302 1300 def obsstore(self):
1303 1301 return obsolete.makestore(self.ui, self)
1304 1302
1305 1303 @storecache('00changelog.i')
1306 1304 def changelog(self):
1307 return changelog.changelog(self.svfs,
1308 trypending=txnutil.mayhavepending(self.root))
1305 return self.store.changelog(txnutil.mayhavepending(self.root))
1309 1306
1310 1307 @storecache('00manifest.i')
1311 1308 def manifestlog(self):
1312 rootstore = manifest.manifestrevlog(self.svfs)
1313 return manifest.manifestlog(self.svfs, self, rootstore,
1314 self._storenarrowmatch)
1309 return self.store.manifestlog(self, self._storenarrowmatch)
1315 1310
1316 1311 @repofilecache('dirstate')
1317 1312 def dirstate(self):
1318 1313 return self._makedirstate()
1319 1314
1320 1315 def _makedirstate(self):
1321 1316 """Extension point for wrapping the dirstate per-repo."""
1322 1317 sparsematchfn = lambda: sparse.matcher(self)
1323 1318
1324 1319 return dirstate.dirstate(self.vfs, self.ui, self.root,
1325 1320 self._dirstatevalidate, sparsematchfn)
1326 1321
1327 1322 def _dirstatevalidate(self, node):
1328 1323 try:
1329 1324 self.changelog.rev(node)
1330 1325 return node
1331 1326 except error.LookupError:
1332 1327 if not self._dirstatevalidatewarned:
1333 1328 self._dirstatevalidatewarned = True
1334 1329 self.ui.warn(_("warning: ignoring unknown"
1335 1330 " working parent %s!\n") % short(node))
1336 1331 return nullid
1337 1332
1338 1333 @storecache(narrowspec.FILENAME)
1339 1334 def narrowpats(self):
1340 1335 """matcher patterns for this repository's narrowspec
1341 1336
1342 1337 A tuple of (includes, excludes).
1343 1338 """
1344 1339 return narrowspec.load(self)
1345 1340
1346 1341 @storecache(narrowspec.FILENAME)
1347 1342 def _storenarrowmatch(self):
1348 1343 if repository.NARROW_REQUIREMENT not in self.requirements:
1349 1344 return matchmod.always()
1350 1345 include, exclude = self.narrowpats
1351 1346 return narrowspec.match(self.root, include=include, exclude=exclude)
1352 1347
1353 1348 @storecache(narrowspec.FILENAME)
1354 1349 def _narrowmatch(self):
1355 1350 if repository.NARROW_REQUIREMENT not in self.requirements:
1356 1351 return matchmod.always()
1357 1352 narrowspec.checkworkingcopynarrowspec(self)
1358 1353 include, exclude = self.narrowpats
1359 1354 return narrowspec.match(self.root, include=include, exclude=exclude)
1360 1355
1361 1356 def narrowmatch(self, match=None, includeexact=False):
1362 1357 """matcher corresponding the the repo's narrowspec
1363 1358
1364 1359 If `match` is given, then that will be intersected with the narrow
1365 1360 matcher.
1366 1361
1367 1362 If `includeexact` is True, then any exact matches from `match` will
1368 1363 be included even if they're outside the narrowspec.
1369 1364 """
1370 1365 if match:
1371 1366 if includeexact and not self._narrowmatch.always():
1372 1367 # do not exclude explicitly-specified paths so that they can
1373 1368 # be warned later on
1374 1369 em = matchmod.exact(match.files())
1375 1370 nm = matchmod.unionmatcher([self._narrowmatch, em])
1376 1371 return matchmod.intersectmatchers(match, nm)
1377 1372 return matchmod.intersectmatchers(match, self._narrowmatch)
1378 1373 return self._narrowmatch
1379 1374
1380 1375 def setnarrowpats(self, newincludes, newexcludes):
1381 1376 narrowspec.save(self, newincludes, newexcludes)
1382 1377 self.invalidate(clearfilecache=True)
1383 1378
1384 1379 def __getitem__(self, changeid):
1385 1380 if changeid is None:
1386 1381 return context.workingctx(self)
1387 1382 if isinstance(changeid, context.basectx):
1388 1383 return changeid
1389 1384 if isinstance(changeid, slice):
1390 1385 # wdirrev isn't contiguous so the slice shouldn't include it
1391 1386 return [self[i]
1392 1387 for i in pycompat.xrange(*changeid.indices(len(self)))
1393 1388 if i not in self.changelog.filteredrevs]
1394 1389 try:
1395 1390 if isinstance(changeid, int):
1396 1391 node = self.changelog.node(changeid)
1397 1392 rev = changeid
1398 1393 elif changeid == 'null':
1399 1394 node = nullid
1400 1395 rev = nullrev
1401 1396 elif changeid == 'tip':
1402 1397 node = self.changelog.tip()
1403 1398 rev = self.changelog.rev(node)
1404 1399 elif changeid == '.':
1405 1400 # this is a hack to delay/avoid loading obsmarkers
1406 1401 # when we know that '.' won't be hidden
1407 1402 node = self.dirstate.p1()
1408 1403 rev = self.unfiltered().changelog.rev(node)
1409 1404 elif len(changeid) == 20:
1410 1405 try:
1411 1406 node = changeid
1412 1407 rev = self.changelog.rev(changeid)
1413 1408 except error.FilteredLookupError:
1414 1409 changeid = hex(changeid) # for the error message
1415 1410 raise
1416 1411 except LookupError:
1417 1412 # check if it might have come from damaged dirstate
1418 1413 #
1419 1414 # XXX we could avoid the unfiltered if we had a recognizable
1420 1415 # exception for filtered changeset access
1421 1416 if (self.local()
1422 1417 and changeid in self.unfiltered().dirstate.parents()):
1423 1418 msg = _("working directory has unknown parent '%s'!")
1424 1419 raise error.Abort(msg % short(changeid))
1425 1420 changeid = hex(changeid) # for the error message
1426 1421 raise
1427 1422
1428 1423 elif len(changeid) == 40:
1429 1424 node = bin(changeid)
1430 1425 rev = self.changelog.rev(node)
1431 1426 else:
1432 1427 raise error.ProgrammingError(
1433 1428 "unsupported changeid '%s' of type %s" %
1434 1429 (changeid, type(changeid)))
1435 1430
1436 1431 return context.changectx(self, rev, node)
1437 1432
1438 1433 except (error.FilteredIndexError, error.FilteredLookupError):
1439 1434 raise error.FilteredRepoLookupError(_("filtered revision '%s'")
1440 1435 % pycompat.bytestr(changeid))
1441 1436 except (IndexError, LookupError):
1442 1437 raise error.RepoLookupError(
1443 1438 _("unknown revision '%s'") % pycompat.bytestr(changeid))
1444 1439 except error.WdirUnsupported:
1445 1440 return context.workingctx(self)
1446 1441
1447 1442 def __contains__(self, changeid):
1448 1443 """True if the given changeid exists
1449 1444
1450 1445 error.AmbiguousPrefixLookupError is raised if an ambiguous node
1451 1446 specified.
1452 1447 """
1453 1448 try:
1454 1449 self[changeid]
1455 1450 return True
1456 1451 except error.RepoLookupError:
1457 1452 return False
1458 1453
1459 1454 def __nonzero__(self):
1460 1455 return True
1461 1456
1462 1457 __bool__ = __nonzero__
1463 1458
1464 1459 def __len__(self):
1465 1460 # no need to pay the cost of repoview.changelog
1466 1461 unfi = self.unfiltered()
1467 1462 return len(unfi.changelog)
1468 1463
1469 1464 def __iter__(self):
1470 1465 return iter(self.changelog)
1471 1466
1472 1467 def revs(self, expr, *args):
1473 1468 '''Find revisions matching a revset.
1474 1469
1475 1470 The revset is specified as a string ``expr`` that may contain
1476 1471 %-formatting to escape certain types. See ``revsetlang.formatspec``.
1477 1472
1478 1473 Revset aliases from the configuration are not expanded. To expand
1479 1474 user aliases, consider calling ``scmutil.revrange()`` or
1480 1475 ``repo.anyrevs([expr], user=True)``.
1481 1476
1482 1477 Returns a revset.abstractsmartset, which is a list-like interface
1483 1478 that contains integer revisions.
1484 1479 '''
1485 1480 tree = revsetlang.spectree(expr, *args)
1486 1481 return revset.makematcher(tree)(self)
1487 1482
1488 1483 def set(self, expr, *args):
1489 1484 '''Find revisions matching a revset and emit changectx instances.
1490 1485
1491 1486 This is a convenience wrapper around ``revs()`` that iterates the
1492 1487 result and is a generator of changectx instances.
1493 1488
1494 1489 Revset aliases from the configuration are not expanded. To expand
1495 1490 user aliases, consider calling ``scmutil.revrange()``.
1496 1491 '''
1497 1492 for r in self.revs(expr, *args):
1498 1493 yield self[r]
1499 1494
1500 1495 def anyrevs(self, specs, user=False, localalias=None):
1501 1496 '''Find revisions matching one of the given revsets.
1502 1497
1503 1498 Revset aliases from the configuration are not expanded by default. To
1504 1499 expand user aliases, specify ``user=True``. To provide some local
1505 1500 definitions overriding user aliases, set ``localalias`` to
1506 1501 ``{name: definitionstring}``.
1507 1502 '''
1508 1503 if user:
1509 1504 m = revset.matchany(self.ui, specs,
1510 1505 lookup=revset.lookupfn(self),
1511 1506 localalias=localalias)
1512 1507 else:
1513 1508 m = revset.matchany(None, specs, localalias=localalias)
1514 1509 return m(self)
1515 1510
1516 1511 def url(self):
1517 1512 return 'file:' + self.root
1518 1513
1519 1514 def hook(self, name, throw=False, **args):
1520 1515 """Call a hook, passing this repo instance.
1521 1516
1522 1517 This a convenience method to aid invoking hooks. Extensions likely
1523 1518 won't call this unless they have registered a custom hook or are
1524 1519 replacing code that is expected to call a hook.
1525 1520 """
1526 1521 return hook.hook(self.ui, self, name, throw, **args)
1527 1522
1528 1523 @filteredpropertycache
1529 1524 def _tagscache(self):
1530 1525 '''Returns a tagscache object that contains various tags related
1531 1526 caches.'''
1532 1527
1533 1528 # This simplifies its cache management by having one decorated
1534 1529 # function (this one) and the rest simply fetch things from it.
1535 1530 class tagscache(object):
1536 1531 def __init__(self):
1537 1532 # These two define the set of tags for this repository. tags
1538 1533 # maps tag name to node; tagtypes maps tag name to 'global' or
1539 1534 # 'local'. (Global tags are defined by .hgtags across all
1540 1535 # heads, and local tags are defined in .hg/localtags.)
1541 1536 # They constitute the in-memory cache of tags.
1542 1537 self.tags = self.tagtypes = None
1543 1538
1544 1539 self.nodetagscache = self.tagslist = None
1545 1540
1546 1541 cache = tagscache()
1547 1542 cache.tags, cache.tagtypes = self._findtags()
1548 1543
1549 1544 return cache
1550 1545
1551 1546 def tags(self):
1552 1547 '''return a mapping of tag to node'''
1553 1548 t = {}
1554 1549 if self.changelog.filteredrevs:
1555 1550 tags, tt = self._findtags()
1556 1551 else:
1557 1552 tags = self._tagscache.tags
1558 1553 rev = self.changelog.rev
1559 1554 for k, v in tags.iteritems():
1560 1555 try:
1561 1556 # ignore tags to unknown nodes
1562 1557 rev(v)
1563 1558 t[k] = v
1564 1559 except (error.LookupError, ValueError):
1565 1560 pass
1566 1561 return t
1567 1562
1568 1563 def _findtags(self):
1569 1564 '''Do the hard work of finding tags. Return a pair of dicts
1570 1565 (tags, tagtypes) where tags maps tag name to node, and tagtypes
1571 1566 maps tag name to a string like \'global\' or \'local\'.
1572 1567 Subclasses or extensions are free to add their own tags, but
1573 1568 should be aware that the returned dicts will be retained for the
1574 1569 duration of the localrepo object.'''
1575 1570
1576 1571 # XXX what tagtype should subclasses/extensions use? Currently
1577 1572 # mq and bookmarks add tags, but do not set the tagtype at all.
1578 1573 # Should each extension invent its own tag type? Should there
1579 1574 # be one tagtype for all such "virtual" tags? Or is the status
1580 1575 # quo fine?
1581 1576
1582 1577
1583 1578 # map tag name to (node, hist)
1584 1579 alltags = tagsmod.findglobaltags(self.ui, self)
1585 1580 # map tag name to tag type
1586 1581 tagtypes = dict((tag, 'global') for tag in alltags)
1587 1582
1588 1583 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
1589 1584
1590 1585 # Build the return dicts. Have to re-encode tag names because
1591 1586 # the tags module always uses UTF-8 (in order not to lose info
1592 1587 # writing to the cache), but the rest of Mercurial wants them in
1593 1588 # local encoding.
1594 1589 tags = {}
1595 1590 for (name, (node, hist)) in alltags.iteritems():
1596 1591 if node != nullid:
1597 1592 tags[encoding.tolocal(name)] = node
1598 1593 tags['tip'] = self.changelog.tip()
1599 1594 tagtypes = dict([(encoding.tolocal(name), value)
1600 1595 for (name, value) in tagtypes.iteritems()])
1601 1596 return (tags, tagtypes)
1602 1597
1603 1598 def tagtype(self, tagname):
1604 1599 '''
1605 1600 return the type of the given tag. result can be:
1606 1601
1607 1602 'local' : a local tag
1608 1603 'global' : a global tag
1609 1604 None : tag does not exist
1610 1605 '''
1611 1606
1612 1607 return self._tagscache.tagtypes.get(tagname)
1613 1608
1614 1609 def tagslist(self):
1615 1610 '''return a list of tags ordered by revision'''
1616 1611 if not self._tagscache.tagslist:
1617 1612 l = []
1618 1613 for t, n in self.tags().iteritems():
1619 1614 l.append((self.changelog.rev(n), t, n))
1620 1615 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
1621 1616
1622 1617 return self._tagscache.tagslist
1623 1618
1624 1619 def nodetags(self, node):
1625 1620 '''return the tags associated with a node'''
1626 1621 if not self._tagscache.nodetagscache:
1627 1622 nodetagscache = {}
1628 1623 for t, n in self._tagscache.tags.iteritems():
1629 1624 nodetagscache.setdefault(n, []).append(t)
1630 1625 for tags in nodetagscache.itervalues():
1631 1626 tags.sort()
1632 1627 self._tagscache.nodetagscache = nodetagscache
1633 1628 return self._tagscache.nodetagscache.get(node, [])
1634 1629
1635 1630 def nodebookmarks(self, node):
1636 1631 """return the list of bookmarks pointing to the specified node"""
1637 1632 return self._bookmarks.names(node)
1638 1633
1639 1634 def branchmap(self):
1640 1635 '''returns a dictionary {branch: [branchheads]} with branchheads
1641 1636 ordered by increasing revision number'''
1642 1637 return self._branchcaches[self]
1643 1638
1644 1639 @unfilteredmethod
1645 1640 def revbranchcache(self):
1646 1641 if not self._revbranchcache:
1647 1642 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
1648 1643 return self._revbranchcache
1649 1644
1650 1645 def branchtip(self, branch, ignoremissing=False):
1651 1646 '''return the tip node for a given branch
1652 1647
1653 1648 If ignoremissing is True, then this method will not raise an error.
1654 1649 This is helpful for callers that only expect None for a missing branch
1655 1650 (e.g. namespace).
1656 1651
1657 1652 '''
1658 1653 try:
1659 1654 return self.branchmap().branchtip(branch)
1660 1655 except KeyError:
1661 1656 if not ignoremissing:
1662 1657 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
1663 1658 else:
1664 1659 pass
1665 1660
1666 1661 def lookup(self, key):
1667 1662 node = scmutil.revsymbol(self, key).node()
1668 1663 if node is None:
1669 1664 raise error.RepoLookupError(_("unknown revision '%s'") % key)
1670 1665 return node
1671 1666
1672 1667 def lookupbranch(self, key):
1673 1668 if self.branchmap().hasbranch(key):
1674 1669 return key
1675 1670
1676 1671 return scmutil.revsymbol(self, key).branch()
1677 1672
1678 1673 def known(self, nodes):
1679 1674 cl = self.changelog
1680 1675 nm = cl.nodemap
1681 1676 filtered = cl.filteredrevs
1682 1677 result = []
1683 1678 for n in nodes:
1684 1679 r = nm.get(n)
1685 1680 resp = not (r is None or r in filtered)
1686 1681 result.append(resp)
1687 1682 return result
1688 1683
1689 1684 def local(self):
1690 1685 return self
1691 1686
1692 1687 def publishing(self):
1693 1688 # it's safe (and desirable) to trust the publish flag unconditionally
1694 1689 # so that we don't finalize changes shared between users via ssh or nfs
1695 1690 return self.ui.configbool('phases', 'publish', untrusted=True)
1696 1691
1697 1692 def cancopy(self):
1698 1693 # so statichttprepo's override of local() works
1699 1694 if not self.local():
1700 1695 return False
1701 1696 if not self.publishing():
1702 1697 return True
1703 1698 # if publishing we can't copy if there is filtered content
1704 1699 return not self.filtered('visible').changelog.filteredrevs
1705 1700
1706 1701 def shared(self):
1707 1702 '''the type of shared repository (None if not shared)'''
1708 1703 if self.sharedpath != self.path:
1709 1704 return 'store'
1710 1705 return None
1711 1706
1712 1707 def wjoin(self, f, *insidef):
1713 1708 return self.vfs.reljoin(self.root, f, *insidef)
1714 1709
1715 1710 def setparents(self, p1, p2=nullid):
1716 1711 with self.dirstate.parentchange():
1717 1712 copies = self.dirstate.setparents(p1, p2)
1718 1713 pctx = self[p1]
1719 1714 if copies:
1720 1715 # Adjust copy records, the dirstate cannot do it, it
1721 1716 # requires access to parents manifests. Preserve them
1722 1717 # only for entries added to first parent.
1723 1718 for f in copies:
1724 1719 if f not in pctx and copies[f] in pctx:
1725 1720 self.dirstate.copy(copies[f], f)
1726 1721 if p2 == nullid:
1727 1722 for f, s in sorted(self.dirstate.copies().items()):
1728 1723 if f not in pctx and s not in pctx:
1729 1724 self.dirstate.copy(None, f)
1730 1725
1731 1726 def filectx(self, path, changeid=None, fileid=None, changectx=None):
1732 1727 """changeid must be a changeset revision, if specified.
1733 1728 fileid can be a file revision or node."""
1734 1729 return context.filectx(self, path, changeid, fileid,
1735 1730 changectx=changectx)
1736 1731
1737 1732 def getcwd(self):
1738 1733 return self.dirstate.getcwd()
1739 1734
1740 1735 def pathto(self, f, cwd=None):
1741 1736 return self.dirstate.pathto(f, cwd)
1742 1737
1743 1738 def _loadfilter(self, filter):
1744 1739 if filter not in self._filterpats:
1745 1740 l = []
1746 1741 for pat, cmd in self.ui.configitems(filter):
1747 1742 if cmd == '!':
1748 1743 continue
1749 1744 mf = matchmod.match(self.root, '', [pat])
1750 1745 fn = None
1751 1746 params = cmd
1752 1747 for name, filterfn in self._datafilters.iteritems():
1753 1748 if cmd.startswith(name):
1754 1749 fn = filterfn
1755 1750 params = cmd[len(name):].lstrip()
1756 1751 break
1757 1752 if not fn:
1758 1753 fn = lambda s, c, **kwargs: procutil.filter(s, c)
1759 1754 # Wrap old filters not supporting keyword arguments
1760 1755 if not pycompat.getargspec(fn)[2]:
1761 1756 oldfn = fn
1762 1757 fn = lambda s, c, **kwargs: oldfn(s, c)
1763 1758 l.append((mf, fn, params))
1764 1759 self._filterpats[filter] = l
1765 1760 return self._filterpats[filter]
1766 1761
1767 1762 def _filter(self, filterpats, filename, data):
1768 1763 for mf, fn, cmd in filterpats:
1769 1764 if mf(filename):
1770 1765 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
1771 1766 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1772 1767 break
1773 1768
1774 1769 return data
1775 1770
1776 1771 @unfilteredpropertycache
1777 1772 def _encodefilterpats(self):
1778 1773 return self._loadfilter('encode')
1779 1774
1780 1775 @unfilteredpropertycache
1781 1776 def _decodefilterpats(self):
1782 1777 return self._loadfilter('decode')
1783 1778
1784 1779 def adddatafilter(self, name, filter):
1785 1780 self._datafilters[name] = filter
1786 1781
1787 1782 def wread(self, filename):
1788 1783 if self.wvfs.islink(filename):
1789 1784 data = self.wvfs.readlink(filename)
1790 1785 else:
1791 1786 data = self.wvfs.read(filename)
1792 1787 return self._filter(self._encodefilterpats, filename, data)
1793 1788
1794 1789 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
1795 1790 """write ``data`` into ``filename`` in the working directory
1796 1791
1797 1792 This returns length of written (maybe decoded) data.
1798 1793 """
1799 1794 data = self._filter(self._decodefilterpats, filename, data)
1800 1795 if 'l' in flags:
1801 1796 self.wvfs.symlink(data, filename)
1802 1797 else:
1803 1798 self.wvfs.write(filename, data, backgroundclose=backgroundclose,
1804 1799 **kwargs)
1805 1800 if 'x' in flags:
1806 1801 self.wvfs.setflags(filename, False, True)
1807 1802 else:
1808 1803 self.wvfs.setflags(filename, False, False)
1809 1804 return len(data)
1810 1805
1811 1806 def wwritedata(self, filename, data):
1812 1807 return self._filter(self._decodefilterpats, filename, data)
1813 1808
1814 1809 def currenttransaction(self):
1815 1810 """return the current transaction or None if non exists"""
1816 1811 if self._transref:
1817 1812 tr = self._transref()
1818 1813 else:
1819 1814 tr = None
1820 1815
1821 1816 if tr and tr.running():
1822 1817 return tr
1823 1818 return None
1824 1819
1825 1820 def transaction(self, desc, report=None):
1826 1821 if (self.ui.configbool('devel', 'all-warnings')
1827 1822 or self.ui.configbool('devel', 'check-locks')):
1828 1823 if self._currentlock(self._lockref) is None:
1829 1824 raise error.ProgrammingError('transaction requires locking')
1830 1825 tr = self.currenttransaction()
1831 1826 if tr is not None:
1832 1827 return tr.nest(name=desc)
1833 1828
1834 1829 # abort here if the journal already exists
1835 1830 if self.svfs.exists("journal"):
1836 1831 raise error.RepoError(
1837 1832 _("abandoned transaction found"),
1838 1833 hint=_("run 'hg recover' to clean up transaction"))
1839 1834
1840 1835 idbase = "%.40f#%f" % (random.random(), time.time())
1841 1836 ha = hex(hashlib.sha1(idbase).digest())
1842 1837 txnid = 'TXN:' + ha
1843 1838 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1844 1839
1845 1840 self._writejournal(desc)
1846 1841 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1847 1842 if report:
1848 1843 rp = report
1849 1844 else:
1850 1845 rp = self.ui.warn
1851 1846 vfsmap = {'plain': self.vfs, 'store': self.svfs} # root of .hg/
1852 1847 # we must avoid cyclic reference between repo and transaction.
1853 1848 reporef = weakref.ref(self)
1854 1849 # Code to track tag movement
1855 1850 #
1856 1851 # Since tags are all handled as file content, it is actually quite hard
1857 1852 # to track these movement from a code perspective. So we fallback to a
1858 1853 # tracking at the repository level. One could envision to track changes
1859 1854 # to the '.hgtags' file through changegroup apply but that fails to
1860 1855 # cope with case where transaction expose new heads without changegroup
1861 1856 # being involved (eg: phase movement).
1862 1857 #
1863 1858 # For now, We gate the feature behind a flag since this likely comes
1864 1859 # with performance impacts. The current code run more often than needed
1865 1860 # and do not use caches as much as it could. The current focus is on
1866 1861 # the behavior of the feature so we disable it by default. The flag
1867 1862 # will be removed when we are happy with the performance impact.
1868 1863 #
1869 1864 # Once this feature is no longer experimental move the following
1870 1865 # documentation to the appropriate help section:
1871 1866 #
1872 1867 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
1873 1868 # tags (new or changed or deleted tags). In addition the details of
1874 1869 # these changes are made available in a file at:
1875 1870 # ``REPOROOT/.hg/changes/tags.changes``.
1876 1871 # Make sure you check for HG_TAG_MOVED before reading that file as it
1877 1872 # might exist from a previous transaction even if no tag were touched
1878 1873 # in this one. Changes are recorded in a line base format::
1879 1874 #
1880 1875 # <action> <hex-node> <tag-name>\n
1881 1876 #
1882 1877 # Actions are defined as follow:
1883 1878 # "-R": tag is removed,
1884 1879 # "+A": tag is added,
1885 1880 # "-M": tag is moved (old value),
1886 1881 # "+M": tag is moved (new value),
1887 1882 tracktags = lambda x: None
1888 1883 # experimental config: experimental.hook-track-tags
1889 1884 shouldtracktags = self.ui.configbool('experimental', 'hook-track-tags')
1890 1885 if desc != 'strip' and shouldtracktags:
1891 1886 oldheads = self.changelog.headrevs()
1892 1887 def tracktags(tr2):
1893 1888 repo = reporef()
1894 1889 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
1895 1890 newheads = repo.changelog.headrevs()
1896 1891 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
1897 1892 # notes: we compare lists here.
1898 1893 # As we do it only once buiding set would not be cheaper
1899 1894 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
1900 1895 if changes:
1901 1896 tr2.hookargs['tag_moved'] = '1'
1902 1897 with repo.vfs('changes/tags.changes', 'w',
1903 1898 atomictemp=True) as changesfile:
1904 1899 # note: we do not register the file to the transaction
1905 1900 # because we needs it to still exist on the transaction
1906 1901 # is close (for txnclose hooks)
1907 1902 tagsmod.writediff(changesfile, changes)
1908 1903 def validate(tr2):
1909 1904 """will run pre-closing hooks"""
1910 1905 # XXX the transaction API is a bit lacking here so we take a hacky
1911 1906 # path for now
1912 1907 #
1913 1908 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
1914 1909 # dict is copied before these run. In addition we needs the data
1915 1910 # available to in memory hooks too.
1916 1911 #
1917 1912 # Moreover, we also need to make sure this runs before txnclose
1918 1913 # hooks and there is no "pending" mechanism that would execute
1919 1914 # logic only if hooks are about to run.
1920 1915 #
1921 1916 # Fixing this limitation of the transaction is also needed to track
1922 1917 # other families of changes (bookmarks, phases, obsolescence).
1923 1918 #
1924 1919 # This will have to be fixed before we remove the experimental
1925 1920 # gating.
1926 1921 tracktags(tr2)
1927 1922 repo = reporef()
1928 1923 if repo.ui.configbool('experimental', 'single-head-per-branch'):
1929 1924 scmutil.enforcesinglehead(repo, tr2, desc)
1930 1925 if hook.hashook(repo.ui, 'pretxnclose-bookmark'):
1931 1926 for name, (old, new) in sorted(tr.changes['bookmarks'].items()):
1932 1927 args = tr.hookargs.copy()
1933 1928 args.update(bookmarks.preparehookargs(name, old, new))
1934 1929 repo.hook('pretxnclose-bookmark', throw=True,
1935 1930 **pycompat.strkwargs(args))
1936 1931 if hook.hashook(repo.ui, 'pretxnclose-phase'):
1937 1932 cl = repo.unfiltered().changelog
1938 1933 for rev, (old, new) in tr.changes['phases'].items():
1939 1934 args = tr.hookargs.copy()
1940 1935 node = hex(cl.node(rev))
1941 1936 args.update(phases.preparehookargs(node, old, new))
1942 1937 repo.hook('pretxnclose-phase', throw=True,
1943 1938 **pycompat.strkwargs(args))
1944 1939
1945 1940 repo.hook('pretxnclose', throw=True,
1946 1941 **pycompat.strkwargs(tr.hookargs))
1947 1942 def releasefn(tr, success):
1948 1943 repo = reporef()
1949 1944 if repo is None:
1950 1945 # If the repo has been GC'd (and this release function is being
1951 1946 # called from transaction.__del__), there's not much we can do,
1952 1947 # so just leave the unfinished transaction there and let the
1953 1948 # user run `hg recover`.
1954 1949 return
1955 1950 if success:
1956 1951 # this should be explicitly invoked here, because
1957 1952 # in-memory changes aren't written out at closing
1958 1953 # transaction, if tr.addfilegenerator (via
1959 1954 # dirstate.write or so) isn't invoked while
1960 1955 # transaction running
1961 1956 repo.dirstate.write(None)
1962 1957 else:
1963 1958 # discard all changes (including ones already written
1964 1959 # out) in this transaction
1965 1960 narrowspec.restorebackup(self, 'journal.narrowspec')
1966 1961 narrowspec.restorewcbackup(self, 'journal.narrowspec.dirstate')
1967 1962 repo.dirstate.restorebackup(None, 'journal.dirstate')
1968 1963
1969 1964 repo.invalidate(clearfilecache=True)
1970 1965
1971 1966 tr = transaction.transaction(rp, self.svfs, vfsmap,
1972 1967 "journal",
1973 1968 "undo",
1974 1969 aftertrans(renames),
1975 1970 self.store.createmode,
1976 1971 validator=validate,
1977 1972 releasefn=releasefn,
1978 1973 checkambigfiles=_cachedfiles,
1979 1974 name=desc)
1980 1975 tr.changes['origrepolen'] = len(self)
1981 1976 tr.changes['obsmarkers'] = set()
1982 1977 tr.changes['phases'] = {}
1983 1978 tr.changes['bookmarks'] = {}
1984 1979
1985 1980 tr.hookargs['txnid'] = txnid
1986 1981 tr.hookargs['txnname'] = desc
1987 1982 # note: writing the fncache only during finalize mean that the file is
1988 1983 # outdated when running hooks. As fncache is used for streaming clone,
1989 1984 # this is not expected to break anything that happen during the hooks.
1990 1985 tr.addfinalize('flush-fncache', self.store.write)
1991 1986 def txnclosehook(tr2):
1992 1987 """To be run if transaction is successful, will schedule a hook run
1993 1988 """
1994 1989 # Don't reference tr2 in hook() so we don't hold a reference.
1995 1990 # This reduces memory consumption when there are multiple
1996 1991 # transactions per lock. This can likely go away if issue5045
1997 1992 # fixes the function accumulation.
1998 1993 hookargs = tr2.hookargs
1999 1994
2000 1995 def hookfunc():
2001 1996 repo = reporef()
2002 1997 if hook.hashook(repo.ui, 'txnclose-bookmark'):
2003 1998 bmchanges = sorted(tr.changes['bookmarks'].items())
2004 1999 for name, (old, new) in bmchanges:
2005 2000 args = tr.hookargs.copy()
2006 2001 args.update(bookmarks.preparehookargs(name, old, new))
2007 2002 repo.hook('txnclose-bookmark', throw=False,
2008 2003 **pycompat.strkwargs(args))
2009 2004
2010 2005 if hook.hashook(repo.ui, 'txnclose-phase'):
2011 2006 cl = repo.unfiltered().changelog
2012 2007 phasemv = sorted(tr.changes['phases'].items())
2013 2008 for rev, (old, new) in phasemv:
2014 2009 args = tr.hookargs.copy()
2015 2010 node = hex(cl.node(rev))
2016 2011 args.update(phases.preparehookargs(node, old, new))
2017 2012 repo.hook('txnclose-phase', throw=False,
2018 2013 **pycompat.strkwargs(args))
2019 2014
2020 2015 repo.hook('txnclose', throw=False,
2021 2016 **pycompat.strkwargs(hookargs))
2022 2017 reporef()._afterlock(hookfunc)
2023 2018 tr.addfinalize('txnclose-hook', txnclosehook)
2024 2019 # Include a leading "-" to make it happen before the transaction summary
2025 2020 # reports registered via scmutil.registersummarycallback() whose names
2026 2021 # are 00-txnreport etc. That way, the caches will be warm when the
2027 2022 # callbacks run.
2028 2023 tr.addpostclose('-warm-cache', self._buildcacheupdater(tr))
2029 2024 def txnaborthook(tr2):
2030 2025 """To be run if transaction is aborted
2031 2026 """
2032 2027 reporef().hook('txnabort', throw=False,
2033 2028 **pycompat.strkwargs(tr2.hookargs))
2034 2029 tr.addabort('txnabort-hook', txnaborthook)
2035 2030 # avoid eager cache invalidation. in-memory data should be identical
2036 2031 # to stored data if transaction has no error.
2037 2032 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
2038 2033 self._transref = weakref.ref(tr)
2039 2034 scmutil.registersummarycallback(self, tr, desc)
2040 2035 return tr
2041 2036
2042 2037 def _journalfiles(self):
2043 2038 return ((self.svfs, 'journal'),
2044 2039 (self.svfs, 'journal.narrowspec'),
2045 2040 (self.vfs, 'journal.narrowspec.dirstate'),
2046 2041 (self.vfs, 'journal.dirstate'),
2047 2042 (self.vfs, 'journal.branch'),
2048 2043 (self.vfs, 'journal.desc'),
2049 2044 (bookmarks.bookmarksvfs(self), 'journal.bookmarks'),
2050 2045 (self.svfs, 'journal.phaseroots'))
2051 2046
2052 2047 def undofiles(self):
2053 2048 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
2054 2049
2055 2050 @unfilteredmethod
2056 2051 def _writejournal(self, desc):
2057 2052 self.dirstate.savebackup(None, 'journal.dirstate')
2058 2053 narrowspec.savewcbackup(self, 'journal.narrowspec.dirstate')
2059 2054 narrowspec.savebackup(self, 'journal.narrowspec')
2060 2055 self.vfs.write("journal.branch",
2061 2056 encoding.fromlocal(self.dirstate.branch()))
2062 2057 self.vfs.write("journal.desc",
2063 2058 "%d\n%s\n" % (len(self), desc))
2064 2059 bookmarksvfs = bookmarks.bookmarksvfs(self)
2065 2060 bookmarksvfs.write("journal.bookmarks",
2066 2061 bookmarksvfs.tryread("bookmarks"))
2067 2062 self.svfs.write("journal.phaseroots",
2068 2063 self.svfs.tryread("phaseroots"))
2069 2064
2070 2065 def recover(self):
2071 2066 with self.lock():
2072 2067 if self.svfs.exists("journal"):
2073 2068 self.ui.status(_("rolling back interrupted transaction\n"))
2074 2069 vfsmap = {'': self.svfs,
2075 2070 'plain': self.vfs,}
2076 2071 transaction.rollback(self.svfs, vfsmap, "journal",
2077 2072 self.ui.warn,
2078 2073 checkambigfiles=_cachedfiles)
2079 2074 self.invalidate()
2080 2075 return True
2081 2076 else:
2082 2077 self.ui.warn(_("no interrupted transaction available\n"))
2083 2078 return False
2084 2079
2085 2080 def rollback(self, dryrun=False, force=False):
2086 2081 wlock = lock = dsguard = None
2087 2082 try:
2088 2083 wlock = self.wlock()
2089 2084 lock = self.lock()
2090 2085 if self.svfs.exists("undo"):
2091 2086 dsguard = dirstateguard.dirstateguard(self, 'rollback')
2092 2087
2093 2088 return self._rollback(dryrun, force, dsguard)
2094 2089 else:
2095 2090 self.ui.warn(_("no rollback information available\n"))
2096 2091 return 1
2097 2092 finally:
2098 2093 release(dsguard, lock, wlock)
2099 2094
2100 2095 @unfilteredmethod # Until we get smarter cache management
2101 2096 def _rollback(self, dryrun, force, dsguard):
2102 2097 ui = self.ui
2103 2098 try:
2104 2099 args = self.vfs.read('undo.desc').splitlines()
2105 2100 (oldlen, desc, detail) = (int(args[0]), args[1], None)
2106 2101 if len(args) >= 3:
2107 2102 detail = args[2]
2108 2103 oldtip = oldlen - 1
2109 2104
2110 2105 if detail and ui.verbose:
2111 2106 msg = (_('repository tip rolled back to revision %d'
2112 2107 ' (undo %s: %s)\n')
2113 2108 % (oldtip, desc, detail))
2114 2109 else:
2115 2110 msg = (_('repository tip rolled back to revision %d'
2116 2111 ' (undo %s)\n')
2117 2112 % (oldtip, desc))
2118 2113 except IOError:
2119 2114 msg = _('rolling back unknown transaction\n')
2120 2115 desc = None
2121 2116
2122 2117 if not force and self['.'] != self['tip'] and desc == 'commit':
2123 2118 raise error.Abort(
2124 2119 _('rollback of last commit while not checked out '
2125 2120 'may lose data'), hint=_('use -f to force'))
2126 2121
2127 2122 ui.status(msg)
2128 2123 if dryrun:
2129 2124 return 0
2130 2125
2131 2126 parents = self.dirstate.parents()
2132 2127 self.destroying()
2133 2128 vfsmap = {'plain': self.vfs, '': self.svfs}
2134 2129 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn,
2135 2130 checkambigfiles=_cachedfiles)
2136 2131 bookmarksvfs = bookmarks.bookmarksvfs(self)
2137 2132 if bookmarksvfs.exists('undo.bookmarks'):
2138 2133 bookmarksvfs.rename('undo.bookmarks', 'bookmarks', checkambig=True)
2139 2134 if self.svfs.exists('undo.phaseroots'):
2140 2135 self.svfs.rename('undo.phaseroots', 'phaseroots', checkambig=True)
2141 2136 self.invalidate()
2142 2137
2143 2138 parentgone = any(p not in self.changelog.nodemap for p in parents)
2144 2139 if parentgone:
2145 2140 # prevent dirstateguard from overwriting already restored one
2146 2141 dsguard.close()
2147 2142
2148 2143 narrowspec.restorebackup(self, 'undo.narrowspec')
2149 2144 narrowspec.restorewcbackup(self, 'undo.narrowspec.dirstate')
2150 2145 self.dirstate.restorebackup(None, 'undo.dirstate')
2151 2146 try:
2152 2147 branch = self.vfs.read('undo.branch')
2153 2148 self.dirstate.setbranch(encoding.tolocal(branch))
2154 2149 except IOError:
2155 2150 ui.warn(_('named branch could not be reset: '
2156 2151 'current branch is still \'%s\'\n')
2157 2152 % self.dirstate.branch())
2158 2153
2159 2154 parents = tuple([p.rev() for p in self[None].parents()])
2160 2155 if len(parents) > 1:
2161 2156 ui.status(_('working directory now based on '
2162 2157 'revisions %d and %d\n') % parents)
2163 2158 else:
2164 2159 ui.status(_('working directory now based on '
2165 2160 'revision %d\n') % parents)
2166 2161 mergemod.mergestate.clean(self, self['.'].node())
2167 2162
2168 2163 # TODO: if we know which new heads may result from this rollback, pass
2169 2164 # them to destroy(), which will prevent the branchhead cache from being
2170 2165 # invalidated.
2171 2166 self.destroyed()
2172 2167 return 0
2173 2168
2174 2169 def _buildcacheupdater(self, newtransaction):
2175 2170 """called during transaction to build the callback updating cache
2176 2171
2177 2172 Lives on the repository to help extension who might want to augment
2178 2173 this logic. For this purpose, the created transaction is passed to the
2179 2174 method.
2180 2175 """
2181 2176 # we must avoid cyclic reference between repo and transaction.
2182 2177 reporef = weakref.ref(self)
2183 2178 def updater(tr):
2184 2179 repo = reporef()
2185 2180 repo.updatecaches(tr)
2186 2181 return updater
2187 2182
2188 2183 @unfilteredmethod
2189 2184 def updatecaches(self, tr=None, full=False):
2190 2185 """warm appropriate caches
2191 2186
2192 2187 If this function is called after a transaction closed. The transaction
2193 2188 will be available in the 'tr' argument. This can be used to selectively
2194 2189 update caches relevant to the changes in that transaction.
2195 2190
2196 2191 If 'full' is set, make sure all caches the function knows about have
2197 2192 up-to-date data. Even the ones usually loaded more lazily.
2198 2193 """
2199 2194 if tr is not None and tr.hookargs.get('source') == 'strip':
2200 2195 # During strip, many caches are invalid but
2201 2196 # later call to `destroyed` will refresh them.
2202 2197 return
2203 2198
2204 2199 if tr is None or tr.changes['origrepolen'] < len(self):
2205 2200 # accessing the 'ser ved' branchmap should refresh all the others,
2206 2201 self.ui.debug('updating the branch cache\n')
2207 2202 self.filtered('served').branchmap()
2208 2203 self.filtered('served.hidden').branchmap()
2209 2204
2210 2205 if full:
2211 2206 unfi = self.unfiltered()
2212 2207 rbc = unfi.revbranchcache()
2213 2208 for r in unfi.changelog:
2214 2209 rbc.branchinfo(r)
2215 2210 rbc.write()
2216 2211
2217 2212 # ensure the working copy parents are in the manifestfulltextcache
2218 2213 for ctx in self['.'].parents():
2219 2214 ctx.manifest() # accessing the manifest is enough
2220 2215
2221 2216 # accessing fnode cache warms the cache
2222 2217 tagsmod.fnoderevs(self.ui, unfi, unfi.changelog.revs())
2223 2218 # accessing tags warm the cache
2224 2219 self.tags()
2225 2220 self.filtered('served').tags()
2226 2221
2227 2222 # The `full` arg is documented as updating even the lazily-loaded
2228 2223 # caches immediately, so we're forcing a write to cause these caches
2229 2224 # to be warmed up even if they haven't explicitly been requested
2230 2225 # yet (if they've never been used by hg, they won't ever have been
2231 2226 # written, even if they're a subset of another kind of cache that
2232 2227 # *has* been used).
2233 2228 for filt in repoview.filtertable.keys():
2234 2229 filtered = self.filtered(filt)
2235 2230 filtered.branchmap().write(filtered)
2236 2231
2237 2232 def invalidatecaches(self):
2238 2233
2239 2234 if r'_tagscache' in vars(self):
2240 2235 # can't use delattr on proxy
2241 2236 del self.__dict__[r'_tagscache']
2242 2237
2243 2238 self._branchcaches.clear()
2244 2239 self.invalidatevolatilesets()
2245 2240 self._sparsesignaturecache.clear()
2246 2241
2247 2242 def invalidatevolatilesets(self):
2248 2243 self.filteredrevcache.clear()
2249 2244 obsolete.clearobscaches(self)
2250 2245
2251 2246 def invalidatedirstate(self):
2252 2247 '''Invalidates the dirstate, causing the next call to dirstate
2253 2248 to check if it was modified since the last time it was read,
2254 2249 rereading it if it has.
2255 2250
2256 2251 This is different to dirstate.invalidate() that it doesn't always
2257 2252 rereads the dirstate. Use dirstate.invalidate() if you want to
2258 2253 explicitly read the dirstate again (i.e. restoring it to a previous
2259 2254 known good state).'''
2260 2255 if hasunfilteredcache(self, r'dirstate'):
2261 2256 for k in self.dirstate._filecache:
2262 2257 try:
2263 2258 delattr(self.dirstate, k)
2264 2259 except AttributeError:
2265 2260 pass
2266 2261 delattr(self.unfiltered(), r'dirstate')
2267 2262
2268 2263 def invalidate(self, clearfilecache=False):
2269 2264 '''Invalidates both store and non-store parts other than dirstate
2270 2265
2271 2266 If a transaction is running, invalidation of store is omitted,
2272 2267 because discarding in-memory changes might cause inconsistency
2273 2268 (e.g. incomplete fncache causes unintentional failure, but
2274 2269 redundant one doesn't).
2275 2270 '''
2276 2271 unfiltered = self.unfiltered() # all file caches are stored unfiltered
2277 2272 for k in list(self._filecache.keys()):
2278 2273 # dirstate is invalidated separately in invalidatedirstate()
2279 2274 if k == 'dirstate':
2280 2275 continue
2281 2276 if (k == 'changelog' and
2282 2277 self.currenttransaction() and
2283 2278 self.changelog._delayed):
2284 2279 # The changelog object may store unwritten revisions. We don't
2285 2280 # want to lose them.
2286 2281 # TODO: Solve the problem instead of working around it.
2287 2282 continue
2288 2283
2289 2284 if clearfilecache:
2290 2285 del self._filecache[k]
2291 2286 try:
2292 2287 delattr(unfiltered, k)
2293 2288 except AttributeError:
2294 2289 pass
2295 2290 self.invalidatecaches()
2296 2291 if not self.currenttransaction():
2297 2292 # TODO: Changing contents of store outside transaction
2298 2293 # causes inconsistency. We should make in-memory store
2299 2294 # changes detectable, and abort if changed.
2300 2295 self.store.invalidatecaches()
2301 2296
2302 2297 def invalidateall(self):
2303 2298 '''Fully invalidates both store and non-store parts, causing the
2304 2299 subsequent operation to reread any outside changes.'''
2305 2300 # extension should hook this to invalidate its caches
2306 2301 self.invalidate()
2307 2302 self.invalidatedirstate()
2308 2303
2309 2304 @unfilteredmethod
2310 2305 def _refreshfilecachestats(self, tr):
2311 2306 """Reload stats of cached files so that they are flagged as valid"""
2312 2307 for k, ce in self._filecache.items():
2313 2308 k = pycompat.sysstr(k)
2314 2309 if k == r'dirstate' or k not in self.__dict__:
2315 2310 continue
2316 2311 ce.refresh()
2317 2312
2318 2313 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
2319 2314 inheritchecker=None, parentenvvar=None):
2320 2315 parentlock = None
2321 2316 # the contents of parentenvvar are used by the underlying lock to
2322 2317 # determine whether it can be inherited
2323 2318 if parentenvvar is not None:
2324 2319 parentlock = encoding.environ.get(parentenvvar)
2325 2320
2326 2321 timeout = 0
2327 2322 warntimeout = 0
2328 2323 if wait:
2329 2324 timeout = self.ui.configint("ui", "timeout")
2330 2325 warntimeout = self.ui.configint("ui", "timeout.warn")
2331 2326 # internal config: ui.signal-safe-lock
2332 2327 signalsafe = self.ui.configbool('ui', 'signal-safe-lock')
2333 2328
2334 2329 l = lockmod.trylock(self.ui, vfs, lockname, timeout, warntimeout,
2335 2330 releasefn=releasefn,
2336 2331 acquirefn=acquirefn, desc=desc,
2337 2332 inheritchecker=inheritchecker,
2338 2333 parentlock=parentlock,
2339 2334 signalsafe=signalsafe)
2340 2335 return l
2341 2336
2342 2337 def _afterlock(self, callback):
2343 2338 """add a callback to be run when the repository is fully unlocked
2344 2339
2345 2340 The callback will be executed when the outermost lock is released
2346 2341 (with wlock being higher level than 'lock')."""
2347 2342 for ref in (self._wlockref, self._lockref):
2348 2343 l = ref and ref()
2349 2344 if l and l.held:
2350 2345 l.postrelease.append(callback)
2351 2346 break
2352 2347 else: # no lock have been found.
2353 2348 callback()
2354 2349
2355 2350 def lock(self, wait=True):
2356 2351 '''Lock the repository store (.hg/store) and return a weak reference
2357 2352 to the lock. Use this before modifying the store (e.g. committing or
2358 2353 stripping). If you are opening a transaction, get a lock as well.)
2359 2354
2360 2355 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2361 2356 'wlock' first to avoid a dead-lock hazard.'''
2362 2357 l = self._currentlock(self._lockref)
2363 2358 if l is not None:
2364 2359 l.lock()
2365 2360 return l
2366 2361
2367 2362 l = self._lock(vfs=self.svfs,
2368 2363 lockname="lock",
2369 2364 wait=wait,
2370 2365 releasefn=None,
2371 2366 acquirefn=self.invalidate,
2372 2367 desc=_('repository %s') % self.origroot)
2373 2368 self._lockref = weakref.ref(l)
2374 2369 return l
2375 2370
2376 2371 def _wlockchecktransaction(self):
2377 2372 if self.currenttransaction() is not None:
2378 2373 raise error.LockInheritanceContractViolation(
2379 2374 'wlock cannot be inherited in the middle of a transaction')
2380 2375
2381 2376 def wlock(self, wait=True):
2382 2377 '''Lock the non-store parts of the repository (everything under
2383 2378 .hg except .hg/store) and return a weak reference to the lock.
2384 2379
2385 2380 Use this before modifying files in .hg.
2386 2381
2387 2382 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2388 2383 'wlock' first to avoid a dead-lock hazard.'''
2389 2384 l = self._wlockref and self._wlockref()
2390 2385 if l is not None and l.held:
2391 2386 l.lock()
2392 2387 return l
2393 2388
2394 2389 # We do not need to check for non-waiting lock acquisition. Such
2395 2390 # acquisition would not cause dead-lock as they would just fail.
2396 2391 if wait and (self.ui.configbool('devel', 'all-warnings')
2397 2392 or self.ui.configbool('devel', 'check-locks')):
2398 2393 if self._currentlock(self._lockref) is not None:
2399 2394 self.ui.develwarn('"wlock" acquired after "lock"')
2400 2395
2401 2396 def unlock():
2402 2397 if self.dirstate.pendingparentchange():
2403 2398 self.dirstate.invalidate()
2404 2399 else:
2405 2400 self.dirstate.write(None)
2406 2401
2407 2402 self._filecache['dirstate'].refresh()
2408 2403
2409 2404 l = self._lock(self.vfs, "wlock", wait, unlock,
2410 2405 self.invalidatedirstate, _('working directory of %s') %
2411 2406 self.origroot,
2412 2407 inheritchecker=self._wlockchecktransaction,
2413 2408 parentenvvar='HG_WLOCK_LOCKER')
2414 2409 self._wlockref = weakref.ref(l)
2415 2410 return l
2416 2411
2417 2412 def _currentlock(self, lockref):
2418 2413 """Returns the lock if it's held, or None if it's not."""
2419 2414 if lockref is None:
2420 2415 return None
2421 2416 l = lockref()
2422 2417 if l is None or not l.held:
2423 2418 return None
2424 2419 return l
2425 2420
2426 2421 def currentwlock(self):
2427 2422 """Returns the wlock if it's held, or None if it's not."""
2428 2423 return self._currentlock(self._wlockref)
2429 2424
2430 2425 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist,
2431 2426 includecopymeta):
2432 2427 """
2433 2428 commit an individual file as part of a larger transaction
2434 2429 """
2435 2430
2436 2431 fname = fctx.path()
2437 2432 fparent1 = manifest1.get(fname, nullid)
2438 2433 fparent2 = manifest2.get(fname, nullid)
2439 2434 if isinstance(fctx, context.filectx):
2440 2435 node = fctx.filenode()
2441 2436 if node in [fparent1, fparent2]:
2442 2437 self.ui.debug('reusing %s filelog entry\n' % fname)
2443 2438 if ((fparent1 != nullid and
2444 2439 manifest1.flags(fname) != fctx.flags()) or
2445 2440 (fparent2 != nullid and
2446 2441 manifest2.flags(fname) != fctx.flags())):
2447 2442 changelist.append(fname)
2448 2443 return node
2449 2444
2450 2445 flog = self.file(fname)
2451 2446 meta = {}
2452 2447 cfname = fctx.copysource()
2453 2448 if cfname and cfname != fname:
2454 2449 # Mark the new revision of this file as a copy of another
2455 2450 # file. This copy data will effectively act as a parent
2456 2451 # of this new revision. If this is a merge, the first
2457 2452 # parent will be the nullid (meaning "look up the copy data")
2458 2453 # and the second one will be the other parent. For example:
2459 2454 #
2460 2455 # 0 --- 1 --- 3 rev1 changes file foo
2461 2456 # \ / rev2 renames foo to bar and changes it
2462 2457 # \- 2 -/ rev3 should have bar with all changes and
2463 2458 # should record that bar descends from
2464 2459 # bar in rev2 and foo in rev1
2465 2460 #
2466 2461 # this allows this merge to succeed:
2467 2462 #
2468 2463 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
2469 2464 # \ / merging rev3 and rev4 should use bar@rev2
2470 2465 # \- 2 --- 4 as the merge base
2471 2466 #
2472 2467
2473 2468 cnode = manifest1.get(cfname)
2474 2469 newfparent = fparent2
2475 2470
2476 2471 if manifest2: # branch merge
2477 2472 if fparent2 == nullid or cnode is None: # copied on remote side
2478 2473 if cfname in manifest2:
2479 2474 cnode = manifest2[cfname]
2480 2475 newfparent = fparent1
2481 2476
2482 2477 # Here, we used to search backwards through history to try to find
2483 2478 # where the file copy came from if the source of a copy was not in
2484 2479 # the parent directory. However, this doesn't actually make sense to
2485 2480 # do (what does a copy from something not in your working copy even
2486 2481 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
2487 2482 # the user that copy information was dropped, so if they didn't
2488 2483 # expect this outcome it can be fixed, but this is the correct
2489 2484 # behavior in this circumstance.
2490 2485
2491 2486 if cnode:
2492 2487 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(cnode)))
2493 2488 if includecopymeta:
2494 2489 meta["copy"] = cfname
2495 2490 meta["copyrev"] = hex(cnode)
2496 2491 fparent1, fparent2 = nullid, newfparent
2497 2492 else:
2498 2493 self.ui.warn(_("warning: can't find ancestor for '%s' "
2499 2494 "copied from '%s'!\n") % (fname, cfname))
2500 2495
2501 2496 elif fparent1 == nullid:
2502 2497 fparent1, fparent2 = fparent2, nullid
2503 2498 elif fparent2 != nullid:
2504 2499 # is one parent an ancestor of the other?
2505 2500 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
2506 2501 if fparent1 in fparentancestors:
2507 2502 fparent1, fparent2 = fparent2, nullid
2508 2503 elif fparent2 in fparentancestors:
2509 2504 fparent2 = nullid
2510 2505
2511 2506 # is the file changed?
2512 2507 text = fctx.data()
2513 2508 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
2514 2509 changelist.append(fname)
2515 2510 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
2516 2511 # are just the flags changed during merge?
2517 2512 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
2518 2513 changelist.append(fname)
2519 2514
2520 2515 return fparent1
2521 2516
2522 2517 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
2523 2518 """check for commit arguments that aren't committable"""
2524 2519 if match.isexact() or match.prefix():
2525 2520 matched = set(status.modified + status.added + status.removed)
2526 2521
2527 2522 for f in match.files():
2528 2523 f = self.dirstate.normalize(f)
2529 2524 if f == '.' or f in matched or f in wctx.substate:
2530 2525 continue
2531 2526 if f in status.deleted:
2532 2527 fail(f, _('file not found!'))
2533 2528 if f in vdirs: # visited directory
2534 2529 d = f + '/'
2535 2530 for mf in matched:
2536 2531 if mf.startswith(d):
2537 2532 break
2538 2533 else:
2539 2534 fail(f, _("no match under directory!"))
2540 2535 elif f not in self.dirstate:
2541 2536 fail(f, _("file not tracked!"))
2542 2537
2543 2538 @unfilteredmethod
2544 2539 def commit(self, text="", user=None, date=None, match=None, force=False,
2545 2540 editor=False, extra=None):
2546 2541 """Add a new revision to current repository.
2547 2542
2548 2543 Revision information is gathered from the working directory,
2549 2544 match can be used to filter the committed files. If editor is
2550 2545 supplied, it is called to get a commit message.
2551 2546 """
2552 2547 if extra is None:
2553 2548 extra = {}
2554 2549
2555 2550 def fail(f, msg):
2556 2551 raise error.Abort('%s: %s' % (f, msg))
2557 2552
2558 2553 if not match:
2559 2554 match = matchmod.always()
2560 2555
2561 2556 if not force:
2562 2557 vdirs = []
2563 2558 match.explicitdir = vdirs.append
2564 2559 match.bad = fail
2565 2560
2566 2561 # lock() for recent changelog (see issue4368)
2567 2562 with self.wlock(), self.lock():
2568 2563 wctx = self[None]
2569 2564 merge = len(wctx.parents()) > 1
2570 2565
2571 2566 if not force and merge and not match.always():
2572 2567 raise error.Abort(_('cannot partially commit a merge '
2573 2568 '(do not specify files or patterns)'))
2574 2569
2575 2570 status = self.status(match=match, clean=force)
2576 2571 if force:
2577 2572 status.modified.extend(status.clean) # mq may commit clean files
2578 2573
2579 2574 # check subrepos
2580 2575 subs, commitsubs, newstate = subrepoutil.precommit(
2581 2576 self.ui, wctx, status, match, force=force)
2582 2577
2583 2578 # make sure all explicit patterns are matched
2584 2579 if not force:
2585 2580 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
2586 2581
2587 2582 cctx = context.workingcommitctx(self, status,
2588 2583 text, user, date, extra)
2589 2584
2590 2585 # internal config: ui.allowemptycommit
2591 2586 allowemptycommit = (wctx.branch() != wctx.p1().branch()
2592 2587 or extra.get('close') or merge or cctx.files()
2593 2588 or self.ui.configbool('ui', 'allowemptycommit'))
2594 2589 if not allowemptycommit:
2595 2590 return None
2596 2591
2597 2592 if merge and cctx.deleted():
2598 2593 raise error.Abort(_("cannot commit merge with missing files"))
2599 2594
2600 2595 ms = mergemod.mergestate.read(self)
2601 2596 mergeutil.checkunresolved(ms)
2602 2597
2603 2598 if editor:
2604 2599 cctx._text = editor(self, cctx, subs)
2605 2600 edited = (text != cctx._text)
2606 2601
2607 2602 # Save commit message in case this transaction gets rolled back
2608 2603 # (e.g. by a pretxncommit hook). Leave the content alone on
2609 2604 # the assumption that the user will use the same editor again.
2610 2605 msgfn = self.savecommitmessage(cctx._text)
2611 2606
2612 2607 # commit subs and write new state
2613 2608 if subs:
2614 2609 uipathfn = scmutil.getuipathfn(self)
2615 2610 for s in sorted(commitsubs):
2616 2611 sub = wctx.sub(s)
2617 2612 self.ui.status(_('committing subrepository %s\n') %
2618 2613 uipathfn(subrepoutil.subrelpath(sub)))
2619 2614 sr = sub.commit(cctx._text, user, date)
2620 2615 newstate[s] = (newstate[s][0], sr)
2621 2616 subrepoutil.writestate(self, newstate)
2622 2617
2623 2618 p1, p2 = self.dirstate.parents()
2624 2619 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
2625 2620 try:
2626 2621 self.hook("precommit", throw=True, parent1=hookp1,
2627 2622 parent2=hookp2)
2628 2623 with self.transaction('commit'):
2629 2624 ret = self.commitctx(cctx, True)
2630 2625 # update bookmarks, dirstate and mergestate
2631 2626 bookmarks.update(self, [p1, p2], ret)
2632 2627 cctx.markcommitted(ret)
2633 2628 ms.reset()
2634 2629 except: # re-raises
2635 2630 if edited:
2636 2631 self.ui.write(
2637 2632 _('note: commit message saved in %s\n') % msgfn)
2638 2633 raise
2639 2634
2640 2635 def commithook():
2641 2636 # hack for command that use a temporary commit (eg: histedit)
2642 2637 # temporary commit got stripped before hook release
2643 2638 if self.changelog.hasnode(ret):
2644 2639 self.hook("commit", node=hex(ret), parent1=hookp1,
2645 2640 parent2=hookp2)
2646 2641 self._afterlock(commithook)
2647 2642 return ret
2648 2643
2649 2644 @unfilteredmethod
2650 2645 def commitctx(self, ctx, error=False, origctx=None):
2651 2646 """Add a new revision to current repository.
2652 2647 Revision information is passed via the context argument.
2653 2648
2654 2649 ctx.files() should list all files involved in this commit, i.e.
2655 2650 modified/added/removed files. On merge, it may be wider than the
2656 2651 ctx.files() to be committed, since any file nodes derived directly
2657 2652 from p1 or p2 are excluded from the committed ctx.files().
2658 2653
2659 2654 origctx is for convert to work around the problem that bug
2660 2655 fixes to the files list in changesets change hashes. For
2661 2656 convert to be the identity, it can pass an origctx and this
2662 2657 function will use the same files list when it makes sense to
2663 2658 do so.
2664 2659 """
2665 2660
2666 2661 p1, p2 = ctx.p1(), ctx.p2()
2667 2662 user = ctx.user()
2668 2663
2669 2664 writecopiesto = self.ui.config('experimental', 'copies.write-to')
2670 2665 writefilecopymeta = writecopiesto != 'changeset-only'
2671 2666 writechangesetcopy = (writecopiesto in
2672 2667 ('changeset-only', 'compatibility'))
2673 2668 p1copies, p2copies = None, None
2674 2669 if writechangesetcopy:
2675 2670 p1copies = ctx.p1copies()
2676 2671 p2copies = ctx.p2copies()
2677 2672 filesadded, filesremoved = None, None
2678 2673 with self.lock(), self.transaction("commit") as tr:
2679 2674 trp = weakref.proxy(tr)
2680 2675
2681 2676 if ctx.manifestnode():
2682 2677 # reuse an existing manifest revision
2683 2678 self.ui.debug('reusing known manifest\n')
2684 2679 mn = ctx.manifestnode()
2685 2680 files = ctx.files()
2686 2681 if writechangesetcopy:
2687 2682 filesadded = ctx.filesadded()
2688 2683 filesremoved = ctx.filesremoved()
2689 2684 elif ctx.files():
2690 2685 m1ctx = p1.manifestctx()
2691 2686 m2ctx = p2.manifestctx()
2692 2687 mctx = m1ctx.copy()
2693 2688
2694 2689 m = mctx.read()
2695 2690 m1 = m1ctx.read()
2696 2691 m2 = m2ctx.read()
2697 2692
2698 2693 # check in files
2699 2694 added = []
2700 2695 changed = []
2701 2696 removed = list(ctx.removed())
2702 2697 linkrev = len(self)
2703 2698 self.ui.note(_("committing files:\n"))
2704 2699 uipathfn = scmutil.getuipathfn(self)
2705 2700 for f in sorted(ctx.modified() + ctx.added()):
2706 2701 self.ui.note(uipathfn(f) + "\n")
2707 2702 try:
2708 2703 fctx = ctx[f]
2709 2704 if fctx is None:
2710 2705 removed.append(f)
2711 2706 else:
2712 2707 added.append(f)
2713 2708 m[f] = self._filecommit(fctx, m1, m2, linkrev,
2714 2709 trp, changed,
2715 2710 writefilecopymeta)
2716 2711 m.setflag(f, fctx.flags())
2717 2712 except OSError:
2718 2713 self.ui.warn(_("trouble committing %s!\n") %
2719 2714 uipathfn(f))
2720 2715 raise
2721 2716 except IOError as inst:
2722 2717 errcode = getattr(inst, 'errno', errno.ENOENT)
2723 2718 if error or errcode and errcode != errno.ENOENT:
2724 2719 self.ui.warn(_("trouble committing %s!\n") %
2725 2720 uipathfn(f))
2726 2721 raise
2727 2722
2728 2723 # update manifest
2729 2724 removed = [f for f in removed if f in m1 or f in m2]
2730 2725 drop = sorted([f for f in removed if f in m])
2731 2726 for f in drop:
2732 2727 del m[f]
2733 2728 if p2.rev() != nullrev:
2734 2729 @util.cachefunc
2735 2730 def mas():
2736 2731 p1n = p1.node()
2737 2732 p2n = p2.node()
2738 2733 cahs = self.changelog.commonancestorsheads(p1n, p2n)
2739 2734 if not cahs:
2740 2735 cahs = [nullrev]
2741 2736 return [self[r].manifest() for r in cahs]
2742 2737 def deletionfromparent(f):
2743 2738 # When a file is removed relative to p1 in a merge, this
2744 2739 # function determines whether the absence is due to a
2745 2740 # deletion from a parent, or whether the merge commit
2746 2741 # itself deletes the file. We decide this by doing a
2747 2742 # simplified three way merge of the manifest entry for
2748 2743 # the file. There are two ways we decide the merge
2749 2744 # itself didn't delete a file:
2750 2745 # - neither parent (nor the merge) contain the file
2751 2746 # - exactly one parent contains the file, and that
2752 2747 # parent has the same filelog entry as the merge
2753 2748 # ancestor (or all of them if there two). In other
2754 2749 # words, that parent left the file unchanged while the
2755 2750 # other one deleted it.
2756 2751 # One way to think about this is that deleting a file is
2757 2752 # similar to emptying it, so the list of changed files
2758 2753 # should be similar either way. The computation
2759 2754 # described above is not done directly in _filecommit
2760 2755 # when creating the list of changed files, however
2761 2756 # it does something very similar by comparing filelog
2762 2757 # nodes.
2763 2758 if f in m1:
2764 2759 return (f not in m2
2765 2760 and all(f in ma and ma.find(f) == m1.find(f)
2766 2761 for ma in mas()))
2767 2762 elif f in m2:
2768 2763 return all(f in ma and ma.find(f) == m2.find(f)
2769 2764 for ma in mas())
2770 2765 else:
2771 2766 return True
2772 2767 removed = [f for f in removed if not deletionfromparent(f)]
2773 2768
2774 2769 files = changed + removed
2775 2770 md = None
2776 2771 if not files:
2777 2772 # if no "files" actually changed in terms of the changelog,
2778 2773 # try hard to detect unmodified manifest entry so that the
2779 2774 # exact same commit can be reproduced later on convert.
2780 2775 md = m1.diff(m, scmutil.matchfiles(self, ctx.files()))
2781 2776 if not files and md:
2782 2777 self.ui.debug('not reusing manifest (no file change in '
2783 2778 'changelog, but manifest differs)\n')
2784 2779 if files or md:
2785 2780 self.ui.note(_("committing manifest\n"))
2786 2781 # we're using narrowmatch here since it's already applied at
2787 2782 # other stages (such as dirstate.walk), so we're already
2788 2783 # ignoring things outside of narrowspec in most cases. The
2789 2784 # one case where we might have files outside the narrowspec
2790 2785 # at this point is merges, and we already error out in the
2791 2786 # case where the merge has files outside of the narrowspec,
2792 2787 # so this is safe.
2793 2788 mn = mctx.write(trp, linkrev,
2794 2789 p1.manifestnode(), p2.manifestnode(),
2795 2790 added, drop, match=self.narrowmatch())
2796 2791
2797 2792 if writechangesetcopy:
2798 2793 filesadded = [f for f in changed
2799 2794 if not (f in m1 or f in m2)]
2800 2795 filesremoved = removed
2801 2796 else:
2802 2797 self.ui.debug('reusing manifest from p1 (listed files '
2803 2798 'actually unchanged)\n')
2804 2799 mn = p1.manifestnode()
2805 2800 else:
2806 2801 self.ui.debug('reusing manifest from p1 (no file change)\n')
2807 2802 mn = p1.manifestnode()
2808 2803 files = []
2809 2804
2810 2805 if writecopiesto == 'changeset-only':
2811 2806 # If writing only to changeset extras, use None to indicate that
2812 2807 # no entry should be written. If writing to both, write an empty
2813 2808 # entry to prevent the reader from falling back to reading
2814 2809 # filelogs.
2815 2810 p1copies = p1copies or None
2816 2811 p2copies = p2copies or None
2817 2812 filesadded = filesadded or None
2818 2813 filesremoved = filesremoved or None
2819 2814
2820 2815 if origctx and origctx.manifestnode() == mn:
2821 2816 files = origctx.files()
2822 2817
2823 2818 # update changelog
2824 2819 self.ui.note(_("committing changelog\n"))
2825 2820 self.changelog.delayupdate(tr)
2826 2821 n = self.changelog.add(mn, files, ctx.description(),
2827 2822 trp, p1.node(), p2.node(),
2828 2823 user, ctx.date(), ctx.extra().copy(),
2829 2824 p1copies, p2copies, filesadded, filesremoved)
2830 2825 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
2831 2826 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
2832 2827 parent2=xp2)
2833 2828 # set the new commit is proper phase
2834 2829 targetphase = subrepoutil.newcommitphase(self.ui, ctx)
2835 2830 if targetphase:
2836 2831 # retract boundary do not alter parent changeset.
2837 2832 # if a parent have higher the resulting phase will
2838 2833 # be compliant anyway
2839 2834 #
2840 2835 # if minimal phase was 0 we don't need to retract anything
2841 2836 phases.registernew(self, tr, targetphase, [n])
2842 2837 return n
2843 2838
2844 2839 @unfilteredmethod
2845 2840 def destroying(self):
2846 2841 '''Inform the repository that nodes are about to be destroyed.
2847 2842 Intended for use by strip and rollback, so there's a common
2848 2843 place for anything that has to be done before destroying history.
2849 2844
2850 2845 This is mostly useful for saving state that is in memory and waiting
2851 2846 to be flushed when the current lock is released. Because a call to
2852 2847 destroyed is imminent, the repo will be invalidated causing those
2853 2848 changes to stay in memory (waiting for the next unlock), or vanish
2854 2849 completely.
2855 2850 '''
2856 2851 # When using the same lock to commit and strip, the phasecache is left
2857 2852 # dirty after committing. Then when we strip, the repo is invalidated,
2858 2853 # causing those changes to disappear.
2859 2854 if '_phasecache' in vars(self):
2860 2855 self._phasecache.write()
2861 2856
2862 2857 @unfilteredmethod
2863 2858 def destroyed(self):
2864 2859 '''Inform the repository that nodes have been destroyed.
2865 2860 Intended for use by strip and rollback, so there's a common
2866 2861 place for anything that has to be done after destroying history.
2867 2862 '''
2868 2863 # When one tries to:
2869 2864 # 1) destroy nodes thus calling this method (e.g. strip)
2870 2865 # 2) use phasecache somewhere (e.g. commit)
2871 2866 #
2872 2867 # then 2) will fail because the phasecache contains nodes that were
2873 2868 # removed. We can either remove phasecache from the filecache,
2874 2869 # causing it to reload next time it is accessed, or simply filter
2875 2870 # the removed nodes now and write the updated cache.
2876 2871 self._phasecache.filterunknown(self)
2877 2872 self._phasecache.write()
2878 2873
2879 2874 # refresh all repository caches
2880 2875 self.updatecaches()
2881 2876
2882 2877 # Ensure the persistent tag cache is updated. Doing it now
2883 2878 # means that the tag cache only has to worry about destroyed
2884 2879 # heads immediately after a strip/rollback. That in turn
2885 2880 # guarantees that "cachetip == currenttip" (comparing both rev
2886 2881 # and node) always means no nodes have been added or destroyed.
2887 2882
2888 2883 # XXX this is suboptimal when qrefresh'ing: we strip the current
2889 2884 # head, refresh the tag cache, then immediately add a new head.
2890 2885 # But I think doing it this way is necessary for the "instant
2891 2886 # tag cache retrieval" case to work.
2892 2887 self.invalidate()
2893 2888
2894 2889 def status(self, node1='.', node2=None, match=None,
2895 2890 ignored=False, clean=False, unknown=False,
2896 2891 listsubrepos=False):
2897 2892 '''a convenience method that calls node1.status(node2)'''
2898 2893 return self[node1].status(node2, match, ignored, clean, unknown,
2899 2894 listsubrepos)
2900 2895
2901 2896 def addpostdsstatus(self, ps):
2902 2897 """Add a callback to run within the wlock, at the point at which status
2903 2898 fixups happen.
2904 2899
2905 2900 On status completion, callback(wctx, status) will be called with the
2906 2901 wlock held, unless the dirstate has changed from underneath or the wlock
2907 2902 couldn't be grabbed.
2908 2903
2909 2904 Callbacks should not capture and use a cached copy of the dirstate --
2910 2905 it might change in the meanwhile. Instead, they should access the
2911 2906 dirstate via wctx.repo().dirstate.
2912 2907
2913 2908 This list is emptied out after each status run -- extensions should
2914 2909 make sure it adds to this list each time dirstate.status is called.
2915 2910 Extensions should also make sure they don't call this for statuses
2916 2911 that don't involve the dirstate.
2917 2912 """
2918 2913
2919 2914 # The list is located here for uniqueness reasons -- it is actually
2920 2915 # managed by the workingctx, but that isn't unique per-repo.
2921 2916 self._postdsstatus.append(ps)
2922 2917
2923 2918 def postdsstatus(self):
2924 2919 """Used by workingctx to get the list of post-dirstate-status hooks."""
2925 2920 return self._postdsstatus
2926 2921
2927 2922 def clearpostdsstatus(self):
2928 2923 """Used by workingctx to clear post-dirstate-status hooks."""
2929 2924 del self._postdsstatus[:]
2930 2925
2931 2926 def heads(self, start=None):
2932 2927 if start is None:
2933 2928 cl = self.changelog
2934 2929 headrevs = reversed(cl.headrevs())
2935 2930 return [cl.node(rev) for rev in headrevs]
2936 2931
2937 2932 heads = self.changelog.heads(start)
2938 2933 # sort the output in rev descending order
2939 2934 return sorted(heads, key=self.changelog.rev, reverse=True)
2940 2935
2941 2936 def branchheads(self, branch=None, start=None, closed=False):
2942 2937 '''return a (possibly filtered) list of heads for the given branch
2943 2938
2944 2939 Heads are returned in topological order, from newest to oldest.
2945 2940 If branch is None, use the dirstate branch.
2946 2941 If start is not None, return only heads reachable from start.
2947 2942 If closed is True, return heads that are marked as closed as well.
2948 2943 '''
2949 2944 if branch is None:
2950 2945 branch = self[None].branch()
2951 2946 branches = self.branchmap()
2952 2947 if not branches.hasbranch(branch):
2953 2948 return []
2954 2949 # the cache returns heads ordered lowest to highest
2955 2950 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
2956 2951 if start is not None:
2957 2952 # filter out the heads that cannot be reached from startrev
2958 2953 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
2959 2954 bheads = [h for h in bheads if h in fbheads]
2960 2955 return bheads
2961 2956
2962 2957 def branches(self, nodes):
2963 2958 if not nodes:
2964 2959 nodes = [self.changelog.tip()]
2965 2960 b = []
2966 2961 for n in nodes:
2967 2962 t = n
2968 2963 while True:
2969 2964 p = self.changelog.parents(n)
2970 2965 if p[1] != nullid or p[0] == nullid:
2971 2966 b.append((t, n, p[0], p[1]))
2972 2967 break
2973 2968 n = p[0]
2974 2969 return b
2975 2970
2976 2971 def between(self, pairs):
2977 2972 r = []
2978 2973
2979 2974 for top, bottom in pairs:
2980 2975 n, l, i = top, [], 0
2981 2976 f = 1
2982 2977
2983 2978 while n != bottom and n != nullid:
2984 2979 p = self.changelog.parents(n)[0]
2985 2980 if i == f:
2986 2981 l.append(n)
2987 2982 f = f * 2
2988 2983 n = p
2989 2984 i += 1
2990 2985
2991 2986 r.append(l)
2992 2987
2993 2988 return r
2994 2989
2995 2990 def checkpush(self, pushop):
2996 2991 """Extensions can override this function if additional checks have
2997 2992 to be performed before pushing, or call it if they override push
2998 2993 command.
2999 2994 """
3000 2995
3001 2996 @unfilteredpropertycache
3002 2997 def prepushoutgoinghooks(self):
3003 2998 """Return util.hooks consists of a pushop with repo, remote, outgoing
3004 2999 methods, which are called before pushing changesets.
3005 3000 """
3006 3001 return util.hooks()
3007 3002
3008 3003 def pushkey(self, namespace, key, old, new):
3009 3004 try:
3010 3005 tr = self.currenttransaction()
3011 3006 hookargs = {}
3012 3007 if tr is not None:
3013 3008 hookargs.update(tr.hookargs)
3014 3009 hookargs = pycompat.strkwargs(hookargs)
3015 3010 hookargs[r'namespace'] = namespace
3016 3011 hookargs[r'key'] = key
3017 3012 hookargs[r'old'] = old
3018 3013 hookargs[r'new'] = new
3019 3014 self.hook('prepushkey', throw=True, **hookargs)
3020 3015 except error.HookAbort as exc:
3021 3016 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
3022 3017 if exc.hint:
3023 3018 self.ui.write_err(_("(%s)\n") % exc.hint)
3024 3019 return False
3025 3020 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
3026 3021 ret = pushkey.push(self, namespace, key, old, new)
3027 3022 def runhook():
3028 3023 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
3029 3024 ret=ret)
3030 3025 self._afterlock(runhook)
3031 3026 return ret
3032 3027
3033 3028 def listkeys(self, namespace):
3034 3029 self.hook('prelistkeys', throw=True, namespace=namespace)
3035 3030 self.ui.debug('listing keys for "%s"\n' % namespace)
3036 3031 values = pushkey.list(self, namespace)
3037 3032 self.hook('listkeys', namespace=namespace, values=values)
3038 3033 return values
3039 3034
3040 3035 def debugwireargs(self, one, two, three=None, four=None, five=None):
3041 3036 '''used to test argument passing over the wire'''
3042 3037 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
3043 3038 pycompat.bytestr(four),
3044 3039 pycompat.bytestr(five))
3045 3040
3046 3041 def savecommitmessage(self, text):
3047 3042 fp = self.vfs('last-message.txt', 'wb')
3048 3043 try:
3049 3044 fp.write(text)
3050 3045 finally:
3051 3046 fp.close()
3052 3047 return self.pathto(fp.name[len(self.root) + 1:])
3053 3048
3054 3049 # used to avoid circular references so destructors work
3055 3050 def aftertrans(files):
3056 3051 renamefiles = [tuple(t) for t in files]
3057 3052 def a():
3058 3053 for vfs, src, dest in renamefiles:
3059 3054 # if src and dest refer to a same file, vfs.rename is a no-op,
3060 3055 # leaving both src and dest on disk. delete dest to make sure
3061 3056 # the rename couldn't be such a no-op.
3062 3057 vfs.tryunlink(dest)
3063 3058 try:
3064 3059 vfs.rename(src, dest)
3065 3060 except OSError: # journal file does not yet exist
3066 3061 pass
3067 3062 return a
3068 3063
3069 3064 def undoname(fn):
3070 3065 base, name = os.path.split(fn)
3071 3066 assert name.startswith('journal')
3072 3067 return os.path.join(base, name.replace('journal', 'undo', 1))
3073 3068
3074 3069 def instance(ui, path, create, intents=None, createopts=None):
3075 3070 localpath = util.urllocalpath(path)
3076 3071 if create:
3077 3072 createrepository(ui, localpath, createopts=createopts)
3078 3073
3079 3074 return makelocalrepository(ui, localpath, intents=intents)
3080 3075
3081 3076 def islocal(path):
3082 3077 return True
3083 3078
3084 3079 def defaultcreateopts(ui, createopts=None):
3085 3080 """Populate the default creation options for a repository.
3086 3081
3087 3082 A dictionary of explicitly requested creation options can be passed
3088 3083 in. Missing keys will be populated.
3089 3084 """
3090 3085 createopts = dict(createopts or {})
3091 3086
3092 3087 if 'backend' not in createopts:
3093 3088 # experimental config: storage.new-repo-backend
3094 3089 createopts['backend'] = ui.config('storage', 'new-repo-backend')
3095 3090
3096 3091 return createopts
3097 3092
3098 3093 def newreporequirements(ui, createopts):
3099 3094 """Determine the set of requirements for a new local repository.
3100 3095
3101 3096 Extensions can wrap this function to specify custom requirements for
3102 3097 new repositories.
3103 3098 """
3104 3099 # If the repo is being created from a shared repository, we copy
3105 3100 # its requirements.
3106 3101 if 'sharedrepo' in createopts:
3107 3102 requirements = set(createopts['sharedrepo'].requirements)
3108 3103 if createopts.get('sharedrelative'):
3109 3104 requirements.add('relshared')
3110 3105 else:
3111 3106 requirements.add('shared')
3112 3107
3113 3108 return requirements
3114 3109
3115 3110 if 'backend' not in createopts:
3116 3111 raise error.ProgrammingError('backend key not present in createopts; '
3117 3112 'was defaultcreateopts() called?')
3118 3113
3119 3114 if createopts['backend'] != 'revlogv1':
3120 3115 raise error.Abort(_('unable to determine repository requirements for '
3121 3116 'storage backend: %s') % createopts['backend'])
3122 3117
3123 3118 requirements = {'revlogv1'}
3124 3119 if ui.configbool('format', 'usestore'):
3125 3120 requirements.add('store')
3126 3121 if ui.configbool('format', 'usefncache'):
3127 3122 requirements.add('fncache')
3128 3123 if ui.configbool('format', 'dotencode'):
3129 3124 requirements.add('dotencode')
3130 3125
3131 3126 compengine = ui.config('format', 'revlog-compression')
3132 3127 if compengine not in util.compengines:
3133 3128 raise error.Abort(_('compression engine %s defined by '
3134 3129 'format.revlog-compression not available') %
3135 3130 compengine,
3136 3131 hint=_('run "hg debuginstall" to list available '
3137 3132 'compression engines'))
3138 3133
3139 3134 # zlib is the historical default and doesn't need an explicit requirement.
3140 3135 elif compengine == 'zstd':
3141 3136 requirements.add('revlog-compression-zstd')
3142 3137 elif compengine != 'zlib':
3143 3138 requirements.add('exp-compression-%s' % compengine)
3144 3139
3145 3140 if scmutil.gdinitconfig(ui):
3146 3141 requirements.add('generaldelta')
3147 3142 if ui.configbool('format', 'sparse-revlog'):
3148 3143 requirements.add(SPARSEREVLOG_REQUIREMENT)
3149 3144 if ui.configbool('experimental', 'treemanifest'):
3150 3145 requirements.add('treemanifest')
3151 3146
3152 3147 revlogv2 = ui.config('experimental', 'revlogv2')
3153 3148 if revlogv2 == 'enable-unstable-format-and-corrupt-my-data':
3154 3149 requirements.remove('revlogv1')
3155 3150 # generaldelta is implied by revlogv2.
3156 3151 requirements.discard('generaldelta')
3157 3152 requirements.add(REVLOGV2_REQUIREMENT)
3158 3153 # experimental config: format.internal-phase
3159 3154 if ui.configbool('format', 'internal-phase'):
3160 3155 requirements.add('internal-phase')
3161 3156
3162 3157 if createopts.get('narrowfiles'):
3163 3158 requirements.add(repository.NARROW_REQUIREMENT)
3164 3159
3165 3160 if createopts.get('lfs'):
3166 3161 requirements.add('lfs')
3167 3162
3168 3163 if ui.configbool('format', 'bookmarks-in-store'):
3169 3164 requirements.add(bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT)
3170 3165
3171 3166 return requirements
3172 3167
3173 3168 def filterknowncreateopts(ui, createopts):
3174 3169 """Filters a dict of repo creation options against options that are known.
3175 3170
3176 3171 Receives a dict of repo creation options and returns a dict of those
3177 3172 options that we don't know how to handle.
3178 3173
3179 3174 This function is called as part of repository creation. If the
3180 3175 returned dict contains any items, repository creation will not
3181 3176 be allowed, as it means there was a request to create a repository
3182 3177 with options not recognized by loaded code.
3183 3178
3184 3179 Extensions can wrap this function to filter out creation options
3185 3180 they know how to handle.
3186 3181 """
3187 3182 known = {
3188 3183 'backend',
3189 3184 'lfs',
3190 3185 'narrowfiles',
3191 3186 'sharedrepo',
3192 3187 'sharedrelative',
3193 3188 'shareditems',
3194 3189 'shallowfilestore',
3195 3190 }
3196 3191
3197 3192 return {k: v for k, v in createopts.items() if k not in known}
3198 3193
3199 3194 def createrepository(ui, path, createopts=None):
3200 3195 """Create a new repository in a vfs.
3201 3196
3202 3197 ``path`` path to the new repo's working directory.
3203 3198 ``createopts`` options for the new repository.
3204 3199
3205 3200 The following keys for ``createopts`` are recognized:
3206 3201
3207 3202 backend
3208 3203 The storage backend to use.
3209 3204 lfs
3210 3205 Repository will be created with ``lfs`` requirement. The lfs extension
3211 3206 will automatically be loaded when the repository is accessed.
3212 3207 narrowfiles
3213 3208 Set up repository to support narrow file storage.
3214 3209 sharedrepo
3215 3210 Repository object from which storage should be shared.
3216 3211 sharedrelative
3217 3212 Boolean indicating if the path to the shared repo should be
3218 3213 stored as relative. By default, the pointer to the "parent" repo
3219 3214 is stored as an absolute path.
3220 3215 shareditems
3221 3216 Set of items to share to the new repository (in addition to storage).
3222 3217 shallowfilestore
3223 3218 Indicates that storage for files should be shallow (not all ancestor
3224 3219 revisions are known).
3225 3220 """
3226 3221 createopts = defaultcreateopts(ui, createopts=createopts)
3227 3222
3228 3223 unknownopts = filterknowncreateopts(ui, createopts)
3229 3224
3230 3225 if not isinstance(unknownopts, dict):
3231 3226 raise error.ProgrammingError('filterknowncreateopts() did not return '
3232 3227 'a dict')
3233 3228
3234 3229 if unknownopts:
3235 3230 raise error.Abort(_('unable to create repository because of unknown '
3236 3231 'creation option: %s') %
3237 3232 ', '.join(sorted(unknownopts)),
3238 3233 hint=_('is a required extension not loaded?'))
3239 3234
3240 3235 requirements = newreporequirements(ui, createopts=createopts)
3241 3236
3242 3237 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
3243 3238
3244 3239 hgvfs = vfsmod.vfs(wdirvfs.join(b'.hg'))
3245 3240 if hgvfs.exists():
3246 3241 raise error.RepoError(_('repository %s already exists') % path)
3247 3242
3248 3243 if 'sharedrepo' in createopts:
3249 3244 sharedpath = createopts['sharedrepo'].sharedpath
3250 3245
3251 3246 if createopts.get('sharedrelative'):
3252 3247 try:
3253 3248 sharedpath = os.path.relpath(sharedpath, hgvfs.base)
3254 3249 except (IOError, ValueError) as e:
3255 3250 # ValueError is raised on Windows if the drive letters differ
3256 3251 # on each path.
3257 3252 raise error.Abort(_('cannot calculate relative path'),
3258 3253 hint=stringutil.forcebytestr(e))
3259 3254
3260 3255 if not wdirvfs.exists():
3261 3256 wdirvfs.makedirs()
3262 3257
3263 3258 hgvfs.makedir(notindexed=True)
3264 3259 if 'sharedrepo' not in createopts:
3265 3260 hgvfs.mkdir(b'cache')
3266 3261 hgvfs.mkdir(b'wcache')
3267 3262
3268 3263 if b'store' in requirements and 'sharedrepo' not in createopts:
3269 3264 hgvfs.mkdir(b'store')
3270 3265
3271 3266 # We create an invalid changelog outside the store so very old
3272 3267 # Mercurial versions (which didn't know about the requirements
3273 3268 # file) encounter an error on reading the changelog. This
3274 3269 # effectively locks out old clients and prevents them from
3275 3270 # mucking with a repo in an unknown format.
3276 3271 #
3277 3272 # The revlog header has version 2, which won't be recognized by
3278 3273 # such old clients.
3279 3274 hgvfs.append(b'00changelog.i',
3280 3275 b'\0\0\0\2 dummy changelog to prevent using the old repo '
3281 3276 b'layout')
3282 3277
3283 3278 scmutil.writerequires(hgvfs, requirements)
3284 3279
3285 3280 # Write out file telling readers where to find the shared store.
3286 3281 if 'sharedrepo' in createopts:
3287 3282 hgvfs.write(b'sharedpath', sharedpath)
3288 3283
3289 3284 if createopts.get('shareditems'):
3290 3285 shared = b'\n'.join(sorted(createopts['shareditems'])) + b'\n'
3291 3286 hgvfs.write(b'shared', shared)
3292 3287
3293 3288 def poisonrepository(repo):
3294 3289 """Poison a repository instance so it can no longer be used."""
3295 3290 # Perform any cleanup on the instance.
3296 3291 repo.close()
3297 3292
3298 3293 # Our strategy is to replace the type of the object with one that
3299 3294 # has all attribute lookups result in error.
3300 3295 #
3301 3296 # But we have to allow the close() method because some constructors
3302 3297 # of repos call close() on repo references.
3303 3298 class poisonedrepository(object):
3304 3299 def __getattribute__(self, item):
3305 3300 if item == r'close':
3306 3301 return object.__getattribute__(self, item)
3307 3302
3308 3303 raise error.ProgrammingError('repo instances should not be used '
3309 3304 'after unshare')
3310 3305
3311 3306 def close(self):
3312 3307 pass
3313 3308
3314 3309 # We may have a repoview, which intercepts __setattr__. So be sure
3315 3310 # we operate at the lowest level possible.
3316 3311 object.__setattr__(repo, r'__class__', poisonedrepository)
@@ -1,669 +1,679 b''
1 1 # store.py - repository store handling for Mercurial
2 2 #
3 3 # Copyright 2008 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 functools
12 12 import hashlib
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from . import (
18 changelog,
18 19 error,
20 manifest,
19 21 node,
20 22 policy,
21 23 pycompat,
22 24 util,
23 25 vfs as vfsmod,
24 26 )
25 27
26 28 parsers = policy.importmod(r'parsers')
27 29 # how much bytes should be read from fncache in one read
28 30 # It is done to prevent loading large fncache files into memory
29 31 fncache_chunksize = 10 ** 6
30 32
31 33 def _matchtrackedpath(path, matcher):
32 34 """parses a fncache entry and returns whether the entry is tracking a path
33 35 matched by matcher or not.
34 36
35 37 If matcher is None, returns True"""
36 38
37 39 if matcher is None:
38 40 return True
39 41 path = decodedir(path)
40 42 if path.startswith('data/'):
41 43 return matcher(path[len('data/'):-len('.i')])
42 44 elif path.startswith('meta/'):
43 45 return matcher.visitdir(path[len('meta/'):-len('/00manifest.i')])
44 46
45 47 raise error.ProgrammingError("cannot decode path %s" % path)
46 48
47 49 # This avoids a collision between a file named foo and a dir named
48 50 # foo.i or foo.d
49 51 def _encodedir(path):
50 52 '''
51 53 >>> _encodedir(b'data/foo.i')
52 54 'data/foo.i'
53 55 >>> _encodedir(b'data/foo.i/bla.i')
54 56 'data/foo.i.hg/bla.i'
55 57 >>> _encodedir(b'data/foo.i.hg/bla.i')
56 58 'data/foo.i.hg.hg/bla.i'
57 59 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
58 60 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
59 61 '''
60 62 return (path
61 63 .replace(".hg/", ".hg.hg/")
62 64 .replace(".i/", ".i.hg/")
63 65 .replace(".d/", ".d.hg/"))
64 66
65 67 encodedir = getattr(parsers, 'encodedir', _encodedir)
66 68
67 69 def decodedir(path):
68 70 '''
69 71 >>> decodedir(b'data/foo.i')
70 72 'data/foo.i'
71 73 >>> decodedir(b'data/foo.i.hg/bla.i')
72 74 'data/foo.i/bla.i'
73 75 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
74 76 'data/foo.i.hg/bla.i'
75 77 '''
76 78 if ".hg/" not in path:
77 79 return path
78 80 return (path
79 81 .replace(".d.hg/", ".d/")
80 82 .replace(".i.hg/", ".i/")
81 83 .replace(".hg.hg/", ".hg/"))
82 84
83 85 def _reserved():
84 86 ''' characters that are problematic for filesystems
85 87
86 88 * ascii escapes (0..31)
87 89 * ascii hi (126..255)
88 90 * windows specials
89 91
90 92 these characters will be escaped by encodefunctions
91 93 '''
92 94 winreserved = [ord(x) for x in u'\\:*?"<>|']
93 95 for x in range(32):
94 96 yield x
95 97 for x in range(126, 256):
96 98 yield x
97 99 for x in winreserved:
98 100 yield x
99 101
100 102 def _buildencodefun():
101 103 '''
102 104 >>> enc, dec = _buildencodefun()
103 105
104 106 >>> enc(b'nothing/special.txt')
105 107 'nothing/special.txt'
106 108 >>> dec(b'nothing/special.txt')
107 109 'nothing/special.txt'
108 110
109 111 >>> enc(b'HELLO')
110 112 '_h_e_l_l_o'
111 113 >>> dec(b'_h_e_l_l_o')
112 114 'HELLO'
113 115
114 116 >>> enc(b'hello:world?')
115 117 'hello~3aworld~3f'
116 118 >>> dec(b'hello~3aworld~3f')
117 119 'hello:world?'
118 120
119 121 >>> enc(b'the\\x07quick\\xADshot')
120 122 'the~07quick~adshot'
121 123 >>> dec(b'the~07quick~adshot')
122 124 'the\\x07quick\\xadshot'
123 125 '''
124 126 e = '_'
125 127 xchr = pycompat.bytechr
126 128 asciistr = list(map(xchr, range(127)))
127 129 capitals = list(range(ord("A"), ord("Z") + 1))
128 130
129 131 cmap = dict((x, x) for x in asciistr)
130 132 for x in _reserved():
131 133 cmap[xchr(x)] = "~%02x" % x
132 134 for x in capitals + [ord(e)]:
133 135 cmap[xchr(x)] = e + xchr(x).lower()
134 136
135 137 dmap = {}
136 138 for k, v in cmap.iteritems():
137 139 dmap[v] = k
138 140 def decode(s):
139 141 i = 0
140 142 while i < len(s):
141 143 for l in pycompat.xrange(1, 4):
142 144 try:
143 145 yield dmap[s[i:i + l]]
144 146 i += l
145 147 break
146 148 except KeyError:
147 149 pass
148 150 else:
149 151 raise KeyError
150 152 return (lambda s: ''.join([cmap[s[c:c + 1]]
151 153 for c in pycompat.xrange(len(s))]),
152 154 lambda s: ''.join(list(decode(s))))
153 155
154 156 _encodefname, _decodefname = _buildencodefun()
155 157
156 158 def encodefilename(s):
157 159 '''
158 160 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
159 161 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
160 162 '''
161 163 return _encodefname(encodedir(s))
162 164
163 165 def decodefilename(s):
164 166 '''
165 167 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
166 168 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
167 169 '''
168 170 return decodedir(_decodefname(s))
169 171
170 172 def _buildlowerencodefun():
171 173 '''
172 174 >>> f = _buildlowerencodefun()
173 175 >>> f(b'nothing/special.txt')
174 176 'nothing/special.txt'
175 177 >>> f(b'HELLO')
176 178 'hello'
177 179 >>> f(b'hello:world?')
178 180 'hello~3aworld~3f'
179 181 >>> f(b'the\\x07quick\\xADshot')
180 182 'the~07quick~adshot'
181 183 '''
182 184 xchr = pycompat.bytechr
183 185 cmap = dict([(xchr(x), xchr(x)) for x in pycompat.xrange(127)])
184 186 for x in _reserved():
185 187 cmap[xchr(x)] = "~%02x" % x
186 188 for x in range(ord("A"), ord("Z") + 1):
187 189 cmap[xchr(x)] = xchr(x).lower()
188 190 def lowerencode(s):
189 191 return "".join([cmap[c] for c in pycompat.iterbytestr(s)])
190 192 return lowerencode
191 193
192 194 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
193 195
194 196 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
195 197 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
196 198 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
197 199 def _auxencode(path, dotencode):
198 200 '''
199 201 Encodes filenames containing names reserved by Windows or which end in
200 202 period or space. Does not touch other single reserved characters c.
201 203 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
202 204 Additionally encodes space or period at the beginning, if dotencode is
203 205 True. Parameter path is assumed to be all lowercase.
204 206 A segment only needs encoding if a reserved name appears as a
205 207 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
206 208 doesn't need encoding.
207 209
208 210 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
209 211 >>> _auxencode(s.split(b'/'), True)
210 212 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
211 213 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
212 214 >>> _auxencode(s.split(b'/'), False)
213 215 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
214 216 >>> _auxencode([b'foo. '], True)
215 217 ['foo.~20']
216 218 >>> _auxencode([b' .foo'], True)
217 219 ['~20.foo']
218 220 '''
219 221 for i, n in enumerate(path):
220 222 if not n:
221 223 continue
222 224 if dotencode and n[0] in '. ':
223 225 n = "~%02x" % ord(n[0:1]) + n[1:]
224 226 path[i] = n
225 227 else:
226 228 l = n.find('.')
227 229 if l == -1:
228 230 l = len(n)
229 231 if ((l == 3 and n[:3] in _winres3) or
230 232 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
231 233 and n[:3] in _winres4)):
232 234 # encode third letter ('aux' -> 'au~78')
233 235 ec = "~%02x" % ord(n[2:3])
234 236 n = n[0:2] + ec + n[3:]
235 237 path[i] = n
236 238 if n[-1] in '. ':
237 239 # encode last period or space ('foo...' -> 'foo..~2e')
238 240 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
239 241 return path
240 242
241 243 _maxstorepathlen = 120
242 244 _dirprefixlen = 8
243 245 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
244 246
245 247 def _hashencode(path, dotencode):
246 248 digest = node.hex(hashlib.sha1(path).digest())
247 249 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
248 250 parts = _auxencode(le, dotencode)
249 251 basename = parts[-1]
250 252 _root, ext = os.path.splitext(basename)
251 253 sdirs = []
252 254 sdirslen = 0
253 255 for p in parts[:-1]:
254 256 d = p[:_dirprefixlen]
255 257 if d[-1] in '. ':
256 258 # Windows can't access dirs ending in period or space
257 259 d = d[:-1] + '_'
258 260 if sdirslen == 0:
259 261 t = len(d)
260 262 else:
261 263 t = sdirslen + 1 + len(d)
262 264 if t > _maxshortdirslen:
263 265 break
264 266 sdirs.append(d)
265 267 sdirslen = t
266 268 dirs = '/'.join(sdirs)
267 269 if len(dirs) > 0:
268 270 dirs += '/'
269 271 res = 'dh/' + dirs + digest + ext
270 272 spaceleft = _maxstorepathlen - len(res)
271 273 if spaceleft > 0:
272 274 filler = basename[:spaceleft]
273 275 res = 'dh/' + dirs + filler + digest + ext
274 276 return res
275 277
276 278 def _hybridencode(path, dotencode):
277 279 '''encodes path with a length limit
278 280
279 281 Encodes all paths that begin with 'data/', according to the following.
280 282
281 283 Default encoding (reversible):
282 284
283 285 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
284 286 characters are encoded as '~xx', where xx is the two digit hex code
285 287 of the character (see encodefilename).
286 288 Relevant path components consisting of Windows reserved filenames are
287 289 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
288 290
289 291 Hashed encoding (not reversible):
290 292
291 293 If the default-encoded path is longer than _maxstorepathlen, a
292 294 non-reversible hybrid hashing of the path is done instead.
293 295 This encoding uses up to _dirprefixlen characters of all directory
294 296 levels of the lowerencoded path, but not more levels than can fit into
295 297 _maxshortdirslen.
296 298 Then follows the filler followed by the sha digest of the full path.
297 299 The filler is the beginning of the basename of the lowerencoded path
298 300 (the basename is everything after the last path separator). The filler
299 301 is as long as possible, filling in characters from the basename until
300 302 the encoded path has _maxstorepathlen characters (or all chars of the
301 303 basename have been taken).
302 304 The extension (e.g. '.i' or '.d') is preserved.
303 305
304 306 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
305 307 encoding was used.
306 308 '''
307 309 path = encodedir(path)
308 310 ef = _encodefname(path).split('/')
309 311 res = '/'.join(_auxencode(ef, dotencode))
310 312 if len(res) > _maxstorepathlen:
311 313 res = _hashencode(path, dotencode)
312 314 return res
313 315
314 316 def _pathencode(path):
315 317 de = encodedir(path)
316 318 if len(path) > _maxstorepathlen:
317 319 return _hashencode(de, True)
318 320 ef = _encodefname(de).split('/')
319 321 res = '/'.join(_auxencode(ef, True))
320 322 if len(res) > _maxstorepathlen:
321 323 return _hashencode(de, True)
322 324 return res
323 325
324 326 _pathencode = getattr(parsers, 'pathencode', _pathencode)
325 327
326 328 def _plainhybridencode(f):
327 329 return _hybridencode(f, False)
328 330
329 331 def _calcmode(vfs):
330 332 try:
331 333 # files in .hg/ will be created using this mode
332 334 mode = vfs.stat().st_mode
333 335 # avoid some useless chmods
334 336 if (0o777 & ~util.umask) == (0o777 & mode):
335 337 mode = None
336 338 except OSError:
337 339 mode = None
338 340 return mode
339 341
340 342 _data = ('bookmarks narrowspec data meta 00manifest.d 00manifest.i'
341 343 ' 00changelog.d 00changelog.i phaseroots obsstore')
342 344
343 345 def isrevlog(f, kind, st):
344 346 return kind == stat.S_IFREG and f[-2:] in ('.i', '.d')
345 347
346 348 class basicstore(object):
347 349 '''base class for local repository stores'''
348 350 def __init__(self, path, vfstype):
349 351 vfs = vfstype(path)
350 352 self.path = vfs.base
351 353 self.createmode = _calcmode(vfs)
352 354 vfs.createmode = self.createmode
353 355 self.rawvfs = vfs
354 356 self.vfs = vfsmod.filtervfs(vfs, encodedir)
355 357 self.opener = self.vfs
356 358
357 359 def join(self, f):
358 360 return self.path + '/' + encodedir(f)
359 361
360 362 def _walk(self, relpath, recurse, filefilter=isrevlog):
361 363 '''yields (unencoded, encoded, size)'''
362 364 path = self.path
363 365 if relpath:
364 366 path += '/' + relpath
365 367 striplen = len(self.path) + 1
366 368 l = []
367 369 if self.rawvfs.isdir(path):
368 370 visit = [path]
369 371 readdir = self.rawvfs.readdir
370 372 while visit:
371 373 p = visit.pop()
372 374 for f, kind, st in readdir(p, stat=True):
373 375 fp = p + '/' + f
374 376 if filefilter(f, kind, st):
375 377 n = util.pconvert(fp[striplen:])
376 378 l.append((decodedir(n), n, st.st_size))
377 379 elif kind == stat.S_IFDIR and recurse:
378 380 visit.append(fp)
379 381 l.sort()
380 382 return l
381 383
384 def changelog(self, trypending):
385 return changelog.changelog(self.vfs, trypending=trypending)
386
387 def manifestlog(self, repo, storenarrowmatch):
388 rootstore = manifest.manifestrevlog(self.vfs)
389 return manifest.manifestlog(
390 self.vfs, repo, rootstore, storenarrowmatch)
391
382 392 def datafiles(self, matcher=None):
383 393 return self._walk('data', True) + self._walk('meta', True)
384 394
385 395 def topfiles(self):
386 396 # yield manifest before changelog
387 397 return reversed(self._walk('', False))
388 398
389 399 def walk(self, matcher=None):
390 400 '''yields (unencoded, encoded, size)
391 401
392 402 if a matcher is passed, storage files of only those tracked paths
393 403 are passed with matches the matcher
394 404 '''
395 405 # yield data files first
396 406 for x in self.datafiles(matcher):
397 407 yield x
398 408 for x in self.topfiles():
399 409 yield x
400 410
401 411 def copylist(self):
402 412 return ['requires'] + _data.split()
403 413
404 414 def write(self, tr):
405 415 pass
406 416
407 417 def invalidatecaches(self):
408 418 pass
409 419
410 420 def markremoved(self, fn):
411 421 pass
412 422
413 423 def __contains__(self, path):
414 424 '''Checks if the store contains path'''
415 425 path = "/".join(("data", path))
416 426 # file?
417 427 if self.vfs.exists(path + ".i"):
418 428 return True
419 429 # dir?
420 430 if not path.endswith("/"):
421 431 path = path + "/"
422 432 return self.vfs.exists(path)
423 433
424 434 class encodedstore(basicstore):
425 435 def __init__(self, path, vfstype):
426 436 vfs = vfstype(path + '/store')
427 437 self.path = vfs.base
428 438 self.createmode = _calcmode(vfs)
429 439 vfs.createmode = self.createmode
430 440 self.rawvfs = vfs
431 441 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
432 442 self.opener = self.vfs
433 443
434 444 def datafiles(self, matcher=None):
435 445 for a, b, size in super(encodedstore, self).datafiles():
436 446 try:
437 447 a = decodefilename(a)
438 448 except KeyError:
439 449 a = None
440 450 if a is not None and not _matchtrackedpath(a, matcher):
441 451 continue
442 452 yield a, b, size
443 453
444 454 def join(self, f):
445 455 return self.path + '/' + encodefilename(f)
446 456
447 457 def copylist(self):
448 458 return (['requires', '00changelog.i'] +
449 459 ['store/' + f for f in _data.split()])
450 460
451 461 class fncache(object):
452 462 # the filename used to be partially encoded
453 463 # hence the encodedir/decodedir dance
454 464 def __init__(self, vfs):
455 465 self.vfs = vfs
456 466 self.entries = None
457 467 self._dirty = False
458 468 # set of new additions to fncache
459 469 self.addls = set()
460 470
461 471 def ensureloaded(self, warn=None):
462 472 '''read the fncache file if not already read.
463 473
464 474 If the file on disk is corrupted, raise. If warn is provided,
465 475 warn and keep going instead.'''
466 476 if self.entries is None:
467 477 self._load(warn)
468 478
469 479 def _load(self, warn=None):
470 480 '''fill the entries from the fncache file'''
471 481 self._dirty = False
472 482 try:
473 483 fp = self.vfs('fncache', mode='rb')
474 484 except IOError:
475 485 # skip nonexistent file
476 486 self.entries = set()
477 487 return
478 488
479 489 self.entries = set()
480 490 chunk = b''
481 491 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
482 492 chunk += c
483 493 try:
484 494 p = chunk.rindex(b'\n')
485 495 self.entries.update(decodedir(chunk[:p + 1]).splitlines())
486 496 chunk = chunk[p + 1:]
487 497 except ValueError:
488 498 # substring '\n' not found, maybe the entry is bigger than the
489 499 # chunksize, so let's keep iterating
490 500 pass
491 501
492 502 if chunk:
493 503 msg = _("fncache does not ends with a newline")
494 504 if warn:
495 505 warn(msg + '\n')
496 506 else:
497 507 raise error.Abort(msg,
498 508 hint=_("use 'hg debugrebuildfncache' to "
499 509 "rebuild the fncache"))
500 510 self._checkentries(fp, warn)
501 511 fp.close()
502 512
503 513 def _checkentries(self, fp, warn):
504 514 """ make sure there is no empty string in entries """
505 515 if '' in self.entries:
506 516 fp.seek(0)
507 517 for n, line in enumerate(util.iterfile(fp)):
508 518 if not line.rstrip('\n'):
509 519 t = _('invalid entry in fncache, line %d') % (n + 1)
510 520 if warn:
511 521 warn(t + '\n')
512 522 else:
513 523 raise error.Abort(t)
514 524
515 525 def write(self, tr):
516 526 if self._dirty:
517 527 assert self.entries is not None
518 528 self.entries = self.entries | self.addls
519 529 self.addls = set()
520 530 tr.addbackup('fncache')
521 531 fp = self.vfs('fncache', mode='wb', atomictemp=True)
522 532 if self.entries:
523 533 fp.write(encodedir('\n'.join(self.entries) + '\n'))
524 534 fp.close()
525 535 self._dirty = False
526 536 if self.addls:
527 537 # if we have just new entries, let's append them to the fncache
528 538 tr.addbackup('fncache')
529 539 fp = self.vfs('fncache', mode='ab', atomictemp=True)
530 540 if self.addls:
531 541 fp.write(encodedir('\n'.join(self.addls) + '\n'))
532 542 fp.close()
533 543 self.entries = None
534 544 self.addls = set()
535 545
536 546 def add(self, fn):
537 547 if self.entries is None:
538 548 self._load()
539 549 if fn not in self.entries:
540 550 self.addls.add(fn)
541 551
542 552 def remove(self, fn):
543 553 if self.entries is None:
544 554 self._load()
545 555 if fn in self.addls:
546 556 self.addls.remove(fn)
547 557 return
548 558 try:
549 559 self.entries.remove(fn)
550 560 self._dirty = True
551 561 except KeyError:
552 562 pass
553 563
554 564 def __contains__(self, fn):
555 565 if fn in self.addls:
556 566 return True
557 567 if self.entries is None:
558 568 self._load()
559 569 return fn in self.entries
560 570
561 571 def __iter__(self):
562 572 if self.entries is None:
563 573 self._load()
564 574 return iter(self.entries | self.addls)
565 575
566 576 class _fncachevfs(vfsmod.proxyvfs):
567 577 def __init__(self, vfs, fnc, encode):
568 578 vfsmod.proxyvfs.__init__(self, vfs)
569 579 self.fncache = fnc
570 580 self.encode = encode
571 581
572 582 def __call__(self, path, mode='r', *args, **kw):
573 583 encoded = self.encode(path)
574 584 if mode not in ('r', 'rb') and (path.startswith('data/') or
575 585 path.startswith('meta/')):
576 586 # do not trigger a fncache load when adding a file that already is
577 587 # known to exist.
578 588 notload = self.fncache.entries is None and self.vfs.exists(encoded)
579 589 if notload and 'a' in mode and not self.vfs.stat(encoded).st_size:
580 590 # when appending to an existing file, if the file has size zero,
581 591 # it should be considered as missing. Such zero-size files are
582 592 # the result of truncation when a transaction is aborted.
583 593 notload = False
584 594 if not notload:
585 595 self.fncache.add(path)
586 596 return self.vfs(encoded, mode, *args, **kw)
587 597
588 598 def join(self, path):
589 599 if path:
590 600 return self.vfs.join(self.encode(path))
591 601 else:
592 602 return self.vfs.join(path)
593 603
594 604 class fncachestore(basicstore):
595 605 def __init__(self, path, vfstype, dotencode):
596 606 if dotencode:
597 607 encode = _pathencode
598 608 else:
599 609 encode = _plainhybridencode
600 610 self.encode = encode
601 611 vfs = vfstype(path + '/store')
602 612 self.path = vfs.base
603 613 self.pathsep = self.path + '/'
604 614 self.createmode = _calcmode(vfs)
605 615 vfs.createmode = self.createmode
606 616 self.rawvfs = vfs
607 617 fnc = fncache(vfs)
608 618 self.fncache = fnc
609 619 self.vfs = _fncachevfs(vfs, fnc, encode)
610 620 self.opener = self.vfs
611 621
612 622 def join(self, f):
613 623 return self.pathsep + self.encode(f)
614 624
615 625 def getsize(self, path):
616 626 return self.rawvfs.stat(path).st_size
617 627
618 628 def datafiles(self, matcher=None):
619 629 for f in sorted(self.fncache):
620 630 if not _matchtrackedpath(f, matcher):
621 631 continue
622 632 ef = self.encode(f)
623 633 try:
624 634 yield f, ef, self.getsize(ef)
625 635 except OSError as err:
626 636 if err.errno != errno.ENOENT:
627 637 raise
628 638
629 639 def copylist(self):
630 640 d = ('bookmarks narrowspec data meta dh fncache phaseroots obsstore'
631 641 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
632 642 return (['requires', '00changelog.i'] +
633 643 ['store/' + f for f in d.split()])
634 644
635 645 def write(self, tr):
636 646 self.fncache.write(tr)
637 647
638 648 def invalidatecaches(self):
639 649 self.fncache.entries = None
640 650 self.fncache.addls = set()
641 651
642 652 def markremoved(self, fn):
643 653 self.fncache.remove(fn)
644 654
645 655 def _exists(self, f):
646 656 ef = self.encode(f)
647 657 try:
648 658 self.getsize(ef)
649 659 return True
650 660 except OSError as err:
651 661 if err.errno != errno.ENOENT:
652 662 raise
653 663 # nonexistent entry
654 664 return False
655 665
656 666 def __contains__(self, path):
657 667 '''Checks if the store contains path'''
658 668 path = "/".join(("data", path))
659 669 # check for files (exact match)
660 670 e = path + '.i'
661 671 if e in self.fncache and self._exists(e):
662 672 return True
663 673 # now check for directories (prefix match)
664 674 if not path.endswith('/'):
665 675 path += '/'
666 676 for e in self.fncache:
667 677 if e.startswith(path) and self._exists(e):
668 678 return True
669 679 return False
General Comments 0
You need to be logged in to leave comments. Login now