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