1# manifest.py - manifest revision 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 heapq
11import itertools
12import struct
13import weakref
14
15from .i18n import _
16from .node import (
17    bin,
18    hex,
19    nullrev,
20)
21from .pycompat import getattr
22from . import (
23    encoding,
24    error,
25    match as matchmod,
26    mdiff,
27    pathutil,
28    policy,
29    pycompat,
30    revlog,
31    util,
32)
33from .interfaces import (
34    repository,
35    util as interfaceutil,
36)
37from .revlogutils import (
38    constants as revlog_constants,
39)
40
41parsers = policy.importmod('parsers')
42propertycache = util.propertycache
43
44# Allow tests to more easily test the alternate path in manifestdict.fastdelta()
45FASTDELTA_TEXTDIFF_THRESHOLD = 1000
46
47
48def _parse(nodelen, data):
49    # This method does a little bit of excessive-looking
50    # precondition checking. This is so that the behavior of this
51    # class exactly matches its C counterpart to try and help
52    # prevent surprise breakage for anyone that develops against
53    # the pure version.
54    if data and data[-1:] != b'\n':
55        raise ValueError(b'Manifest did not end in a newline.')
56    prev = None
57    for l in data.splitlines():
58        if prev is not None and prev > l:
59            raise ValueError(b'Manifest lines not in sorted order.')
60        prev = l
61        f, n = l.split(b'\0')
62        nl = len(n)
63        flags = n[-1:]
64        if flags in _manifestflags:
65            n = n[:-1]
66            nl -= 1
67        else:
68            flags = b''
69        if nl != 2 * nodelen:
70            raise ValueError(b'Invalid manifest line')
71
72        yield f, bin(n), flags
73
74
75def _text(it):
76    files = []
77    lines = []
78    for f, n, fl in it:
79        files.append(f)
80        # if this is changed to support newlines in filenames,
81        # be sure to check the templates/ dir again (especially *-raw.tmpl)
82        lines.append(b"%s\0%s%s\n" % (f, hex(n), fl))
83
84    _checkforbidden(files)
85    return b''.join(lines)
86
87
88class lazymanifestiter(object):
89    def __init__(self, lm):
90        self.pos = 0
91        self.lm = lm
92
93    def __iter__(self):
94        return self
95
96    def next(self):
97        try:
98            data, pos = self.lm._get(self.pos)
99        except IndexError:
100            raise StopIteration
101        if pos == -1:
102            self.pos += 1
103            return data[0]
104        self.pos += 1
105        zeropos = data.find(b'\x00', pos)
106        return data[pos:zeropos]
107
108    __next__ = next
109
110
111class lazymanifestiterentries(object):
112    def __init__(self, lm):
113        self.lm = lm
114        self.pos = 0
115
116    def __iter__(self):
117        return self
118
119    def next(self):
120        try:
121            data, pos = self.lm._get(self.pos)
122        except IndexError:
123            raise StopIteration
124        if pos == -1:
125            self.pos += 1
126            return data
127        zeropos = data.find(b'\x00', pos)
128        nlpos = data.find(b'\n', pos)
129        if zeropos == -1 or nlpos == -1 or nlpos < zeropos:
130            raise error.StorageError(b'Invalid manifest line')
131        flags = data[nlpos - 1 : nlpos]
132        if flags in _manifestflags:
133            hlen = nlpos - zeropos - 2
134        else:
135            hlen = nlpos - zeropos - 1
136            flags = b''
137        if hlen != 2 * self.lm._nodelen:
138            raise error.StorageError(b'Invalid manifest line')
139        hashval = unhexlify(
140            data, self.lm.extrainfo[self.pos], zeropos + 1, hlen
141        )
142        self.pos += 1
143        return (data[pos:zeropos], hashval, flags)
144
145    __next__ = next
146
147
148def unhexlify(data, extra, pos, length):
149    s = bin(data[pos : pos + length])
150    if extra:
151        s += chr(extra & 0xFF)
152    return s
153
154
155def _cmp(a, b):
156    return (a > b) - (a < b)
157
158
159_manifestflags = {b'', b'l', b't', b'x'}
160
161
162class _lazymanifest(object):
163    """A pure python manifest backed by a byte string.  It is supplimented with
164    internal lists as it is modified, until it is compacted back to a pure byte
165    string.
166
167    ``data`` is the initial manifest data.
168
169    ``positions`` is a list of offsets, one per manifest entry.  Positive
170    values are offsets into ``data``, negative values are offsets into the
171    ``extradata`` list.  When an entry is removed, its entry is dropped from
172    ``positions``.  The values are encoded such that when walking the list and
173    indexing into ``data`` or ``extradata`` as appropriate, the entries are
174    sorted by filename.
175
176    ``extradata`` is a list of (key, hash, flags) for entries that were added or
177    modified since the manifest was created or compacted.
178    """
179
180    def __init__(
181        self,
182        nodelen,
183        data,
184        positions=None,
185        extrainfo=None,
186        extradata=None,
187        hasremovals=False,
188    ):
189        self._nodelen = nodelen
190        if positions is None:
191            self.positions = self.findlines(data)
192            self.extrainfo = [0] * len(self.positions)
193            self.data = data
194            self.extradata = []
195            self.hasremovals = False
196        else:
197            self.positions = positions[:]
198            self.extrainfo = extrainfo[:]
199            self.extradata = extradata[:]
200            self.data = data
201            self.hasremovals = hasremovals
202
203    def findlines(self, data):
204        if not data:
205            return []
206        pos = data.find(b"\n")
207        if pos == -1 or data[-1:] != b'\n':
208            raise ValueError(b"Manifest did not end in a newline.")
209        positions = [0]
210        prev = data[: data.find(b'\x00')]
211        while pos < len(data) - 1 and pos != -1:
212            positions.append(pos + 1)
213            nexts = data[pos + 1 : data.find(b'\x00', pos + 1)]
214            if nexts < prev:
215                raise ValueError(b"Manifest lines not in sorted order.")
216            prev = nexts
217            pos = data.find(b"\n", pos + 1)
218        return positions
219
220    def _get(self, index):
221        # get the position encoded in pos:
222        #   positive number is an index in 'data'
223        #   negative number is in extrapieces
224        pos = self.positions[index]
225        if pos >= 0:
226            return self.data, pos
227        return self.extradata[-pos - 1], -1
228
229    def _getkey(self, pos):
230        if pos >= 0:
231            return self.data[pos : self.data.find(b'\x00', pos + 1)]
232        return self.extradata[-pos - 1][0]
233
234    def bsearch(self, key):
235        first = 0
236        last = len(self.positions) - 1
237
238        while first <= last:
239            midpoint = (first + last) // 2
240            nextpos = self.positions[midpoint]
241            candidate = self._getkey(nextpos)
242            r = _cmp(key, candidate)
243            if r == 0:
244                return midpoint
245            else:
246                if r < 0:
247                    last = midpoint - 1
248                else:
249                    first = midpoint + 1
250        return -1
251
252    def bsearch2(self, key):
253        # same as the above, but will always return the position
254        # done for performance reasons
255        first = 0
256        last = len(self.positions) - 1
257
258        while first <= last:
259            midpoint = (first + last) // 2
260            nextpos = self.positions[midpoint]
261            candidate = self._getkey(nextpos)
262            r = _cmp(key, candidate)
263            if r == 0:
264                return (midpoint, True)
265            else:
266                if r < 0:
267                    last = midpoint - 1
268                else:
269                    first = midpoint + 1
270        return (first, False)
271
272    def __contains__(self, key):
273        return self.bsearch(key) != -1
274
275    def __getitem__(self, key):
276        if not isinstance(key, bytes):
277            raise TypeError(b"getitem: manifest keys must be a bytes.")
278        needle = self.bsearch(key)
279        if needle == -1:
280            raise KeyError
281        data, pos = self._get(needle)
282        if pos == -1:
283            return (data[1], data[2])
284        zeropos = data.find(b'\x00', pos)
285        nlpos = data.find(b'\n', zeropos)
286        assert 0 <= needle <= len(self.positions)
287        assert len(self.extrainfo) == len(self.positions)
288        if zeropos == -1 or nlpos == -1 or nlpos < zeropos:
289            raise error.StorageError(b'Invalid manifest line')
290        hlen = nlpos - zeropos - 1
291        flags = data[nlpos - 1 : nlpos]
292        if flags in _manifestflags:
293            hlen -= 1
294        else:
295            flags = b''
296        if hlen != 2 * self._nodelen:
297            raise error.StorageError(b'Invalid manifest line')
298        hashval = unhexlify(data, self.extrainfo[needle], zeropos + 1, hlen)
299        return (hashval, flags)
300
301    def __delitem__(self, key):
302        needle, found = self.bsearch2(key)
303        if not found:
304            raise KeyError
305        cur = self.positions[needle]
306        self.positions = self.positions[:needle] + self.positions[needle + 1 :]
307        self.extrainfo = self.extrainfo[:needle] + self.extrainfo[needle + 1 :]
308        if cur >= 0:
309            # This does NOT unsort the list as far as the search functions are
310            # concerned, as they only examine lines mapped by self.positions.
311            self.data = self.data[:cur] + b'\x00' + self.data[cur + 1 :]
312            self.hasremovals = True
313
314    def __setitem__(self, key, value):
315        if not isinstance(key, bytes):
316            raise TypeError(b"setitem: manifest keys must be a byte string.")
317        if not isinstance(value, tuple) or len(value) != 2:
318            raise TypeError(
319                b"Manifest values must be a tuple of (node, flags)."
320            )
321        hashval = value[0]
322        if not isinstance(hashval, bytes) or len(hashval) not in (20, 32):
323            raise TypeError(b"node must be a 20-byte or 32-byte byte string")
324        flags = value[1]
325        if not isinstance(flags, bytes) or len(flags) > 1:
326            raise TypeError(b"flags must a 0 or 1 byte string, got %r", flags)
327        needle, found = self.bsearch2(key)
328        if found:
329            # put the item
330            pos = self.positions[needle]
331            if pos < 0:
332                self.extradata[-pos - 1] = (key, hashval, value[1])
333            else:
334                # just don't bother
335                self.extradata.append((key, hashval, value[1]))
336                self.positions[needle] = -len(self.extradata)
337        else:
338            # not found, put it in with extra positions
339            self.extradata.append((key, hashval, value[1]))
340            self.positions = (
341                self.positions[:needle]
342                + [-len(self.extradata)]
343                + self.positions[needle:]
344            )
345            self.extrainfo = (
346                self.extrainfo[:needle] + [0] + self.extrainfo[needle:]
347            )
348
349    def copy(self):
350        # XXX call _compact like in C?
351        return _lazymanifest(
352            self._nodelen,
353            self.data,
354            self.positions,
355            self.extrainfo,
356            self.extradata,
357            self.hasremovals,
358        )
359
360    def _compact(self):
361        # hopefully not called TOO often
362        if len(self.extradata) == 0 and not self.hasremovals:
363            return
364        l = []
365        i = 0
366        offset = 0
367        self.extrainfo = [0] * len(self.positions)
368        while i < len(self.positions):
369            if self.positions[i] >= 0:
370                cur = self.positions[i]
371                last_cut = cur
372
373                # Collect all contiguous entries in the buffer at the current
374                # offset, breaking out only for added/modified items held in
375                # extradata, or a deleted line prior to the next position.
376                while True:
377                    self.positions[i] = offset
378                    i += 1
379                    if i == len(self.positions) or self.positions[i] < 0:
380                        break
381
382                    # A removed file has no positions[] entry, but does have an
383                    # overwritten first byte.  Break out and find the end of the
384                    # current good entry/entries if there is a removed file
385                    # before the next position.
386                    if (
387                        self.hasremovals
388                        and self.data.find(b'\n\x00', cur, self.positions[i])
389                        != -1
390                    ):
391                        break
392
393                    offset += self.positions[i] - cur
394                    cur = self.positions[i]
395                end_cut = self.data.find(b'\n', cur)
396                if end_cut != -1:
397                    end_cut += 1
398                offset += end_cut - cur
399                l.append(self.data[last_cut:end_cut])
400            else:
401                while i < len(self.positions) and self.positions[i] < 0:
402                    cur = self.positions[i]
403                    t = self.extradata[-cur - 1]
404                    l.append(self._pack(t))
405                    self.positions[i] = offset
406                    # Hashes are either 20 bytes (old sha1s) or 32
407                    # bytes (new non-sha1).
408                    hlen = 20
409                    if len(t[1]) > 25:
410                        hlen = 32
411                    if len(t[1]) > hlen:
412                        self.extrainfo[i] = ord(t[1][hlen + 1])
413                    offset += len(l[-1])
414                    i += 1
415        self.data = b''.join(l)
416        self.hasremovals = False
417        self.extradata = []
418
419    def _pack(self, d):
420        n = d[1]
421        assert len(n) in (20, 32)
422        return d[0] + b'\x00' + hex(n) + d[2] + b'\n'
423
424    def text(self):
425        self._compact()
426        return self.data
427
428    def diff(self, m2, clean=False):
429        '''Finds changes between the current manifest and m2.'''
430        # XXX think whether efficiency matters here
431        diff = {}
432
433        for fn, e1, flags in self.iterentries():
434            if fn not in m2:
435                diff[fn] = (e1, flags), (None, b'')
436            else:
437                e2 = m2[fn]
438                if (e1, flags) != e2:
439                    diff[fn] = (e1, flags), e2
440                elif clean:
441                    diff[fn] = None
442
443        for fn, e2, flags in m2.iterentries():
444            if fn not in self:
445                diff[fn] = (None, b''), (e2, flags)
446
447        return diff
448
449    def iterentries(self):
450        return lazymanifestiterentries(self)
451
452    def iterkeys(self):
453        return lazymanifestiter(self)
454
455    def __iter__(self):
456        return lazymanifestiter(self)
457
458    def __len__(self):
459        return len(self.positions)
460
461    def filtercopy(self, filterfn):
462        # XXX should be optimized
463        c = _lazymanifest(self._nodelen, b'')
464        for f, n, fl in self.iterentries():
465            if filterfn(f):
466                c[f] = n, fl
467        return c
468
469
470try:
471    _lazymanifest = parsers.lazymanifest
472except AttributeError:
473    pass
474
475
476@interfaceutil.implementer(repository.imanifestdict)
477class manifestdict(object):
478    def __init__(self, nodelen, data=b''):
479        self._nodelen = nodelen
480        self._lm = _lazymanifest(nodelen, data)
481
482    def __getitem__(self, key):
483        return self._lm[key][0]
484
485    def find(self, key):
486        return self._lm[key]
487
488    def __len__(self):
489        return len(self._lm)
490
491    def __nonzero__(self):
492        # nonzero is covered by the __len__ function, but implementing it here
493        # makes it easier for extensions to override.
494        return len(self._lm) != 0
495
496    __bool__ = __nonzero__
497
498    def __setitem__(self, key, node):
499        self._lm[key] = node, self.flags(key)
500
501    def __contains__(self, key):
502        if key is None:
503            return False
504        return key in self._lm
505
506    def __delitem__(self, key):
507        del self._lm[key]
508
509    def __iter__(self):
510        return self._lm.__iter__()
511
512    def iterkeys(self):
513        return self._lm.iterkeys()
514
515    def keys(self):
516        return list(self.iterkeys())
517
518    def filesnotin(self, m2, match=None):
519        '''Set of files in this manifest that are not in the other'''
520        if match is not None:
521            match = matchmod.badmatch(match, lambda path, msg: None)
522            sm2 = set(m2.walk(match))
523            return {f for f in self.walk(match) if f not in sm2}
524        return {f for f in self if f not in m2}
525
526    @propertycache
527    def _dirs(self):
528        return pathutil.dirs(self)
529
530    def dirs(self):
531        return self._dirs
532
533    def hasdir(self, dir):
534        return dir in self._dirs
535
536    def _filesfastpath(self, match):
537        """Checks whether we can correctly and quickly iterate over matcher
538        files instead of over manifest files."""
539        files = match.files()
540        return len(files) < 100 and (
541            match.isexact()
542            or (match.prefix() and all(fn in self for fn in files))
543        )
544
545    def walk(self, match):
546        """Generates matching file names.
547
548        Equivalent to manifest.matches(match).iterkeys(), but without creating
549        an entirely new manifest.
550
551        It also reports nonexistent files by marking them bad with match.bad().
552        """
553        if match.always():
554            for f in iter(self):
555                yield f
556            return
557
558        fset = set(match.files())
559
560        # avoid the entire walk if we're only looking for specific files
561        if self._filesfastpath(match):
562            for fn in sorted(fset):
563                if fn in self:
564                    yield fn
565            return
566
567        for fn in self:
568            if fn in fset:
569                # specified pattern is the exact name
570                fset.remove(fn)
571            if match(fn):
572                yield fn
573
574        # for dirstate.walk, files=[''] means "walk the whole tree".
575        # follow that here, too
576        fset.discard(b'')
577
578        for fn in sorted(fset):
579            if not self.hasdir(fn):
580                match.bad(fn, None)
581
582    def _matches(self, match):
583        '''generate a new manifest filtered by the match argument'''
584        if match.always():
585            return self.copy()
586
587        if self._filesfastpath(match):
588            m = manifestdict(self._nodelen)
589            lm = self._lm
590            for fn in match.files():
591                if fn in lm:
592                    m._lm[fn] = lm[fn]
593            return m
594
595        m = manifestdict(self._nodelen)
596        m._lm = self._lm.filtercopy(match)
597        return m
598
599    def diff(self, m2, match=None, clean=False):
600        """Finds changes between the current manifest and m2.
601
602        Args:
603          m2: the manifest to which this manifest should be compared.
604          clean: if true, include files unchanged between these manifests
605                 with a None value in the returned dictionary.
606
607        The result is returned as a dict with filename as key and
608        values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
609        nodeid in the current/other manifest and fl1/fl2 is the flag
610        in the current/other manifest. Where the file does not exist,
611        the nodeid will be None and the flags will be the empty
612        string.
613        """
614        if match:
615            m1 = self._matches(match)
616            m2 = m2._matches(match)
617            return m1.diff(m2, clean=clean)
618        return self._lm.diff(m2._lm, clean)
619
620    def setflag(self, key, flag):
621        if flag not in _manifestflags:
622            raise TypeError(b"Invalid manifest flag set.")
623        self._lm[key] = self[key], flag
624
625    def get(self, key, default=None):
626        try:
627            return self._lm[key][0]
628        except KeyError:
629            return default
630
631    def flags(self, key):
632        try:
633            return self._lm[key][1]
634        except KeyError:
635            return b''
636
637    def copy(self):
638        c = manifestdict(self._nodelen)
639        c._lm = self._lm.copy()
640        return c
641
642    def items(self):
643        return (x[:2] for x in self._lm.iterentries())
644
645    def iteritems(self):
646        return (x[:2] for x in self._lm.iterentries())
647
648    def iterentries(self):
649        return self._lm.iterentries()
650
651    def text(self):
652        # most likely uses native version
653        return self._lm.text()
654
655    def fastdelta(self, base, changes):
656        """Given a base manifest text as a bytearray and a list of changes
657        relative to that text, compute a delta that can be used by revlog.
658        """
659        delta = []
660        dstart = None
661        dend = None
662        dline = [b""]
663        start = 0
664        # zero copy representation of base as a buffer
665        addbuf = util.buffer(base)
666
667        changes = list(changes)
668        if len(changes) < FASTDELTA_TEXTDIFF_THRESHOLD:
669            # start with a readonly loop that finds the offset of
670            # each line and creates the deltas
671            for f, todelete in changes:
672                # bs will either be the index of the item or the insert point
673                start, end = _msearch(addbuf, f, start)
674                if not todelete:
675                    h, fl = self._lm[f]
676                    l = b"%s\0%s%s\n" % (f, hex(h), fl)
677                else:
678                    if start == end:
679                        # item we want to delete was not found, error out
680                        raise AssertionError(
681                            _(b"failed to remove %s from manifest") % f
682                        )
683                    l = b""
684                if dstart is not None and dstart <= start and dend >= start:
685                    if dend < end:
686                        dend = end
687                    if l:
688                        dline.append(l)
689                else:
690                    if dstart is not None:
691                        delta.append([dstart, dend, b"".join(dline)])
692                    dstart = start
693                    dend = end
694                    dline = [l]
695
696            if dstart is not None:
697                delta.append([dstart, dend, b"".join(dline)])
698            # apply the delta to the base, and get a delta for addrevision
699            deltatext, arraytext = _addlistdelta(base, delta)
700        else:
701            # For large changes, it's much cheaper to just build the text and
702            # diff it.
703            arraytext = bytearray(self.text())
704            deltatext = mdiff.textdiff(
705                util.buffer(base), util.buffer(arraytext)
706            )
707
708        return arraytext, deltatext
709
710
711def _msearch(m, s, lo=0, hi=None):
712    """return a tuple (start, end) that says where to find s within m.
713
714    If the string is found m[start:end] are the line containing
715    that string.  If start == end the string was not found and
716    they indicate the proper sorted insertion point.
717
718    m should be a buffer, a memoryview or a byte string.
719    s is a byte string"""
720
721    def advance(i, c):
722        while i < lenm and m[i : i + 1] != c:
723            i += 1
724        return i
725
726    if not s:
727        return (lo, lo)
728    lenm = len(m)
729    if not hi:
730        hi = lenm
731    while lo < hi:
732        mid = (lo + hi) // 2
733        start = mid
734        while start > 0 and m[start - 1 : start] != b'\n':
735            start -= 1
736        end = advance(start, b'\0')
737        if bytes(m[start:end]) < s:
738            # we know that after the null there are 40 bytes of sha1
739            # this translates to the bisect lo = mid + 1
740            lo = advance(end + 40, b'\n') + 1
741        else:
742            # this translates to the bisect hi = mid
743            hi = start
744    end = advance(lo, b'\0')
745    found = m[lo:end]
746    if s == found:
747        # we know that after the null there are 40 bytes of sha1
748        end = advance(end + 40, b'\n')
749        return (lo, end + 1)
750    else:
751        return (lo, lo)
752
753
754def _checkforbidden(l):
755    """Check filenames for illegal characters."""
756    for f in l:
757        if b'\n' in f or b'\r' in f:
758            raise error.StorageError(
759                _(b"'\\n' and '\\r' disallowed in filenames: %r")
760                % pycompat.bytestr(f)
761            )
762
763
764# apply the changes collected during the bisect loop to our addlist
765# return a delta suitable for addrevision
766def _addlistdelta(addlist, x):
767    # for large addlist arrays, building a new array is cheaper
768    # than repeatedly modifying the existing one
769    currentposition = 0
770    newaddlist = bytearray()
771
772    for start, end, content in x:
773        newaddlist += addlist[currentposition:start]
774        if content:
775            newaddlist += bytearray(content)
776
777        currentposition = end
778
779    newaddlist += addlist[currentposition:]
780
781    deltatext = b"".join(
782        struct.pack(b">lll", start, end, len(content)) + content
783        for start, end, content in x
784    )
785    return deltatext, newaddlist
786
787
788def _splittopdir(f):
789    if b'/' in f:
790        dir, subpath = f.split(b'/', 1)
791        return dir + b'/', subpath
792    else:
793        return b'', f
794
795
796_noop = lambda s: None
797
798
799@interfaceutil.implementer(repository.imanifestdict)
800class treemanifest(object):
801    def __init__(self, nodeconstants, dir=b'', text=b''):
802        self._dir = dir
803        self.nodeconstants = nodeconstants
804        self._node = self.nodeconstants.nullid
805        self._nodelen = self.nodeconstants.nodelen
806        self._loadfunc = _noop
807        self._copyfunc = _noop
808        self._dirty = False
809        self._dirs = {}
810        self._lazydirs = {}
811        # Using _lazymanifest here is a little slower than plain old dicts
812        self._files = {}
813        self._flags = {}
814        if text:
815
816            def readsubtree(subdir, subm):
817                raise AssertionError(
818                    b'treemanifest constructor only accepts flat manifests'
819                )
820
821            self.parse(text, readsubtree)
822            self._dirty = True  # Mark flat manifest dirty after parsing
823
824    def _subpath(self, path):
825        return self._dir + path
826
827    def _loadalllazy(self):
828        selfdirs = self._dirs
829        subpath = self._subpath
830        for d, (node, readsubtree, docopy) in pycompat.iteritems(
831            self._lazydirs
832        ):
833            if docopy:
834                selfdirs[d] = readsubtree(subpath(d), node).copy()
835            else:
836                selfdirs[d] = readsubtree(subpath(d), node)
837        self._lazydirs = {}
838
839    def _loadlazy(self, d):
840        v = self._lazydirs.get(d)
841        if v:
842            node, readsubtree, docopy = v
843            if docopy:
844                self._dirs[d] = readsubtree(self._subpath(d), node).copy()
845            else:
846                self._dirs[d] = readsubtree(self._subpath(d), node)
847            del self._lazydirs[d]
848
849    def _loadchildrensetlazy(self, visit):
850        if not visit:
851            return None
852        if visit == b'all' or visit == b'this':
853            self._loadalllazy()
854            return None
855
856        loadlazy = self._loadlazy
857        for k in visit:
858            loadlazy(k + b'/')
859        return visit
860
861    def _loaddifflazy(self, t1, t2):
862        """load items in t1 and t2 if they're needed for diffing.
863
864        The criteria currently is:
865        - if it's not present in _lazydirs in either t1 or t2, load it in the
866          other (it may already be loaded or it may not exist, doesn't matter)
867        - if it's present in _lazydirs in both, compare the nodeid; if it
868          differs, load it in both
869        """
870        toloadlazy = []
871        for d, v1 in pycompat.iteritems(t1._lazydirs):
872            v2 = t2._lazydirs.get(d)
873            if not v2 or v2[0] != v1[0]:
874                toloadlazy.append(d)
875        for d, v1 in pycompat.iteritems(t2._lazydirs):
876            if d not in t1._lazydirs:
877                toloadlazy.append(d)
878
879        for d in toloadlazy:
880            t1._loadlazy(d)
881            t2._loadlazy(d)
882
883    def __len__(self):
884        self._load()
885        size = len(self._files)
886        self._loadalllazy()
887        for m in self._dirs.values():
888            size += m.__len__()
889        return size
890
891    def __nonzero__(self):
892        # Faster than "__len() != 0" since it avoids loading sub-manifests
893        return not self._isempty()
894
895    __bool__ = __nonzero__
896
897    def _isempty(self):
898        self._load()  # for consistency; already loaded by all callers
899        # See if we can skip loading everything.
900        if self._files or (
901            self._dirs and any(not m._isempty() for m in self._dirs.values())
902        ):
903            return False
904        self._loadalllazy()
905        return not self._dirs or all(m._isempty() for m in self._dirs.values())
906
907    @encoding.strmethod
908    def __repr__(self):
909        return (
910            b'<treemanifest dir=%s, node=%s, loaded=%r, dirty=%r at 0x%x>'
911            % (
912                self._dir,
913                hex(self._node),
914                bool(self._loadfunc is _noop),
915                self._dirty,
916                id(self),
917            )
918        )
919
920    def dir(self):
921        """The directory that this tree manifest represents, including a
922        trailing '/'. Empty string for the repo root directory."""
923        return self._dir
924
925    def node(self):
926        """This node of this instance. nullid for unsaved instances. Should
927        be updated when the instance is read or written from a revlog.
928        """
929        assert not self._dirty
930        return self._node
931
932    def setnode(self, node):
933        self._node = node
934        self._dirty = False
935
936    def iterentries(self):
937        self._load()
938        self._loadalllazy()
939        for p, n in sorted(
940            itertools.chain(self._dirs.items(), self._files.items())
941        ):
942            if p in self._files:
943                yield self._subpath(p), n, self._flags.get(p, b'')
944            else:
945                for x in n.iterentries():
946                    yield x
947
948    def items(self):
949        self._load()
950        self._loadalllazy()
951        for p, n in sorted(
952            itertools.chain(self._dirs.items(), self._files.items())
953        ):
954            if p in self._files:
955                yield self._subpath(p), n
956            else:
957                for f, sn in pycompat.iteritems(n):
958                    yield f, sn
959
960    iteritems = items
961
962    def iterkeys(self):
963        self._load()
964        self._loadalllazy()
965        for p in sorted(itertools.chain(self._dirs, self._files)):
966            if p in self._files:
967                yield self._subpath(p)
968            else:
969                for f in self._dirs[p]:
970                    yield f
971
972    def keys(self):
973        return list(self.iterkeys())
974
975    def __iter__(self):
976        return self.iterkeys()
977
978    def __contains__(self, f):
979        if f is None:
980            return False
981        self._load()
982        dir, subpath = _splittopdir(f)
983        if dir:
984            self._loadlazy(dir)
985
986            if dir not in self._dirs:
987                return False
988
989            return self._dirs[dir].__contains__(subpath)
990        else:
991            return f in self._files
992
993    def get(self, f, default=None):
994        self._load()
995        dir, subpath = _splittopdir(f)
996        if dir:
997            self._loadlazy(dir)
998
999            if dir not in self._dirs:
1000                return default
1001            return self._dirs[dir].get(subpath, default)
1002        else:
1003            return self._files.get(f, default)
1004
1005    def __getitem__(self, f):
1006        self._load()
1007        dir, subpath = _splittopdir(f)
1008        if dir:
1009            self._loadlazy(dir)
1010
1011            return self._dirs[dir].__getitem__(subpath)
1012        else:
1013            return self._files[f]
1014
1015    def flags(self, f):
1016        self._load()
1017        dir, subpath = _splittopdir(f)
1018        if dir:
1019            self._loadlazy(dir)
1020
1021            if dir not in self._dirs:
1022                return b''
1023            return self._dirs[dir].flags(subpath)
1024        else:
1025            if f in self._lazydirs or f in self._dirs:
1026                return b''
1027            return self._flags.get(f, b'')
1028
1029    def find(self, f):
1030        self._load()
1031        dir, subpath = _splittopdir(f)
1032        if dir:
1033            self._loadlazy(dir)
1034
1035            return self._dirs[dir].find(subpath)
1036        else:
1037            return self._files[f], self._flags.get(f, b'')
1038
1039    def __delitem__(self, f):
1040        self._load()
1041        dir, subpath = _splittopdir(f)
1042        if dir:
1043            self._loadlazy(dir)
1044
1045            self._dirs[dir].__delitem__(subpath)
1046            # If the directory is now empty, remove it
1047            if self._dirs[dir]._isempty():
1048                del self._dirs[dir]
1049        else:
1050            del self._files[f]
1051            if f in self._flags:
1052                del self._flags[f]
1053        self._dirty = True
1054
1055    def __setitem__(self, f, n):
1056        assert n is not None
1057        self._load()
1058        dir, subpath = _splittopdir(f)
1059        if dir:
1060            self._loadlazy(dir)
1061            if dir not in self._dirs:
1062                self._dirs[dir] = treemanifest(
1063                    self.nodeconstants, self._subpath(dir)
1064                )
1065            self._dirs[dir].__setitem__(subpath, n)
1066        else:
1067            # manifest nodes are either 20 bytes or 32 bytes,
1068            # depending on the hash in use. Assert this as historically
1069            # sometimes extra bytes were added.
1070            assert len(n) in (20, 32)
1071            self._files[f] = n
1072        self._dirty = True
1073
1074    def _load(self):
1075        if self._loadfunc is not _noop:
1076            lf, self._loadfunc = self._loadfunc, _noop
1077            lf(self)
1078        elif self._copyfunc is not _noop:
1079            cf, self._copyfunc = self._copyfunc, _noop
1080            cf(self)
1081
1082    def setflag(self, f, flags):
1083        """Set the flags (symlink, executable) for path f."""
1084        if flags not in _manifestflags:
1085            raise TypeError(b"Invalid manifest flag set.")
1086        self._load()
1087        dir, subpath = _splittopdir(f)
1088        if dir:
1089            self._loadlazy(dir)
1090            if dir not in self._dirs:
1091                self._dirs[dir] = treemanifest(
1092                    self.nodeconstants, self._subpath(dir)
1093                )
1094            self._dirs[dir].setflag(subpath, flags)
1095        else:
1096            self._flags[f] = flags
1097        self._dirty = True
1098
1099    def copy(self):
1100        copy = treemanifest(self.nodeconstants, self._dir)
1101        copy._node = self._node
1102        copy._dirty = self._dirty
1103        if self._copyfunc is _noop:
1104
1105            def _copyfunc(s):
1106                self._load()
1107                s._lazydirs = {
1108                    d: (n, r, True)
1109                    for d, (n, r, c) in pycompat.iteritems(self._lazydirs)
1110                }
1111                sdirs = s._dirs
1112                for d, v in pycompat.iteritems(self._dirs):
1113                    sdirs[d] = v.copy()
1114                s._files = dict.copy(self._files)
1115                s._flags = dict.copy(self._flags)
1116
1117            if self._loadfunc is _noop:
1118                _copyfunc(copy)
1119            else:
1120                copy._copyfunc = _copyfunc
1121        else:
1122            copy._copyfunc = self._copyfunc
1123        return copy
1124
1125    def filesnotin(self, m2, match=None):
1126        '''Set of files in this manifest that are not in the other'''
1127        if match and not match.always():
1128            m1 = self._matches(match)
1129            m2 = m2._matches(match)
1130            return m1.filesnotin(m2)
1131
1132        files = set()
1133
1134        def _filesnotin(t1, t2):
1135            if t1._node == t2._node and not t1._dirty and not t2._dirty:
1136                return
1137            t1._load()
1138            t2._load()
1139            self._loaddifflazy(t1, t2)
1140            for d, m1 in pycompat.iteritems(t1._dirs):
1141                if d in t2._dirs:
1142                    m2 = t2._dirs[d]
1143                    _filesnotin(m1, m2)
1144                else:
1145                    files.update(m1.iterkeys())
1146
1147            for fn in t1._files:
1148                if fn not in t2._files:
1149                    files.add(t1._subpath(fn))
1150
1151        _filesnotin(self, m2)
1152        return files
1153
1154    @propertycache
1155    def _alldirs(self):
1156        return pathutil.dirs(self)
1157
1158    def dirs(self):
1159        return self._alldirs
1160
1161    def hasdir(self, dir):
1162        self._load()
1163        topdir, subdir = _splittopdir(dir)
1164        if topdir:
1165            self._loadlazy(topdir)
1166            if topdir in self._dirs:
1167                return self._dirs[topdir].hasdir(subdir)
1168            return False
1169        dirslash = dir + b'/'
1170        return dirslash in self._dirs or dirslash in self._lazydirs
1171
1172    def walk(self, match):
1173        """Generates matching file names.
1174
1175        It also reports nonexistent files by marking them bad with match.bad().
1176        """
1177        if match.always():
1178            for f in iter(self):
1179                yield f
1180            return
1181
1182        fset = set(match.files())
1183
1184        for fn in self._walk(match):
1185            if fn in fset:
1186                # specified pattern is the exact name
1187                fset.remove(fn)
1188            yield fn
1189
1190        # for dirstate.walk, files=[''] means "walk the whole tree".
1191        # follow that here, too
1192        fset.discard(b'')
1193
1194        for fn in sorted(fset):
1195            if not self.hasdir(fn):
1196                match.bad(fn, None)
1197
1198    def _walk(self, match):
1199        '''Recursively generates matching file names for walk().'''
1200        visit = match.visitchildrenset(self._dir[:-1])
1201        if not visit:
1202            return
1203
1204        # yield this dir's files and walk its submanifests
1205        self._load()
1206        visit = self._loadchildrensetlazy(visit)
1207        for p in sorted(list(self._dirs) + list(self._files)):
1208            if p in self._files:
1209                fullp = self._subpath(p)
1210                if match(fullp):
1211                    yield fullp
1212            else:
1213                if not visit or p[:-1] in visit:
1214                    for f in self._dirs[p]._walk(match):
1215                        yield f
1216
1217    def _matches(self, match):
1218        """recursively generate a new manifest filtered by the match argument."""
1219        if match.always():
1220            return self.copy()
1221        return self._matches_inner(match)
1222
1223    def _matches_inner(self, match):
1224        if match.always():
1225            return self.copy()
1226
1227        visit = match.visitchildrenset(self._dir[:-1])
1228        if visit == b'all':
1229            return self.copy()
1230        ret = treemanifest(self.nodeconstants, self._dir)
1231        if not visit:
1232            return ret
1233
1234        self._load()
1235        for fn in self._files:
1236            # While visitchildrenset *usually* lists only subdirs, this is
1237            # actually up to the matcher and may have some files in the set().
1238            # If visit == 'this', we should obviously look at the files in this
1239            # directory; if visit is a set, and fn is in it, we should inspect
1240            # fn (but no need to inspect things not in the set).
1241            if visit != b'this' and fn not in visit:
1242                continue
1243            fullp = self._subpath(fn)
1244            # visitchildrenset isn't perfect, we still need to call the regular
1245            # matcher code to further filter results.
1246            if not match(fullp):
1247                continue
1248            ret._files[fn] = self._files[fn]
1249            if fn in self._flags:
1250                ret._flags[fn] = self._flags[fn]
1251
1252        visit = self._loadchildrensetlazy(visit)
1253        for dir, subm in pycompat.iteritems(self._dirs):
1254            if visit and dir[:-1] not in visit:
1255                continue
1256            m = subm._matches_inner(match)
1257            if not m._isempty():
1258                ret._dirs[dir] = m
1259
1260        if not ret._isempty():
1261            ret._dirty = True
1262        return ret
1263
1264    def fastdelta(self, base, changes):
1265        raise FastdeltaUnavailable()
1266
1267    def diff(self, m2, match=None, clean=False):
1268        """Finds changes between the current manifest and m2.
1269
1270        Args:
1271          m2: the manifest to which this manifest should be compared.
1272          clean: if true, include files unchanged between these manifests
1273                 with a None value in the returned dictionary.
1274
1275        The result is returned as a dict with filename as key and
1276        values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
1277        nodeid in the current/other manifest and fl1/fl2 is the flag
1278        in the current/other manifest. Where the file does not exist,
1279        the nodeid will be None and the flags will be the empty
1280        string.
1281        """
1282        if match and not match.always():
1283            m1 = self._matches(match)
1284            m2 = m2._matches(match)
1285            return m1.diff(m2, clean=clean)
1286        result = {}
1287        emptytree = treemanifest(self.nodeconstants)
1288
1289        def _iterativediff(t1, t2, stack):
1290            """compares two tree manifests and append new tree-manifests which
1291            needs to be compared to stack"""
1292            if t1._node == t2._node and not t1._dirty and not t2._dirty:
1293                return
1294            t1._load()
1295            t2._load()
1296            self._loaddifflazy(t1, t2)
1297
1298            for d, m1 in pycompat.iteritems(t1._dirs):
1299                m2 = t2._dirs.get(d, emptytree)
1300                stack.append((m1, m2))
1301
1302            for d, m2 in pycompat.iteritems(t2._dirs):
1303                if d not in t1._dirs:
1304                    stack.append((emptytree, m2))
1305
1306            for fn, n1 in pycompat.iteritems(t1._files):
1307                fl1 = t1._flags.get(fn, b'')
1308                n2 = t2._files.get(fn, None)
1309                fl2 = t2._flags.get(fn, b'')
1310                if n1 != n2 or fl1 != fl2:
1311                    result[t1._subpath(fn)] = ((n1, fl1), (n2, fl2))
1312                elif clean:
1313                    result[t1._subpath(fn)] = None
1314
1315            for fn, n2 in pycompat.iteritems(t2._files):
1316                if fn not in t1._files:
1317                    fl2 = t2._flags.get(fn, b'')
1318                    result[t2._subpath(fn)] = ((None, b''), (n2, fl2))
1319
1320        stackls = []
1321        _iterativediff(self, m2, stackls)
1322        while stackls:
1323            t1, t2 = stackls.pop()
1324            # stackls is populated in the function call
1325            _iterativediff(t1, t2, stackls)
1326        return result
1327
1328    def unmodifiedsince(self, m2):
1329        return not self._dirty and not m2._dirty and self._node == m2._node
1330
1331    def parse(self, text, readsubtree):
1332        selflazy = self._lazydirs
1333        for f, n, fl in _parse(self._nodelen, text):
1334            if fl == b't':
1335                f = f + b'/'
1336                # False below means "doesn't need to be copied" and can use the
1337                # cached value from readsubtree directly.
1338                selflazy[f] = (n, readsubtree, False)
1339            elif b'/' in f:
1340                # This is a flat manifest, so use __setitem__ and setflag rather
1341                # than assigning directly to _files and _flags, so we can
1342                # assign a path in a subdirectory, and to mark dirty (compared
1343                # to nullid).
1344                self[f] = n
1345                if fl:
1346                    self.setflag(f, fl)
1347            else:
1348                # Assigning to _files and _flags avoids marking as dirty,
1349                # and should be a little faster.
1350                self._files[f] = n
1351                if fl:
1352                    self._flags[f] = fl
1353
1354    def text(self):
1355        """Get the full data of this manifest as a bytestring."""
1356        self._load()
1357        return _text(self.iterentries())
1358
1359    def dirtext(self):
1360        """Get the full data of this directory as a bytestring. Make sure that
1361        any submanifests have been written first, so their nodeids are correct.
1362        """
1363        self._load()
1364        flags = self.flags
1365        lazydirs = [
1366            (d[:-1], v[0], b't') for d, v in pycompat.iteritems(self._lazydirs)
1367        ]
1368        dirs = [(d[:-1], self._dirs[d]._node, b't') for d in self._dirs]
1369        files = [(f, self._files[f], flags(f)) for f in self._files]
1370        return _text(sorted(dirs + files + lazydirs))
1371
1372    def read(self, gettext, readsubtree):
1373        def _load_for_read(s):
1374            s.parse(gettext(), readsubtree)
1375            s._dirty = False
1376
1377        self._loadfunc = _load_for_read
1378
1379    def writesubtrees(self, m1, m2, writesubtree, match):
1380        self._load()  # for consistency; should never have any effect here
1381        m1._load()
1382        m2._load()
1383        emptytree = treemanifest(self.nodeconstants)
1384
1385        def getnode(m, d):
1386            ld = m._lazydirs.get(d)
1387            if ld:
1388                return ld[0]
1389            return m._dirs.get(d, emptytree)._node
1390
1391        # let's skip investigating things that `match` says we do not need.
1392        visit = match.visitchildrenset(self._dir[:-1])
1393        visit = self._loadchildrensetlazy(visit)
1394        if visit == b'this' or visit == b'all':
1395            visit = None
1396        for d, subm in pycompat.iteritems(self._dirs):
1397            if visit and d[:-1] not in visit:
1398                continue
1399            subp1 = getnode(m1, d)
1400            subp2 = getnode(m2, d)
1401            if subp1 == self.nodeconstants.nullid:
1402                subp1, subp2 = subp2, subp1
1403            writesubtree(subm, subp1, subp2, match)
1404
1405    def walksubtrees(self, matcher=None):
1406        """Returns an iterator of the subtrees of this manifest, including this
1407        manifest itself.
1408
1409        If `matcher` is provided, it only returns subtrees that match.
1410        """
1411        if matcher and not matcher.visitdir(self._dir[:-1]):
1412            return
1413        if not matcher or matcher(self._dir[:-1]):
1414            yield self
1415
1416        self._load()
1417        # OPT: use visitchildrenset to avoid loading everything.
1418        self._loadalllazy()
1419        for d, subm in pycompat.iteritems(self._dirs):
1420            for subtree in subm.walksubtrees(matcher=matcher):
1421                yield subtree
1422
1423
1424class manifestfulltextcache(util.lrucachedict):
1425    """File-backed LRU cache for the manifest cache
1426
1427    File consists of entries, up to EOF:
1428
1429    - 20 bytes node, 4 bytes length, <length> manifest data
1430
1431    These are written in reverse cache order (oldest to newest).
1432
1433    """
1434
1435    _file = b'manifestfulltextcache'
1436
1437    def __init__(self, max):
1438        super(manifestfulltextcache, self).__init__(max)
1439        self._dirty = False
1440        self._read = False
1441        self._opener = None
1442
1443    def read(self):
1444        if self._read or self._opener is None:
1445            return
1446
1447        try:
1448            with self._opener(self._file) as fp:
1449                set = super(manifestfulltextcache, self).__setitem__
1450                # ignore trailing data, this is a cache, corruption is skipped
1451                while True:
1452                    # TODO do we need to do work here for sha1 portability?
1453                    node = fp.read(20)
1454                    if len(node) < 20:
1455                        break
1456                    try:
1457                        size = struct.unpack(b'>L', fp.read(4))[0]
1458                    except struct.error:
1459                        break
1460                    value = bytearray(fp.read(size))
1461                    if len(value) != size:
1462                        break
1463                    set(node, value)
1464        except IOError:
1465            # the file is allowed to be missing
1466            pass
1467
1468        self._read = True
1469        self._dirty = False
1470
1471    def write(self):
1472        if not self._dirty or self._opener is None:
1473            return
1474        # rotate backwards to the first used node
1475        try:
1476            with self._opener(
1477                self._file, b'w', atomictemp=True, checkambig=True
1478            ) as fp:
1479                node = self._head.prev
1480                while True:
1481                    if node.key in self._cache:
1482                        fp.write(node.key)
1483                        fp.write(struct.pack(b'>L', len(node.value)))
1484                        fp.write(node.value)
1485                    if node is self._head:
1486                        break
1487                    node = node.prev
1488        except IOError:
1489            # We could not write the cache (eg: permission error)
1490            # the content can be missing.
1491            #
1492            # We could try harder and see if we could recreate a wcache
1493            # directory were we coudl write too.
1494            #
1495            # XXX the error pass silently, having some way to issue an error
1496            # log `ui.log` would be nice.
1497            pass
1498
1499    def __len__(self):
1500        if not self._read:
1501            self.read()
1502        return super(manifestfulltextcache, self).__len__()
1503
1504    def __contains__(self, k):
1505        if not self._read:
1506            self.read()
1507        return super(manifestfulltextcache, self).__contains__(k)
1508
1509    def __iter__(self):
1510        if not self._read:
1511            self.read()
1512        return super(manifestfulltextcache, self).__iter__()
1513
1514    def __getitem__(self, k):
1515        if not self._read:
1516            self.read()
1517        # the cache lru order can change on read
1518        setdirty = self._cache.get(k) is not self._head
1519        value = super(manifestfulltextcache, self).__getitem__(k)
1520        if setdirty:
1521            self._dirty = True
1522        return value
1523
1524    def __setitem__(self, k, v):
1525        if not self._read:
1526            self.read()
1527        super(manifestfulltextcache, self).__setitem__(k, v)
1528        self._dirty = True
1529
1530    def __delitem__(self, k):
1531        if not self._read:
1532            self.read()
1533        super(manifestfulltextcache, self).__delitem__(k)
1534        self._dirty = True
1535
1536    def get(self, k, default=None):
1537        if not self._read:
1538            self.read()
1539        return super(manifestfulltextcache, self).get(k, default=default)
1540
1541    def clear(self, clear_persisted_data=False):
1542        super(manifestfulltextcache, self).clear()
1543        if clear_persisted_data:
1544            self._dirty = True
1545            self.write()
1546        self._read = False
1547
1548
1549# and upper bound of what we expect from compression
1550# (real live value seems to be "3")
1551MAXCOMPRESSION = 3
1552
1553
1554class FastdeltaUnavailable(Exception):
1555    """Exception raised when fastdelta isn't usable on a manifest."""
1556
1557
1558@interfaceutil.implementer(repository.imanifeststorage)
1559class manifestrevlog(object):
1560    """A revlog that stores manifest texts. This is responsible for caching the
1561    full-text manifest contents.
1562    """
1563
1564    def __init__(
1565        self,
1566        nodeconstants,
1567        opener,
1568        tree=b'',
1569        dirlogcache=None,
1570        treemanifest=False,
1571    ):
1572        """Constructs a new manifest revlog
1573
1574        `indexfile` - used by extensions to have two manifests at once, like
1575        when transitioning between flatmanifeset and treemanifests.
1576
1577        `treemanifest` - used to indicate this is a tree manifest revlog. Opener
1578        options can also be used to make this a tree manifest revlog. The opener
1579        option takes precedence, so if it is set to True, we ignore whatever
1580        value is passed in to the constructor.
1581        """
1582        self.nodeconstants = nodeconstants
1583        # During normal operations, we expect to deal with not more than four
1584        # revs at a time (such as during commit --amend). When rebasing large
1585        # stacks of commits, the number can go up, hence the config knob below.
1586        cachesize = 4
1587        optiontreemanifest = False
1588        opts = getattr(opener, 'options', None)
1589        if opts is not None:
1590            cachesize = opts.get(b'manifestcachesize', cachesize)
1591            optiontreemanifest = opts.get(b'treemanifest', False)
1592
1593        self._treeondisk = optiontreemanifest or treemanifest
1594
1595        self._fulltextcache = manifestfulltextcache(cachesize)
1596
1597        if tree:
1598            assert self._treeondisk, b'opts is %r' % opts
1599
1600        radix = b'00manifest'
1601        if tree:
1602            radix = b"meta/" + tree + radix
1603
1604        self.tree = tree
1605
1606        # The dirlogcache is kept on the root manifest log
1607        if tree:
1608            self._dirlogcache = dirlogcache
1609        else:
1610            self._dirlogcache = {b'': self}
1611
1612        self._revlog = revlog.revlog(
1613            opener,
1614            target=(revlog_constants.KIND_MANIFESTLOG, self.tree),
1615            radix=radix,
1616            # only root indexfile is cached
1617            checkambig=not bool(tree),
1618            mmaplargeindex=True,
1619            upperboundcomp=MAXCOMPRESSION,
1620            persistentnodemap=opener.options.get(b'persistent-nodemap', False),
1621        )
1622
1623        self.index = self._revlog.index
1624        self._generaldelta = self._revlog._generaldelta
1625
1626    def _setupmanifestcachehooks(self, repo):
1627        """Persist the manifestfulltextcache on lock release"""
1628        if not util.safehasattr(repo, b'_wlockref'):
1629            return
1630
1631        self._fulltextcache._opener = repo.wcachevfs
1632        if repo._currentlock(repo._wlockref) is None:
1633            return
1634
1635        reporef = weakref.ref(repo)
1636        manifestrevlogref = weakref.ref(self)
1637
1638        def persistmanifestcache(success):
1639            # Repo is in an unknown state, do not persist.
1640            if not success:
1641                return
1642
1643            repo = reporef()
1644            self = manifestrevlogref()
1645            if repo is None or self is None:
1646                return
1647            if repo.manifestlog.getstorage(b'') is not self:
1648                # there's a different manifest in play now, abort
1649                return
1650            self._fulltextcache.write()
1651
1652        repo._afterlock(persistmanifestcache)
1653
1654    @property
1655    def fulltextcache(self):
1656        return self._fulltextcache
1657
1658    def clearcaches(self, clear_persisted_data=False):
1659        self._revlog.clearcaches()
1660        self._fulltextcache.clear(clear_persisted_data=clear_persisted_data)
1661        self._dirlogcache = {self.tree: self}
1662
1663    def dirlog(self, d):
1664        if d:
1665            assert self._treeondisk
1666        if d not in self._dirlogcache:
1667            mfrevlog = manifestrevlog(
1668                self.nodeconstants,
1669                self.opener,
1670                d,
1671                self._dirlogcache,
1672                treemanifest=self._treeondisk,
1673            )
1674            self._dirlogcache[d] = mfrevlog
1675        return self._dirlogcache[d]
1676
1677    def add(
1678        self,
1679        m,
1680        transaction,
1681        link,
1682        p1,
1683        p2,
1684        added,
1685        removed,
1686        readtree=None,
1687        match=None,
1688    ):
1689        """add some manifest entry in to the manifest log
1690
1691        input:
1692
1693          m:           the manifest dict we want to store
1694          transaction: the open transaction
1695          p1:          manifest-node of p1
1696          p2:          manifest-node of p2
1697          added:       file added/changed compared to parent
1698          removed:     file removed compared to parent
1699
1700        tree manifest input:
1701
1702          readtree:    a function to read a subtree
1703          match:       a filematcher for the subpart of the tree manifest
1704        """
1705        try:
1706            if p1 not in self.fulltextcache:
1707                raise FastdeltaUnavailable()
1708            # If our first parent is in the manifest cache, we can
1709            # compute a delta here using properties we know about the
1710            # manifest up-front, which may save time later for the
1711            # revlog layer.
1712
1713            _checkforbidden(added)
1714            # combine the changed lists into one sorted iterator
1715            work = heapq.merge(
1716                [(x, False) for x in sorted(added)],
1717                [(x, True) for x in sorted(removed)],
1718            )
1719
1720            arraytext, deltatext = m.fastdelta(self.fulltextcache[p1], work)
1721            cachedelta = self._revlog.rev(p1), deltatext
1722            text = util.buffer(arraytext)
1723            rev = self._revlog.addrevision(
1724                text, transaction, link, p1, p2, cachedelta
1725            )
1726            n = self._revlog.node(rev)
1727        except FastdeltaUnavailable:
1728            # The first parent manifest isn't already loaded or the
1729            # manifest implementation doesn't support fastdelta, so
1730            # we'll just encode a fulltext of the manifest and pass
1731            # that through to the revlog layer, and let it handle the
1732            # delta process.
1733            if self._treeondisk:
1734                assert readtree, b"readtree must be set for treemanifest writes"
1735                assert match, b"match must be specified for treemanifest writes"
1736                m1 = readtree(self.tree, p1)
1737                m2 = readtree(self.tree, p2)
1738                n = self._addtree(
1739                    m, transaction, link, m1, m2, readtree, match=match
1740                )
1741                arraytext = None
1742            else:
1743                text = m.text()
1744                rev = self._revlog.addrevision(text, transaction, link, p1, p2)
1745                n = self._revlog.node(rev)
1746                arraytext = bytearray(text)
1747
1748        if arraytext is not None:
1749            self.fulltextcache[n] = arraytext
1750
1751        return n
1752
1753    def _addtree(self, m, transaction, link, m1, m2, readtree, match):
1754        # If the manifest is unchanged compared to one parent,
1755        # don't write a new revision
1756        if self.tree != b'' and (
1757            m.unmodifiedsince(m1) or m.unmodifiedsince(m2)
1758        ):
1759            return m.node()
1760
1761        def writesubtree(subm, subp1, subp2, match):
1762            sublog = self.dirlog(subm.dir())
1763            sublog.add(
1764                subm,
1765                transaction,
1766                link,
1767                subp1,
1768                subp2,
1769                None,
1770                None,
1771                readtree=readtree,
1772                match=match,
1773            )
1774
1775        m.writesubtrees(m1, m2, writesubtree, match)
1776        text = m.dirtext()
1777        n = None
1778        if self.tree != b'':
1779            # Double-check whether contents are unchanged to one parent
1780            if text == m1.dirtext():
1781                n = m1.node()
1782            elif text == m2.dirtext():
1783                n = m2.node()
1784
1785        if not n:
1786            rev = self._revlog.addrevision(
1787                text, transaction, link, m1.node(), m2.node()
1788            )
1789            n = self._revlog.node(rev)
1790
1791        # Save nodeid so parent manifest can calculate its nodeid
1792        m.setnode(n)
1793        return n
1794
1795    def __len__(self):
1796        return len(self._revlog)
1797
1798    def __iter__(self):
1799        return self._revlog.__iter__()
1800
1801    def rev(self, node):
1802        return self._revlog.rev(node)
1803
1804    def node(self, rev):
1805        return self._revlog.node(rev)
1806
1807    def lookup(self, value):
1808        return self._revlog.lookup(value)
1809
1810    def parentrevs(self, rev):
1811        return self._revlog.parentrevs(rev)
1812
1813    def parents(self, node):
1814        return self._revlog.parents(node)
1815
1816    def linkrev(self, rev):
1817        return self._revlog.linkrev(rev)
1818
1819    def checksize(self):
1820        return self._revlog.checksize()
1821
1822    def revision(self, node, _df=None, raw=False):
1823        return self._revlog.revision(node, _df=_df, raw=raw)
1824
1825    def rawdata(self, node, _df=None):
1826        return self._revlog.rawdata(node, _df=_df)
1827
1828    def revdiff(self, rev1, rev2):
1829        return self._revlog.revdiff(rev1, rev2)
1830
1831    def cmp(self, node, text):
1832        return self._revlog.cmp(node, text)
1833
1834    def deltaparent(self, rev):
1835        return self._revlog.deltaparent(rev)
1836
1837    def emitrevisions(
1838        self,
1839        nodes,
1840        nodesorder=None,
1841        revisiondata=False,
1842        assumehaveparentrevisions=False,
1843        deltamode=repository.CG_DELTAMODE_STD,
1844        sidedata_helpers=None,
1845    ):
1846        return self._revlog.emitrevisions(
1847            nodes,
1848            nodesorder=nodesorder,
1849            revisiondata=revisiondata,
1850            assumehaveparentrevisions=assumehaveparentrevisions,
1851            deltamode=deltamode,
1852            sidedata_helpers=sidedata_helpers,
1853        )
1854
1855    def addgroup(
1856        self,
1857        deltas,
1858        linkmapper,
1859        transaction,
1860        alwayscache=False,
1861        addrevisioncb=None,
1862        duplicaterevisioncb=None,
1863    ):
1864        return self._revlog.addgroup(
1865            deltas,
1866            linkmapper,
1867            transaction,
1868            alwayscache=alwayscache,
1869            addrevisioncb=addrevisioncb,
1870            duplicaterevisioncb=duplicaterevisioncb,
1871        )
1872
1873    def rawsize(self, rev):
1874        return self._revlog.rawsize(rev)
1875
1876    def getstrippoint(self, minlink):
1877        return self._revlog.getstrippoint(minlink)
1878
1879    def strip(self, minlink, transaction):
1880        return self._revlog.strip(minlink, transaction)
1881
1882    def files(self):
1883        return self._revlog.files()
1884
1885    def clone(self, tr, destrevlog, **kwargs):
1886        if not isinstance(destrevlog, manifestrevlog):
1887            raise error.ProgrammingError(b'expected manifestrevlog to clone()')
1888
1889        return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
1890
1891    def storageinfo(
1892        self,
1893        exclusivefiles=False,
1894        sharedfiles=False,
1895        revisionscount=False,
1896        trackedsize=False,
1897        storedsize=False,
1898    ):
1899        return self._revlog.storageinfo(
1900            exclusivefiles=exclusivefiles,
1901            sharedfiles=sharedfiles,
1902            revisionscount=revisionscount,
1903            trackedsize=trackedsize,
1904            storedsize=storedsize,
1905        )
1906
1907    @property
1908    def opener(self):
1909        return self._revlog.opener
1910
1911    @opener.setter
1912    def opener(self, value):
1913        self._revlog.opener = value
1914
1915
1916@interfaceutil.implementer(repository.imanifestlog)
1917class manifestlog(object):
1918    """A collection class representing the collection of manifest snapshots
1919    referenced by commits in the repository.
1920
1921    In this situation, 'manifest' refers to the abstract concept of a snapshot
1922    of the list of files in the given commit. Consumers of the output of this
1923    class do not care about the implementation details of the actual manifests
1924    they receive (i.e. tree or flat or lazily loaded, etc)."""
1925
1926    def __init__(self, opener, repo, rootstore, narrowmatch):
1927        self.nodeconstants = repo.nodeconstants
1928        usetreemanifest = False
1929        cachesize = 4
1930
1931        opts = getattr(opener, 'options', None)
1932        if opts is not None:
1933            usetreemanifest = opts.get(b'treemanifest', usetreemanifest)
1934            cachesize = opts.get(b'manifestcachesize', cachesize)
1935
1936        self._treemanifests = usetreemanifest
1937
1938        self._rootstore = rootstore
1939        self._rootstore._setupmanifestcachehooks(repo)
1940        self._narrowmatch = narrowmatch
1941
1942        # A cache of the manifestctx or treemanifestctx for each directory
1943        self._dirmancache = {}
1944        self._dirmancache[b''] = util.lrucachedict(cachesize)
1945
1946        self._cachesize = cachesize
1947
1948    def __getitem__(self, node):
1949        """Retrieves the manifest instance for the given node. Throws a
1950        LookupError if not found.
1951        """
1952        return self.get(b'', node)
1953
1954    def get(self, tree, node, verify=True):
1955        """Retrieves the manifest instance for the given node. Throws a
1956        LookupError if not found.
1957
1958        `verify` - if True an exception will be thrown if the node is not in
1959                   the revlog
1960        """
1961        if node in self._dirmancache.get(tree, ()):
1962            return self._dirmancache[tree][node]
1963
1964        if not self._narrowmatch.always():
1965            if not self._narrowmatch.visitdir(tree[:-1]):
1966                return excludeddirmanifestctx(self.nodeconstants, tree, node)
1967        if tree:
1968            if self._rootstore._treeondisk:
1969                if verify:
1970                    # Side-effect is LookupError is raised if node doesn't
1971                    # exist.
1972                    self.getstorage(tree).rev(node)
1973
1974                m = treemanifestctx(self, tree, node)
1975            else:
1976                raise error.Abort(
1977                    _(
1978                        b"cannot ask for manifest directory '%s' in a flat "
1979                        b"manifest"
1980                    )
1981                    % tree
1982                )
1983        else:
1984            if verify:
1985                # Side-effect is LookupError is raised if node doesn't exist.
1986                self._rootstore.rev(node)
1987
1988            if self._treemanifests:
1989                m = treemanifestctx(self, b'', node)
1990            else:
1991                m = manifestctx(self, node)
1992
1993        if node != self.nodeconstants.nullid:
1994            mancache = self._dirmancache.get(tree)
1995            if not mancache:
1996                mancache = util.lrucachedict(self._cachesize)
1997                self._dirmancache[tree] = mancache
1998            mancache[node] = m
1999        return m
2000
2001    def getstorage(self, tree):
2002        return self._rootstore.dirlog(tree)
2003
2004    def clearcaches(self, clear_persisted_data=False):
2005        self._dirmancache.clear()
2006        self._rootstore.clearcaches(clear_persisted_data=clear_persisted_data)
2007
2008    def rev(self, node):
2009        return self._rootstore.rev(node)
2010
2011    def update_caches(self, transaction):
2012        return self._rootstore._revlog.update_caches(transaction=transaction)
2013
2014
2015@interfaceutil.implementer(repository.imanifestrevisionwritable)
2016class memmanifestctx(object):
2017    def __init__(self, manifestlog):
2018        self._manifestlog = manifestlog
2019        self._manifestdict = manifestdict(manifestlog.nodeconstants.nodelen)
2020
2021    def _storage(self):
2022        return self._manifestlog.getstorage(b'')
2023
2024    def copy(self):
2025        memmf = memmanifestctx(self._manifestlog)
2026        memmf._manifestdict = self.read().copy()
2027        return memmf
2028
2029    def read(self):
2030        return self._manifestdict
2031
2032    def write(self, transaction, link, p1, p2, added, removed, match=None):
2033        return self._storage().add(
2034            self._manifestdict,
2035            transaction,
2036            link,
2037            p1,
2038            p2,
2039            added,
2040            removed,
2041            match=match,
2042        )
2043
2044
2045@interfaceutil.implementer(repository.imanifestrevisionstored)
2046class manifestctx(object):
2047    """A class representing a single revision of a manifest, including its
2048    contents, its parent revs, and its linkrev.
2049    """
2050
2051    def __init__(self, manifestlog, node):
2052        self._manifestlog = manifestlog
2053        self._data = None
2054
2055        self._node = node
2056
2057        # TODO: We eventually want p1, p2, and linkrev exposed on this class,
2058        # but let's add it later when something needs it and we can load it
2059        # lazily.
2060        # self.p1, self.p2 = store.parents(node)
2061        # rev = store.rev(node)
2062        # self.linkrev = store.linkrev(rev)
2063
2064    def _storage(self):
2065        return self._manifestlog.getstorage(b'')
2066
2067    def node(self):
2068        return self._node
2069
2070    def copy(self):
2071        memmf = memmanifestctx(self._manifestlog)
2072        memmf._manifestdict = self.read().copy()
2073        return memmf
2074
2075    @propertycache
2076    def parents(self):
2077        return self._storage().parents(self._node)
2078
2079    def read(self):
2080        if self._data is None:
2081            nc = self._manifestlog.nodeconstants
2082            if self._node == nc.nullid:
2083                self._data = manifestdict(nc.nodelen)
2084            else:
2085                store = self._storage()
2086                if self._node in store.fulltextcache:
2087                    text = pycompat.bytestr(store.fulltextcache[self._node])
2088                else:
2089                    text = store.revision(self._node)
2090                    arraytext = bytearray(text)
2091                    store.fulltextcache[self._node] = arraytext
2092                self._data = manifestdict(nc.nodelen, text)
2093        return self._data
2094
2095    def readfast(self, shallow=False):
2096        """Calls either readdelta or read, based on which would be less work.
2097        readdelta is called if the delta is against the p1, and therefore can be
2098        read quickly.
2099
2100        If `shallow` is True, nothing changes since this is a flat manifest.
2101        """
2102        store = self._storage()
2103        r = store.rev(self._node)
2104        deltaparent = store.deltaparent(r)
2105        if deltaparent != nullrev and deltaparent in store.parentrevs(r):
2106            return self.readdelta()
2107        return self.read()
2108
2109    def readdelta(self, shallow=False):
2110        """Returns a manifest containing just the entries that are present
2111        in this manifest, but not in its p1 manifest. This is efficient to read
2112        if the revlog delta is already p1.
2113
2114        Changing the value of `shallow` has no effect on flat manifests.
2115        """
2116        store = self._storage()
2117        r = store.rev(self._node)
2118        d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
2119        return manifestdict(store.nodeconstants.nodelen, d)
2120
2121    def find(self, key):
2122        return self.read().find(key)
2123
2124
2125@interfaceutil.implementer(repository.imanifestrevisionwritable)
2126class memtreemanifestctx(object):
2127    def __init__(self, manifestlog, dir=b''):
2128        self._manifestlog = manifestlog
2129        self._dir = dir
2130        self._treemanifest = treemanifest(manifestlog.nodeconstants)
2131
2132    def _storage(self):
2133        return self._manifestlog.getstorage(b'')
2134
2135    def copy(self):
2136        memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
2137        memmf._treemanifest = self._treemanifest.copy()
2138        return memmf
2139
2140    def read(self):
2141        return self._treemanifest
2142
2143    def write(self, transaction, link, p1, p2, added, removed, match=None):
2144        def readtree(dir, node):
2145            return self._manifestlog.get(dir, node).read()
2146
2147        return self._storage().add(
2148            self._treemanifest,
2149            transaction,
2150            link,
2151            p1,
2152            p2,
2153            added,
2154            removed,
2155            readtree=readtree,
2156            match=match,
2157        )
2158
2159
2160@interfaceutil.implementer(repository.imanifestrevisionstored)
2161class treemanifestctx(object):
2162    def __init__(self, manifestlog, dir, node):
2163        self._manifestlog = manifestlog
2164        self._dir = dir
2165        self._data = None
2166
2167        self._node = node
2168
2169        # TODO: Load p1/p2/linkrev lazily. They need to be lazily loaded so that
2170        # we can instantiate treemanifestctx objects for directories we don't
2171        # have on disk.
2172        # self.p1, self.p2 = store.parents(node)
2173        # rev = store.rev(node)
2174        # self.linkrev = store.linkrev(rev)
2175
2176    def _storage(self):
2177        narrowmatch = self._manifestlog._narrowmatch
2178        if not narrowmatch.always():
2179            if not narrowmatch.visitdir(self._dir[:-1]):
2180                return excludedmanifestrevlog(
2181                    self._manifestlog.nodeconstants, self._dir
2182                )
2183        return self._manifestlog.getstorage(self._dir)
2184
2185    def read(self):
2186        if self._data is None:
2187            store = self._storage()
2188            if self._node == self._manifestlog.nodeconstants.nullid:
2189                self._data = treemanifest(self._manifestlog.nodeconstants)
2190            # TODO accessing non-public API
2191            elif store._treeondisk:
2192                m = treemanifest(self._manifestlog.nodeconstants, dir=self._dir)
2193
2194                def gettext():
2195                    return store.revision(self._node)
2196
2197                def readsubtree(dir, subm):
2198                    # Set verify to False since we need to be able to create
2199                    # subtrees for trees that don't exist on disk.
2200                    return self._manifestlog.get(dir, subm, verify=False).read()
2201
2202                m.read(gettext, readsubtree)
2203                m.setnode(self._node)
2204                self._data = m
2205            else:
2206                if self._node in store.fulltextcache:
2207                    text = pycompat.bytestr(store.fulltextcache[self._node])
2208                else:
2209                    text = store.revision(self._node)
2210                    arraytext = bytearray(text)
2211                    store.fulltextcache[self._node] = arraytext
2212                self._data = treemanifest(
2213                    self._manifestlog.nodeconstants, dir=self._dir, text=text
2214                )
2215
2216        return self._data
2217
2218    def node(self):
2219        return self._node
2220
2221    def copy(self):
2222        memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
2223        memmf._treemanifest = self.read().copy()
2224        return memmf
2225
2226    @propertycache
2227    def parents(self):
2228        return self._storage().parents(self._node)
2229
2230    def readdelta(self, shallow=False):
2231        """Returns a manifest containing just the entries that are present
2232        in this manifest, but not in its p1 manifest. This is efficient to read
2233        if the revlog delta is already p1.
2234
2235        If `shallow` is True, this will read the delta for this directory,
2236        without recursively reading subdirectory manifests. Instead, any
2237        subdirectory entry will be reported as it appears in the manifest, i.e.
2238        the subdirectory will be reported among files and distinguished only by
2239        its 't' flag.
2240        """
2241        store = self._storage()
2242        if shallow:
2243            r = store.rev(self._node)
2244            d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
2245            return manifestdict(store.nodeconstants.nodelen, d)
2246        else:
2247            # Need to perform a slow delta
2248            r0 = store.deltaparent(store.rev(self._node))
2249            m0 = self._manifestlog.get(self._dir, store.node(r0)).read()
2250            m1 = self.read()
2251            md = treemanifest(self._manifestlog.nodeconstants, dir=self._dir)
2252            for f, ((n0, fl0), (n1, fl1)) in pycompat.iteritems(m0.diff(m1)):
2253                if n1:
2254                    md[f] = n1
2255                    if fl1:
2256                        md.setflag(f, fl1)
2257            return md
2258
2259    def readfast(self, shallow=False):
2260        """Calls either readdelta or read, based on which would be less work.
2261        readdelta is called if the delta is against the p1, and therefore can be
2262        read quickly.
2263
2264        If `shallow` is True, it only returns the entries from this manifest,
2265        and not any submanifests.
2266        """
2267        store = self._storage()
2268        r = store.rev(self._node)
2269        deltaparent = store.deltaparent(r)
2270        if deltaparent != nullrev and deltaparent in store.parentrevs(r):
2271            return self.readdelta(shallow=shallow)
2272
2273        if shallow:
2274            return manifestdict(
2275                store.nodeconstants.nodelen, store.revision(self._node)
2276            )
2277        else:
2278            return self.read()
2279
2280    def find(self, key):
2281        return self.read().find(key)
2282
2283
2284class excludeddir(treemanifest):
2285    """Stand-in for a directory that is excluded from the repository.
2286
2287    With narrowing active on a repository that uses treemanifests,
2288    some of the directory revlogs will be excluded from the resulting
2289    clone. This is a huge storage win for clients, but means we need
2290    some sort of pseudo-manifest to surface to internals so we can
2291    detect a merge conflict outside the narrowspec. That's what this
2292    class is: it stands in for a directory whose node is known, but
2293    whose contents are unknown.
2294    """
2295
2296    def __init__(self, nodeconstants, dir, node):
2297        super(excludeddir, self).__init__(nodeconstants, dir)
2298        self._node = node
2299        # Add an empty file, which will be included by iterators and such,
2300        # appearing as the directory itself (i.e. something like "dir/")
2301        self._files[b''] = node
2302        self._flags[b''] = b't'
2303
2304    # Manifests outside the narrowspec should never be modified, so avoid
2305    # copying. This makes a noticeable difference when there are very many
2306    # directories outside the narrowspec. Also, it makes sense for the copy to
2307    # be of the same type as the original, which would not happen with the
2308    # super type's copy().
2309    def copy(self):
2310        return self
2311
2312
2313class excludeddirmanifestctx(treemanifestctx):
2314    """context wrapper for excludeddir - see that docstring for rationale"""
2315
2316    def __init__(self, nodeconstants, dir, node):
2317        self.nodeconstants = nodeconstants
2318        self._dir = dir
2319        self._node = node
2320
2321    def read(self):
2322        return excludeddir(self.nodeconstants, self._dir, self._node)
2323
2324    def readfast(self, shallow=False):
2325        # special version of readfast since we don't have underlying storage
2326        return self.read()
2327
2328    def write(self, *args):
2329        raise error.ProgrammingError(
2330            b'attempt to write manifest from excluded dir %s' % self._dir
2331        )
2332
2333
2334class excludedmanifestrevlog(manifestrevlog):
2335    """Stand-in for excluded treemanifest revlogs.
2336
2337    When narrowing is active on a treemanifest repository, we'll have
2338    references to directories we can't see due to the revlog being
2339    skipped. This class exists to conform to the manifestrevlog
2340    interface for those directories and proactively prevent writes to
2341    outside the narrowspec.
2342    """
2343
2344    def __init__(self, nodeconstants, dir):
2345        self.nodeconstants = nodeconstants
2346        self._dir = dir
2347
2348    def __len__(self):
2349        raise error.ProgrammingError(
2350            b'attempt to get length of excluded dir %s' % self._dir
2351        )
2352
2353    def rev(self, node):
2354        raise error.ProgrammingError(
2355            b'attempt to get rev from excluded dir %s' % self._dir
2356        )
2357
2358    def linkrev(self, node):
2359        raise error.ProgrammingError(
2360            b'attempt to get linkrev from excluded dir %s' % self._dir
2361        )
2362
2363    def node(self, rev):
2364        raise error.ProgrammingError(
2365            b'attempt to get node from excluded dir %s' % self._dir
2366        )
2367
2368    def add(self, *args, **kwargs):
2369        # We should never write entries in dirlogs outside the narrow clone.
2370        # However, the method still gets called from writesubtree() in
2371        # _addtree(), so we need to handle it. We should possibly make that
2372        # avoid calling add() with a clean manifest (_dirty is always False
2373        # in excludeddir instances).
2374        pass
2375