1# context.py - changeset and file context objects for mercurial
2#
3# Copyright 2006, 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 filecmp
12import os
13import stat
14
15from .i18n import _
16from .node import (
17    hex,
18    nullrev,
19    short,
20)
21from .pycompat import (
22    getattr,
23    open,
24)
25from . import (
26    dagop,
27    encoding,
28    error,
29    fileset,
30    match as matchmod,
31    mergestate as mergestatemod,
32    metadata,
33    obsolete as obsmod,
34    patch,
35    pathutil,
36    phases,
37    pycompat,
38    repoview,
39    scmutil,
40    sparse,
41    subrepo,
42    subrepoutil,
43    util,
44)
45from .utils import (
46    dateutil,
47    stringutil,
48)
49
50propertycache = util.propertycache
51
52
53class basectx(object):
54    """A basectx object represents the common logic for its children:
55    changectx: read-only context that is already present in the repo,
56    workingctx: a context that represents the working directory and can
57                be committed,
58    memctx: a context that represents changes in-memory and can also
59            be committed."""
60
61    def __init__(self, repo):
62        self._repo = repo
63
64    def __bytes__(self):
65        return short(self.node())
66
67    __str__ = encoding.strmethod(__bytes__)
68
69    def __repr__(self):
70        return "<%s %s>" % (type(self).__name__, str(self))
71
72    def __eq__(self, other):
73        try:
74            return type(self) == type(other) and self._rev == other._rev
75        except AttributeError:
76            return False
77
78    def __ne__(self, other):
79        return not (self == other)
80
81    def __contains__(self, key):
82        return key in self._manifest
83
84    def __getitem__(self, key):
85        return self.filectx(key)
86
87    def __iter__(self):
88        return iter(self._manifest)
89
90    def _buildstatusmanifest(self, status):
91        """Builds a manifest that includes the given status results, if this is
92        a working copy context. For non-working copy contexts, it just returns
93        the normal manifest."""
94        return self.manifest()
95
96    def _matchstatus(self, other, match):
97        """This internal method provides a way for child objects to override the
98        match operator.
99        """
100        return match
101
102    def _buildstatus(
103        self, other, s, match, listignored, listclean, listunknown
104    ):
105        """build a status with respect to another context"""
106        # Load earliest manifest first for caching reasons. More specifically,
107        # if you have revisions 1000 and 1001, 1001 is probably stored as a
108        # delta against 1000. Thus, if you read 1000 first, we'll reconstruct
109        # 1000 and cache it so that when you read 1001, we just need to apply a
110        # delta to what's in the cache. So that's one full reconstruction + one
111        # delta application.
112        mf2 = None
113        if self.rev() is not None and self.rev() < other.rev():
114            mf2 = self._buildstatusmanifest(s)
115        mf1 = other._buildstatusmanifest(s)
116        if mf2 is None:
117            mf2 = self._buildstatusmanifest(s)
118
119        modified, added = [], []
120        removed = []
121        clean = []
122        deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
123        deletedset = set(deleted)
124        d = mf1.diff(mf2, match=match, clean=listclean)
125        for fn, value in pycompat.iteritems(d):
126            if fn in deletedset:
127                continue
128            if value is None:
129                clean.append(fn)
130                continue
131            (node1, flag1), (node2, flag2) = value
132            if node1 is None:
133                added.append(fn)
134            elif node2 is None:
135                removed.append(fn)
136            elif flag1 != flag2:
137                modified.append(fn)
138            elif node2 not in self._repo.nodeconstants.wdirfilenodeids:
139                # When comparing files between two commits, we save time by
140                # not comparing the file contents when the nodeids differ.
141                # Note that this means we incorrectly report a reverted change
142                # to a file as a modification.
143                modified.append(fn)
144            elif self[fn].cmp(other[fn]):
145                modified.append(fn)
146            else:
147                clean.append(fn)
148
149        if removed:
150            # need to filter files if they are already reported as removed
151            unknown = [
152                fn
153                for fn in unknown
154                if fn not in mf1 and (not match or match(fn))
155            ]
156            ignored = [
157                fn
158                for fn in ignored
159                if fn not in mf1 and (not match or match(fn))
160            ]
161            # if they're deleted, don't report them as removed
162            removed = [fn for fn in removed if fn not in deletedset]
163
164        return scmutil.status(
165            modified, added, removed, deleted, unknown, ignored, clean
166        )
167
168    @propertycache
169    def substate(self):
170        return subrepoutil.state(self, self._repo.ui)
171
172    def subrev(self, subpath):
173        return self.substate[subpath][1]
174
175    def rev(self):
176        return self._rev
177
178    def node(self):
179        return self._node
180
181    def hex(self):
182        return hex(self.node())
183
184    def manifest(self):
185        return self._manifest
186
187    def manifestctx(self):
188        return self._manifestctx
189
190    def repo(self):
191        return self._repo
192
193    def phasestr(self):
194        return phases.phasenames[self.phase()]
195
196    def mutable(self):
197        return self.phase() > phases.public
198
199    def matchfileset(self, cwd, expr, badfn=None):
200        return fileset.match(self, cwd, expr, badfn=badfn)
201
202    def obsolete(self):
203        """True if the changeset is obsolete"""
204        return self.rev() in obsmod.getrevs(self._repo, b'obsolete')
205
206    def extinct(self):
207        """True if the changeset is extinct"""
208        return self.rev() in obsmod.getrevs(self._repo, b'extinct')
209
210    def orphan(self):
211        """True if the changeset is not obsolete, but its ancestor is"""
212        return self.rev() in obsmod.getrevs(self._repo, b'orphan')
213
214    def phasedivergent(self):
215        """True if the changeset tries to be a successor of a public changeset
216
217        Only non-public and non-obsolete changesets may be phase-divergent.
218        """
219        return self.rev() in obsmod.getrevs(self._repo, b'phasedivergent')
220
221    def contentdivergent(self):
222        """Is a successor of a changeset with multiple possible successor sets
223
224        Only non-public and non-obsolete changesets may be content-divergent.
225        """
226        return self.rev() in obsmod.getrevs(self._repo, b'contentdivergent')
227
228    def isunstable(self):
229        """True if the changeset is either orphan, phase-divergent or
230        content-divergent"""
231        return self.orphan() or self.phasedivergent() or self.contentdivergent()
232
233    def instabilities(self):
234        """return the list of instabilities affecting this changeset.
235
236        Instabilities are returned as strings. possible values are:
237        - orphan,
238        - phase-divergent,
239        - content-divergent.
240        """
241        instabilities = []
242        if self.orphan():
243            instabilities.append(b'orphan')
244        if self.phasedivergent():
245            instabilities.append(b'phase-divergent')
246        if self.contentdivergent():
247            instabilities.append(b'content-divergent')
248        return instabilities
249
250    def parents(self):
251        """return contexts for each parent changeset"""
252        return self._parents
253
254    def p1(self):
255        return self._parents[0]
256
257    def p2(self):
258        parents = self._parents
259        if len(parents) == 2:
260            return parents[1]
261        return self._repo[nullrev]
262
263    def _fileinfo(self, path):
264        if '_manifest' in self.__dict__:
265            try:
266                return self._manifest.find(path)
267            except KeyError:
268                raise error.ManifestLookupError(
269                    self._node or b'None', path, _(b'not found in manifest')
270                )
271        if '_manifestdelta' in self.__dict__ or path in self.files():
272            if path in self._manifestdelta:
273                return (
274                    self._manifestdelta[path],
275                    self._manifestdelta.flags(path),
276                )
277        mfl = self._repo.manifestlog
278        try:
279            node, flag = mfl[self._changeset.manifest].find(path)
280        except KeyError:
281            raise error.ManifestLookupError(
282                self._node or b'None', path, _(b'not found in manifest')
283            )
284
285        return node, flag
286
287    def filenode(self, path):
288        return self._fileinfo(path)[0]
289
290    def flags(self, path):
291        try:
292            return self._fileinfo(path)[1]
293        except error.LookupError:
294            return b''
295
296    @propertycache
297    def _copies(self):
298        return metadata.computechangesetcopies(self)
299
300    def p1copies(self):
301        return self._copies[0]
302
303    def p2copies(self):
304        return self._copies[1]
305
306    def sub(self, path, allowcreate=True):
307        '''return a subrepo for the stored revision of path, never wdir()'''
308        return subrepo.subrepo(self, path, allowcreate=allowcreate)
309
310    def nullsub(self, path, pctx):
311        return subrepo.nullsubrepo(self, path, pctx)
312
313    def workingsub(self, path):
314        """return a subrepo for the stored revision, or wdir if this is a wdir
315        context.
316        """
317        return subrepo.subrepo(self, path, allowwdir=True)
318
319    def match(
320        self,
321        pats=None,
322        include=None,
323        exclude=None,
324        default=b'glob',
325        listsubrepos=False,
326        badfn=None,
327        cwd=None,
328    ):
329        r = self._repo
330        if not cwd:
331            cwd = r.getcwd()
332        return matchmod.match(
333            r.root,
334            cwd,
335            pats,
336            include,
337            exclude,
338            default,
339            auditor=r.nofsauditor,
340            ctx=self,
341            listsubrepos=listsubrepos,
342            badfn=badfn,
343        )
344
345    def diff(
346        self,
347        ctx2=None,
348        match=None,
349        changes=None,
350        opts=None,
351        losedatafn=None,
352        pathfn=None,
353        copy=None,
354        copysourcematch=None,
355        hunksfilterfn=None,
356    ):
357        """Returns a diff generator for the given contexts and matcher"""
358        if ctx2 is None:
359            ctx2 = self.p1()
360        if ctx2 is not None:
361            ctx2 = self._repo[ctx2]
362        return patch.diff(
363            self._repo,
364            ctx2,
365            self,
366            match=match,
367            changes=changes,
368            opts=opts,
369            losedatafn=losedatafn,
370            pathfn=pathfn,
371            copy=copy,
372            copysourcematch=copysourcematch,
373            hunksfilterfn=hunksfilterfn,
374        )
375
376    def dirs(self):
377        return self._manifest.dirs()
378
379    def hasdir(self, dir):
380        return self._manifest.hasdir(dir)
381
382    def status(
383        self,
384        other=None,
385        match=None,
386        listignored=False,
387        listclean=False,
388        listunknown=False,
389        listsubrepos=False,
390    ):
391        """return status of files between two nodes or node and working
392        directory.
393
394        If other is None, compare this node with working directory.
395
396        ctx1.status(ctx2) returns the status of change from ctx1 to ctx2
397
398        Returns a mercurial.scmutils.status object.
399
400        Data can be accessed using either tuple notation:
401
402            (modified, added, removed, deleted, unknown, ignored, clean)
403
404        or direct attribute access:
405
406            s.modified, s.added, ...
407        """
408
409        ctx1 = self
410        ctx2 = self._repo[other]
411
412        # This next code block is, admittedly, fragile logic that tests for
413        # reversing the contexts and wouldn't need to exist if it weren't for
414        # the fast (and common) code path of comparing the working directory
415        # with its first parent.
416        #
417        # What we're aiming for here is the ability to call:
418        #
419        # workingctx.status(parentctx)
420        #
421        # If we always built the manifest for each context and compared those,
422        # then we'd be done. But the special case of the above call means we
423        # just copy the manifest of the parent.
424        reversed = False
425        if not isinstance(ctx1, changectx) and isinstance(ctx2, changectx):
426            reversed = True
427            ctx1, ctx2 = ctx2, ctx1
428
429        match = self._repo.narrowmatch(match)
430        match = ctx2._matchstatus(ctx1, match)
431        r = scmutil.status([], [], [], [], [], [], [])
432        r = ctx2._buildstatus(
433            ctx1, r, match, listignored, listclean, listunknown
434        )
435
436        if reversed:
437            # Reverse added and removed. Clear deleted, unknown and ignored as
438            # these make no sense to reverse.
439            r = scmutil.status(
440                r.modified, r.removed, r.added, [], [], [], r.clean
441            )
442
443        if listsubrepos:
444            for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
445                try:
446                    rev2 = ctx2.subrev(subpath)
447                except KeyError:
448                    # A subrepo that existed in node1 was deleted between
449                    # node1 and node2 (inclusive). Thus, ctx2's substate
450                    # won't contain that subpath. The best we can do ignore it.
451                    rev2 = None
452                submatch = matchmod.subdirmatcher(subpath, match)
453                s = sub.status(
454                    rev2,
455                    match=submatch,
456                    ignored=listignored,
457                    clean=listclean,
458                    unknown=listunknown,
459                    listsubrepos=True,
460                )
461                for k in (
462                    'modified',
463                    'added',
464                    'removed',
465                    'deleted',
466                    'unknown',
467                    'ignored',
468                    'clean',
469                ):
470                    rfiles, sfiles = getattr(r, k), getattr(s, k)
471                    rfiles.extend(b"%s/%s" % (subpath, f) for f in sfiles)
472
473        r.modified.sort()
474        r.added.sort()
475        r.removed.sort()
476        r.deleted.sort()
477        r.unknown.sort()
478        r.ignored.sort()
479        r.clean.sort()
480
481        return r
482
483    def mergestate(self, clean=False):
484        """Get a mergestate object for this context."""
485        raise NotImplementedError(
486            '%s does not implement mergestate()' % self.__class__
487        )
488
489    def isempty(self):
490        return not (
491            len(self.parents()) > 1
492            or self.branch() != self.p1().branch()
493            or self.closesbranch()
494            or self.files()
495        )
496
497
498class changectx(basectx):
499    """A changecontext object makes access to data related to a particular
500    changeset convenient. It represents a read-only context already present in
501    the repo."""
502
503    def __init__(self, repo, rev, node, maybe_filtered=True):
504        super(changectx, self).__init__(repo)
505        self._rev = rev
506        self._node = node
507        # When maybe_filtered is True, the revision might be affected by
508        # changelog filtering and operation through the filtered changelog must be used.
509        #
510        # When maybe_filtered is False, the revision has already been checked
511        # against filtering and is not filtered. Operation through the
512        # unfiltered changelog might be used in some case.
513        self._maybe_filtered = maybe_filtered
514
515    def __hash__(self):
516        try:
517            return hash(self._rev)
518        except AttributeError:
519            return id(self)
520
521    def __nonzero__(self):
522        return self._rev != nullrev
523
524    __bool__ = __nonzero__
525
526    @propertycache
527    def _changeset(self):
528        if self._maybe_filtered:
529            repo = self._repo
530        else:
531            repo = self._repo.unfiltered()
532        return repo.changelog.changelogrevision(self.rev())
533
534    @propertycache
535    def _manifest(self):
536        return self._manifestctx.read()
537
538    @property
539    def _manifestctx(self):
540        return self._repo.manifestlog[self._changeset.manifest]
541
542    @propertycache
543    def _manifestdelta(self):
544        return self._manifestctx.readdelta()
545
546    @propertycache
547    def _parents(self):
548        repo = self._repo
549        if self._maybe_filtered:
550            cl = repo.changelog
551        else:
552            cl = repo.unfiltered().changelog
553
554        p1, p2 = cl.parentrevs(self._rev)
555        if p2 == nullrev:
556            return [changectx(repo, p1, cl.node(p1), maybe_filtered=False)]
557        return [
558            changectx(repo, p1, cl.node(p1), maybe_filtered=False),
559            changectx(repo, p2, cl.node(p2), maybe_filtered=False),
560        ]
561
562    def changeset(self):
563        c = self._changeset
564        return (
565            c.manifest,
566            c.user,
567            c.date,
568            c.files,
569            c.description,
570            c.extra,
571        )
572
573    def manifestnode(self):
574        return self._changeset.manifest
575
576    def user(self):
577        return self._changeset.user
578
579    def date(self):
580        return self._changeset.date
581
582    def files(self):
583        return self._changeset.files
584
585    def filesmodified(self):
586        modified = set(self.files())
587        modified.difference_update(self.filesadded())
588        modified.difference_update(self.filesremoved())
589        return sorted(modified)
590
591    def filesadded(self):
592        filesadded = self._changeset.filesadded
593        compute_on_none = True
594        if self._repo.filecopiesmode == b'changeset-sidedata':
595            compute_on_none = False
596        else:
597            source = self._repo.ui.config(b'experimental', b'copies.read-from')
598            if source == b'changeset-only':
599                compute_on_none = False
600            elif source != b'compatibility':
601                # filelog mode, ignore any changelog content
602                filesadded = None
603        if filesadded is None:
604            if compute_on_none:
605                filesadded = metadata.computechangesetfilesadded(self)
606            else:
607                filesadded = []
608        return filesadded
609
610    def filesremoved(self):
611        filesremoved = self._changeset.filesremoved
612        compute_on_none = True
613        if self._repo.filecopiesmode == b'changeset-sidedata':
614            compute_on_none = False
615        else:
616            source = self._repo.ui.config(b'experimental', b'copies.read-from')
617            if source == b'changeset-only':
618                compute_on_none = False
619            elif source != b'compatibility':
620                # filelog mode, ignore any changelog content
621                filesremoved = None
622        if filesremoved is None:
623            if compute_on_none:
624                filesremoved = metadata.computechangesetfilesremoved(self)
625            else:
626                filesremoved = []
627        return filesremoved
628
629    @propertycache
630    def _copies(self):
631        p1copies = self._changeset.p1copies
632        p2copies = self._changeset.p2copies
633        compute_on_none = True
634        if self._repo.filecopiesmode == b'changeset-sidedata':
635            compute_on_none = False
636        else:
637            source = self._repo.ui.config(b'experimental', b'copies.read-from')
638            # If config says to get copy metadata only from changeset, then
639            # return that, defaulting to {} if there was no copy metadata.  In
640            # compatibility mode, we return copy data from the changeset if it
641            # was recorded there, and otherwise we fall back to getting it from
642            # the filelogs (below).
643            #
644            # If we are in compatiblity mode and there is not data in the
645            # changeset), we get the copy metadata from the filelogs.
646            #
647            # otherwise, when config said to read only from filelog, we get the
648            # copy metadata from the filelogs.
649            if source == b'changeset-only':
650                compute_on_none = False
651            elif source != b'compatibility':
652                # filelog mode, ignore any changelog content
653                p1copies = p2copies = None
654        if p1copies is None:
655            if compute_on_none:
656                p1copies, p2copies = super(changectx, self)._copies
657            else:
658                if p1copies is None:
659                    p1copies = {}
660        if p2copies is None:
661            p2copies = {}
662        return p1copies, p2copies
663
664    def description(self):
665        return self._changeset.description
666
667    def branch(self):
668        return encoding.tolocal(self._changeset.extra.get(b"branch"))
669
670    def closesbranch(self):
671        return b'close' in self._changeset.extra
672
673    def extra(self):
674        """Return a dict of extra information."""
675        return self._changeset.extra
676
677    def tags(self):
678        """Return a list of byte tag names"""
679        return self._repo.nodetags(self._node)
680
681    def bookmarks(self):
682        """Return a list of byte bookmark names."""
683        return self._repo.nodebookmarks(self._node)
684
685    def phase(self):
686        return self._repo._phasecache.phase(self._repo, self._rev)
687
688    def hidden(self):
689        return self._rev in repoview.filterrevs(self._repo, b'visible')
690
691    def isinmemory(self):
692        return False
693
694    def children(self):
695        """return list of changectx contexts for each child changeset.
696
697        This returns only the immediate child changesets. Use descendants() to
698        recursively walk children.
699        """
700        c = self._repo.changelog.children(self._node)
701        return [self._repo[x] for x in c]
702
703    def ancestors(self):
704        for a in self._repo.changelog.ancestors([self._rev]):
705            yield self._repo[a]
706
707    def descendants(self):
708        """Recursively yield all children of the changeset.
709
710        For just the immediate children, use children()
711        """
712        for d in self._repo.changelog.descendants([self._rev]):
713            yield self._repo[d]
714
715    def filectx(self, path, fileid=None, filelog=None):
716        """get a file context from this changeset"""
717        if fileid is None:
718            fileid = self.filenode(path)
719        return filectx(
720            self._repo, path, fileid=fileid, changectx=self, filelog=filelog
721        )
722
723    def ancestor(self, c2, warn=False):
724        """return the "best" ancestor context of self and c2
725
726        If there are multiple candidates, it will show a message and check
727        merge.preferancestor configuration before falling back to the
728        revlog ancestor."""
729        # deal with workingctxs
730        n2 = c2._node
731        if n2 is None:
732            n2 = c2._parents[0]._node
733        cahs = self._repo.changelog.commonancestorsheads(self._node, n2)
734        if not cahs:
735            anc = self._repo.nodeconstants.nullid
736        elif len(cahs) == 1:
737            anc = cahs[0]
738        else:
739            # experimental config: merge.preferancestor
740            for r in self._repo.ui.configlist(b'merge', b'preferancestor'):
741                try:
742                    ctx = scmutil.revsymbol(self._repo, r)
743                except error.RepoLookupError:
744                    continue
745                anc = ctx.node()
746                if anc in cahs:
747                    break
748            else:
749                anc = self._repo.changelog.ancestor(self._node, n2)
750            if warn:
751                self._repo.ui.status(
752                    (
753                        _(b"note: using %s as ancestor of %s and %s\n")
754                        % (short(anc), short(self._node), short(n2))
755                    )
756                    + b''.join(
757                        _(
758                            b"      alternatively, use --config "
759                            b"merge.preferancestor=%s\n"
760                        )
761                        % short(n)
762                        for n in sorted(cahs)
763                        if n != anc
764                    )
765                )
766        return self._repo[anc]
767
768    def isancestorof(self, other):
769        """True if this changeset is an ancestor of other"""
770        return self._repo.changelog.isancestorrev(self._rev, other._rev)
771
772    def walk(self, match):
773        '''Generates matching file names.'''
774
775        # Wrap match.bad method to have message with nodeid
776        def bad(fn, msg):
777            # The manifest doesn't know about subrepos, so don't complain about
778            # paths into valid subrepos.
779            if any(fn == s or fn.startswith(s + b'/') for s in self.substate):
780                return
781            match.bad(fn, _(b'no such file in rev %s') % self)
782
783        m = matchmod.badmatch(self._repo.narrowmatch(match), bad)
784        return self._manifest.walk(m)
785
786    def matches(self, match):
787        return self.walk(match)
788
789
790class basefilectx(object):
791    """A filecontext object represents the common logic for its children:
792    filectx: read-only access to a filerevision that is already present
793             in the repo,
794    workingfilectx: a filecontext that represents files from the working
795                    directory,
796    memfilectx: a filecontext that represents files in-memory,
797    """
798
799    @propertycache
800    def _filelog(self):
801        return self._repo.file(self._path)
802
803    @propertycache
804    def _changeid(self):
805        if '_changectx' in self.__dict__:
806            return self._changectx.rev()
807        elif '_descendantrev' in self.__dict__:
808            # this file context was created from a revision with a known
809            # descendant, we can (lazily) correct for linkrev aliases
810            return self._adjustlinkrev(self._descendantrev)
811        else:
812            return self._filelog.linkrev(self._filerev)
813
814    @propertycache
815    def _filenode(self):
816        if '_fileid' in self.__dict__:
817            return self._filelog.lookup(self._fileid)
818        else:
819            return self._changectx.filenode(self._path)
820
821    @propertycache
822    def _filerev(self):
823        return self._filelog.rev(self._filenode)
824
825    @propertycache
826    def _repopath(self):
827        return self._path
828
829    def __nonzero__(self):
830        try:
831            self._filenode
832            return True
833        except error.LookupError:
834            # file is missing
835            return False
836
837    __bool__ = __nonzero__
838
839    def __bytes__(self):
840        try:
841            return b"%s@%s" % (self.path(), self._changectx)
842        except error.LookupError:
843            return b"%s@???" % self.path()
844
845    __str__ = encoding.strmethod(__bytes__)
846
847    def __repr__(self):
848        return "<%s %s>" % (type(self).__name__, str(self))
849
850    def __hash__(self):
851        try:
852            return hash((self._path, self._filenode))
853        except AttributeError:
854            return id(self)
855
856    def __eq__(self, other):
857        try:
858            return (
859                type(self) == type(other)
860                and self._path == other._path
861                and self._filenode == other._filenode
862            )
863        except AttributeError:
864            return False
865
866    def __ne__(self, other):
867        return not (self == other)
868
869    def filerev(self):
870        return self._filerev
871
872    def filenode(self):
873        return self._filenode
874
875    @propertycache
876    def _flags(self):
877        return self._changectx.flags(self._path)
878
879    def flags(self):
880        return self._flags
881
882    def filelog(self):
883        return self._filelog
884
885    def rev(self):
886        return self._changeid
887
888    def linkrev(self):
889        return self._filelog.linkrev(self._filerev)
890
891    def node(self):
892        return self._changectx.node()
893
894    def hex(self):
895        return self._changectx.hex()
896
897    def user(self):
898        return self._changectx.user()
899
900    def date(self):
901        return self._changectx.date()
902
903    def files(self):
904        return self._changectx.files()
905
906    def description(self):
907        return self._changectx.description()
908
909    def branch(self):
910        return self._changectx.branch()
911
912    def extra(self):
913        return self._changectx.extra()
914
915    def phase(self):
916        return self._changectx.phase()
917
918    def phasestr(self):
919        return self._changectx.phasestr()
920
921    def obsolete(self):
922        return self._changectx.obsolete()
923
924    def instabilities(self):
925        return self._changectx.instabilities()
926
927    def manifest(self):
928        return self._changectx.manifest()
929
930    def changectx(self):
931        return self._changectx
932
933    def renamed(self):
934        return self._copied
935
936    def copysource(self):
937        return self._copied and self._copied[0]
938
939    def repo(self):
940        return self._repo
941
942    def size(self):
943        return len(self.data())
944
945    def path(self):
946        return self._path
947
948    def isbinary(self):
949        try:
950            return stringutil.binary(self.data())
951        except IOError:
952            return False
953
954    def isexec(self):
955        return b'x' in self.flags()
956
957    def islink(self):
958        return b'l' in self.flags()
959
960    def isabsent(self):
961        """whether this filectx represents a file not in self._changectx
962
963        This is mainly for merge code to detect change/delete conflicts. This is
964        expected to be True for all subclasses of basectx."""
965        return False
966
967    _customcmp = False
968
969    def cmp(self, fctx):
970        """compare with other file context
971
972        returns True if different than fctx.
973        """
974        if fctx._customcmp:
975            return fctx.cmp(self)
976
977        if self._filenode is None:
978            raise error.ProgrammingError(
979                b'filectx.cmp() must be reimplemented if not backed by revlog'
980            )
981
982        if fctx._filenode is None:
983            if self._repo._encodefilterpats:
984                # can't rely on size() because wdir content may be decoded
985                return self._filelog.cmp(self._filenode, fctx.data())
986            if self.size() - 4 == fctx.size():
987                # size() can match:
988                # if file data starts with '\1\n', empty metadata block is
989                # prepended, which adds 4 bytes to filelog.size().
990                return self._filelog.cmp(self._filenode, fctx.data())
991        if self.size() == fctx.size() or self.flags() == b'l':
992            # size() matches: need to compare content
993            # issue6456: Always compare symlinks because size can represent
994            # encrypted string for EXT-4 encryption(fscrypt).
995            return self._filelog.cmp(self._filenode, fctx.data())
996
997        # size() differs
998        return True
999
1000    def _adjustlinkrev(self, srcrev, inclusive=False, stoprev=None):
1001        """return the first ancestor of <srcrev> introducing <fnode>
1002
1003        If the linkrev of the file revision does not point to an ancestor of
1004        srcrev, we'll walk down the ancestors until we find one introducing
1005        this file revision.
1006
1007        :srcrev: the changeset revision we search ancestors from
1008        :inclusive: if true, the src revision will also be checked
1009        :stoprev: an optional revision to stop the walk at. If no introduction
1010                  of this file content could be found before this floor
1011                  revision, the function will returns "None" and stops its
1012                  iteration.
1013        """
1014        repo = self._repo
1015        cl = repo.unfiltered().changelog
1016        mfl = repo.manifestlog
1017        # fetch the linkrev
1018        lkr = self.linkrev()
1019        if srcrev == lkr:
1020            return lkr
1021        # hack to reuse ancestor computation when searching for renames
1022        memberanc = getattr(self, '_ancestrycontext', None)
1023        iteranc = None
1024        if srcrev is None:
1025            # wctx case, used by workingfilectx during mergecopy
1026            revs = [p.rev() for p in self._repo[None].parents()]
1027            inclusive = True  # we skipped the real (revless) source
1028        else:
1029            revs = [srcrev]
1030        if memberanc is None:
1031            memberanc = iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
1032        # check if this linkrev is an ancestor of srcrev
1033        if lkr not in memberanc:
1034            if iteranc is None:
1035                iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
1036            fnode = self._filenode
1037            path = self._path
1038            for a in iteranc:
1039                if stoprev is not None and a < stoprev:
1040                    return None
1041                ac = cl.read(a)  # get changeset data (we avoid object creation)
1042                if path in ac[3]:  # checking the 'files' field.
1043                    # The file has been touched, check if the content is
1044                    # similar to the one we search for.
1045                    if fnode == mfl[ac[0]].readfast().get(path):
1046                        return a
1047            # In theory, we should never get out of that loop without a result.
1048            # But if manifest uses a buggy file revision (not children of the
1049            # one it replaces) we could. Such a buggy situation will likely
1050            # result is crash somewhere else at to some point.
1051        return lkr
1052
1053    def isintroducedafter(self, changelogrev):
1054        """True if a filectx has been introduced after a given floor revision"""
1055        if self.linkrev() >= changelogrev:
1056            return True
1057        introrev = self._introrev(stoprev=changelogrev)
1058        if introrev is None:
1059            return False
1060        return introrev >= changelogrev
1061
1062    def introrev(self):
1063        """return the rev of the changeset which introduced this file revision
1064
1065        This method is different from linkrev because it take into account the
1066        changeset the filectx was created from. It ensures the returned
1067        revision is one of its ancestors. This prevents bugs from
1068        'linkrev-shadowing' when a file revision is used by multiple
1069        changesets.
1070        """
1071        return self._introrev()
1072
1073    def _introrev(self, stoprev=None):
1074        """
1075        Same as `introrev` but, with an extra argument to limit changelog
1076        iteration range in some internal usecase.
1077
1078        If `stoprev` is set, the `introrev` will not be searched past that
1079        `stoprev` revision and "None" might be returned. This is useful to
1080        limit the iteration range.
1081        """
1082        toprev = None
1083        attrs = vars(self)
1084        if '_changeid' in attrs:
1085            # We have a cached value already
1086            toprev = self._changeid
1087        elif '_changectx' in attrs:
1088            # We know which changelog entry we are coming from
1089            toprev = self._changectx.rev()
1090
1091        if toprev is not None:
1092            return self._adjustlinkrev(toprev, inclusive=True, stoprev=stoprev)
1093        elif '_descendantrev' in attrs:
1094            introrev = self._adjustlinkrev(self._descendantrev, stoprev=stoprev)
1095            # be nice and cache the result of the computation
1096            if introrev is not None:
1097                self._changeid = introrev
1098            return introrev
1099        else:
1100            return self.linkrev()
1101
1102    def introfilectx(self):
1103        """Return filectx having identical contents, but pointing to the
1104        changeset revision where this filectx was introduced"""
1105        introrev = self.introrev()
1106        if self.rev() == introrev:
1107            return self
1108        return self.filectx(self.filenode(), changeid=introrev)
1109
1110    def _parentfilectx(self, path, fileid, filelog):
1111        """create parent filectx keeping ancestry info for _adjustlinkrev()"""
1112        fctx = filectx(self._repo, path, fileid=fileid, filelog=filelog)
1113        if '_changeid' in vars(self) or '_changectx' in vars(self):
1114            # If self is associated with a changeset (probably explicitly
1115            # fed), ensure the created filectx is associated with a
1116            # changeset that is an ancestor of self.changectx.
1117            # This lets us later use _adjustlinkrev to get a correct link.
1118            fctx._descendantrev = self.rev()
1119            fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1120        elif '_descendantrev' in vars(self):
1121            # Otherwise propagate _descendantrev if we have one associated.
1122            fctx._descendantrev = self._descendantrev
1123            fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1124        return fctx
1125
1126    def parents(self):
1127        _path = self._path
1128        fl = self._filelog
1129        parents = self._filelog.parents(self._filenode)
1130        pl = [
1131            (_path, node, fl)
1132            for node in parents
1133            if node != self._repo.nodeconstants.nullid
1134        ]
1135
1136        r = fl.renamed(self._filenode)
1137        if r:
1138            # - In the simple rename case, both parent are nullid, pl is empty.
1139            # - In case of merge, only one of the parent is null id and should
1140            # be replaced with the rename information. This parent is -always-
1141            # the first one.
1142            #
1143            # As null id have always been filtered out in the previous list
1144            # comprehension, inserting to 0 will always result in "replacing
1145            # first nullid parent with rename information.
1146            pl.insert(0, (r[0], r[1], self._repo.file(r[0])))
1147
1148        return [self._parentfilectx(path, fnode, l) for path, fnode, l in pl]
1149
1150    def p1(self):
1151        return self.parents()[0]
1152
1153    def p2(self):
1154        p = self.parents()
1155        if len(p) == 2:
1156            return p[1]
1157        return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
1158
1159    def annotate(self, follow=False, skiprevs=None, diffopts=None):
1160        """Returns a list of annotateline objects for each line in the file
1161
1162        - line.fctx is the filectx of the node where that line was last changed
1163        - line.lineno is the line number at the first appearance in the managed
1164          file
1165        - line.text is the data on that line (including newline character)
1166        """
1167        getlog = util.lrucachefunc(lambda x: self._repo.file(x))
1168
1169        def parents(f):
1170            # Cut _descendantrev here to mitigate the penalty of lazy linkrev
1171            # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
1172            # from the topmost introrev (= srcrev) down to p.linkrev() if it
1173            # isn't an ancestor of the srcrev.
1174            f._changeid
1175            pl = f.parents()
1176
1177            # Don't return renamed parents if we aren't following.
1178            if not follow:
1179                pl = [p for p in pl if p.path() == f.path()]
1180
1181            # renamed filectx won't have a filelog yet, so set it
1182            # from the cache to save time
1183            for p in pl:
1184                if not '_filelog' in p.__dict__:
1185                    p._filelog = getlog(p.path())
1186
1187            return pl
1188
1189        # use linkrev to find the first changeset where self appeared
1190        base = self.introfilectx()
1191        if getattr(base, '_ancestrycontext', None) is None:
1192            # it is safe to use an unfiltered repository here because we are
1193            # walking ancestors only.
1194            cl = self._repo.unfiltered().changelog
1195            if base.rev() is None:
1196                # wctx is not inclusive, but works because _ancestrycontext
1197                # is used to test filelog revisions
1198                ac = cl.ancestors(
1199                    [p.rev() for p in base.parents()], inclusive=True
1200                )
1201            else:
1202                ac = cl.ancestors([base.rev()], inclusive=True)
1203            base._ancestrycontext = ac
1204
1205        return dagop.annotate(
1206            base, parents, skiprevs=skiprevs, diffopts=diffopts
1207        )
1208
1209    def ancestors(self, followfirst=False):
1210        visit = {}
1211        c = self
1212        if followfirst:
1213            cut = 1
1214        else:
1215            cut = None
1216
1217        while True:
1218            for parent in c.parents()[:cut]:
1219                visit[(parent.linkrev(), parent.filenode())] = parent
1220            if not visit:
1221                break
1222            c = visit.pop(max(visit))
1223            yield c
1224
1225    def decodeddata(self):
1226        """Returns `data()` after running repository decoding filters.
1227
1228        This is often equivalent to how the data would be expressed on disk.
1229        """
1230        return self._repo.wwritedata(self.path(), self.data())
1231
1232
1233class filectx(basefilectx):
1234    """A filecontext object makes access to data related to a particular
1235    filerevision convenient."""
1236
1237    def __init__(
1238        self,
1239        repo,
1240        path,
1241        changeid=None,
1242        fileid=None,
1243        filelog=None,
1244        changectx=None,
1245    ):
1246        """changeid must be a revision number, if specified.
1247        fileid can be a file revision or node."""
1248        self._repo = repo
1249        self._path = path
1250
1251        assert (
1252            changeid is not None or fileid is not None or changectx is not None
1253        ), b"bad args: changeid=%r, fileid=%r, changectx=%r" % (
1254            changeid,
1255            fileid,
1256            changectx,
1257        )
1258
1259        if filelog is not None:
1260            self._filelog = filelog
1261
1262        if changeid is not None:
1263            self._changeid = changeid
1264        if changectx is not None:
1265            self._changectx = changectx
1266        if fileid is not None:
1267            self._fileid = fileid
1268
1269    @propertycache
1270    def _changectx(self):
1271        try:
1272            return self._repo[self._changeid]
1273        except error.FilteredRepoLookupError:
1274            # Linkrev may point to any revision in the repository.  When the
1275            # repository is filtered this may lead to `filectx` trying to build
1276            # `changectx` for filtered revision. In such case we fallback to
1277            # creating `changectx` on the unfiltered version of the reposition.
1278            # This fallback should not be an issue because `changectx` from
1279            # `filectx` are not used in complex operations that care about
1280            # filtering.
1281            #
1282            # This fallback is a cheap and dirty fix that prevent several
1283            # crashes. It does not ensure the behavior is correct. However the
1284            # behavior was not correct before filtering either and "incorrect
1285            # behavior" is seen as better as "crash"
1286            #
1287            # Linkrevs have several serious troubles with filtering that are
1288            # complicated to solve. Proper handling of the issue here should be
1289            # considered when solving linkrev issue are on the table.
1290            return self._repo.unfiltered()[self._changeid]
1291
1292    def filectx(self, fileid, changeid=None):
1293        """opens an arbitrary revision of the file without
1294        opening a new filelog"""
1295        return filectx(
1296            self._repo,
1297            self._path,
1298            fileid=fileid,
1299            filelog=self._filelog,
1300            changeid=changeid,
1301        )
1302
1303    def rawdata(self):
1304        return self._filelog.rawdata(self._filenode)
1305
1306    def rawflags(self):
1307        """low-level revlog flags"""
1308        return self._filelog.flags(self._filerev)
1309
1310    def data(self):
1311        try:
1312            return self._filelog.read(self._filenode)
1313        except error.CensoredNodeError:
1314            if self._repo.ui.config(b"censor", b"policy") == b"ignore":
1315                return b""
1316            raise error.Abort(
1317                _(b"censored node: %s") % short(self._filenode),
1318                hint=_(b"set censor.policy to ignore errors"),
1319            )
1320
1321    def size(self):
1322        return self._filelog.size(self._filerev)
1323
1324    @propertycache
1325    def _copied(self):
1326        """check if file was actually renamed in this changeset revision
1327
1328        If rename logged in file revision, we report copy for changeset only
1329        if file revisions linkrev points back to the changeset in question
1330        or both changeset parents contain different file revisions.
1331        """
1332
1333        renamed = self._filelog.renamed(self._filenode)
1334        if not renamed:
1335            return None
1336
1337        if self.rev() == self.linkrev():
1338            return renamed
1339
1340        name = self.path()
1341        fnode = self._filenode
1342        for p in self._changectx.parents():
1343            try:
1344                if fnode == p.filenode(name):
1345                    return None
1346            except error.LookupError:
1347                pass
1348        return renamed
1349
1350    def children(self):
1351        # hard for renames
1352        c = self._filelog.children(self._filenode)
1353        return [
1354            filectx(self._repo, self._path, fileid=x, filelog=self._filelog)
1355            for x in c
1356        ]
1357
1358
1359class committablectx(basectx):
1360    """A committablectx object provides common functionality for a context that
1361    wants the ability to commit, e.g. workingctx or memctx."""
1362
1363    def __init__(
1364        self,
1365        repo,
1366        text=b"",
1367        user=None,
1368        date=None,
1369        extra=None,
1370        changes=None,
1371        branch=None,
1372    ):
1373        super(committablectx, self).__init__(repo)
1374        self._rev = None
1375        self._node = None
1376        self._text = text
1377        if date:
1378            self._date = dateutil.parsedate(date)
1379        if user:
1380            self._user = user
1381        if changes:
1382            self._status = changes
1383
1384        self._extra = {}
1385        if extra:
1386            self._extra = extra.copy()
1387        if branch is not None:
1388            self._extra[b'branch'] = encoding.fromlocal(branch)
1389        if not self._extra.get(b'branch'):
1390            self._extra[b'branch'] = b'default'
1391
1392    def __bytes__(self):
1393        return bytes(self._parents[0]) + b"+"
1394
1395    def hex(self):
1396        self._repo.nodeconstants.wdirhex
1397
1398    __str__ = encoding.strmethod(__bytes__)
1399
1400    def __nonzero__(self):
1401        return True
1402
1403    __bool__ = __nonzero__
1404
1405    @propertycache
1406    def _status(self):
1407        return self._repo.status()
1408
1409    @propertycache
1410    def _user(self):
1411        return self._repo.ui.username()
1412
1413    @propertycache
1414    def _date(self):
1415        ui = self._repo.ui
1416        date = ui.configdate(b'devel', b'default-date')
1417        if date is None:
1418            date = dateutil.makedate()
1419        return date
1420
1421    def subrev(self, subpath):
1422        return None
1423
1424    def manifestnode(self):
1425        return None
1426
1427    def user(self):
1428        return self._user or self._repo.ui.username()
1429
1430    def date(self):
1431        return self._date
1432
1433    def description(self):
1434        return self._text
1435
1436    def files(self):
1437        return sorted(
1438            self._status.modified + self._status.added + self._status.removed
1439        )
1440
1441    def modified(self):
1442        return self._status.modified
1443
1444    def added(self):
1445        return self._status.added
1446
1447    def removed(self):
1448        return self._status.removed
1449
1450    def deleted(self):
1451        return self._status.deleted
1452
1453    filesmodified = modified
1454    filesadded = added
1455    filesremoved = removed
1456
1457    def branch(self):
1458        return encoding.tolocal(self._extra[b'branch'])
1459
1460    def closesbranch(self):
1461        return b'close' in self._extra
1462
1463    def extra(self):
1464        return self._extra
1465
1466    def isinmemory(self):
1467        return False
1468
1469    def tags(self):
1470        return []
1471
1472    def bookmarks(self):
1473        b = []
1474        for p in self.parents():
1475            b.extend(p.bookmarks())
1476        return b
1477
1478    def phase(self):
1479        phase = phases.newcommitphase(self._repo.ui)
1480        for p in self.parents():
1481            phase = max(phase, p.phase())
1482        return phase
1483
1484    def hidden(self):
1485        return False
1486
1487    def children(self):
1488        return []
1489
1490    def flags(self, path):
1491        if '_manifest' in self.__dict__:
1492            try:
1493                return self._manifest.flags(path)
1494            except KeyError:
1495                return b''
1496
1497        try:
1498            return self._flagfunc(path)
1499        except OSError:
1500            return b''
1501
1502    def ancestor(self, c2):
1503        """return the "best" ancestor context of self and c2"""
1504        return self._parents[0].ancestor(c2)  # punt on two parents for now
1505
1506    def ancestors(self):
1507        for p in self._parents:
1508            yield p
1509        for a in self._repo.changelog.ancestors(
1510            [p.rev() for p in self._parents]
1511        ):
1512            yield self._repo[a]
1513
1514    def markcommitted(self, node):
1515        """Perform post-commit cleanup necessary after committing this ctx
1516
1517        Specifically, this updates backing stores this working context
1518        wraps to reflect the fact that the changes reflected by this
1519        workingctx have been committed.  For example, it marks
1520        modified and added files as normal in the dirstate.
1521
1522        """
1523
1524    def dirty(self, missing=False, merge=True, branch=True):
1525        return False
1526
1527
1528class workingctx(committablectx):
1529    """A workingctx object makes access to data related to
1530    the current working directory convenient.
1531    date - any valid date string or (unixtime, offset), or None.
1532    user - username string, or None.
1533    extra - a dictionary of extra values, or None.
1534    changes - a list of file lists as returned by localrepo.status()
1535               or None to use the repository status.
1536    """
1537
1538    def __init__(
1539        self, repo, text=b"", user=None, date=None, extra=None, changes=None
1540    ):
1541        branch = None
1542        if not extra or b'branch' not in extra:
1543            try:
1544                branch = repo.dirstate.branch()
1545            except UnicodeDecodeError:
1546                raise error.Abort(_(b'branch name not in UTF-8!'))
1547        super(workingctx, self).__init__(
1548            repo, text, user, date, extra, changes, branch=branch
1549        )
1550
1551    def __iter__(self):
1552        d = self._repo.dirstate
1553        for f in d:
1554            if d.get_entry(f).tracked:
1555                yield f
1556
1557    def __contains__(self, key):
1558        return self._repo.dirstate.get_entry(key).tracked
1559
1560    def hex(self):
1561        return self._repo.nodeconstants.wdirhex
1562
1563    @propertycache
1564    def _parents(self):
1565        p = self._repo.dirstate.parents()
1566        if p[1] == self._repo.nodeconstants.nullid:
1567            p = p[:-1]
1568        # use unfiltered repo to delay/avoid loading obsmarkers
1569        unfi = self._repo.unfiltered()
1570        return [
1571            changectx(
1572                self._repo, unfi.changelog.rev(n), n, maybe_filtered=False
1573            )
1574            for n in p
1575        ]
1576
1577    def setparents(self, p1node, p2node=None):
1578        if p2node is None:
1579            p2node = self._repo.nodeconstants.nullid
1580        dirstate = self._repo.dirstate
1581        with dirstate.parentchange():
1582            copies = dirstate.setparents(p1node, p2node)
1583            pctx = self._repo[p1node]
1584            if copies:
1585                # Adjust copy records, the dirstate cannot do it, it
1586                # requires access to parents manifests. Preserve them
1587                # only for entries added to first parent.
1588                for f in copies:
1589                    if f not in pctx and copies[f] in pctx:
1590                        dirstate.copy(copies[f], f)
1591            if p2node == self._repo.nodeconstants.nullid:
1592                for f, s in sorted(dirstate.copies().items()):
1593                    if f not in pctx and s not in pctx:
1594                        dirstate.copy(None, f)
1595
1596    def _fileinfo(self, path):
1597        # populate __dict__['_manifest'] as workingctx has no _manifestdelta
1598        self._manifest
1599        return super(workingctx, self)._fileinfo(path)
1600
1601    def _buildflagfunc(self):
1602        # Create a fallback function for getting file flags when the
1603        # filesystem doesn't support them
1604
1605        copiesget = self._repo.dirstate.copies().get
1606        parents = self.parents()
1607        if len(parents) < 2:
1608            # when we have one parent, it's easy: copy from parent
1609            man = parents[0].manifest()
1610
1611            def func(f):
1612                f = copiesget(f, f)
1613                return man.flags(f)
1614
1615        else:
1616            # merges are tricky: we try to reconstruct the unstored
1617            # result from the merge (issue1802)
1618            p1, p2 = parents
1619            pa = p1.ancestor(p2)
1620            m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
1621
1622            def func(f):
1623                f = copiesget(f, f)  # may be wrong for merges with copies
1624                fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
1625                if fl1 == fl2:
1626                    return fl1
1627                if fl1 == fla:
1628                    return fl2
1629                if fl2 == fla:
1630                    return fl1
1631                return b''  # punt for conflicts
1632
1633        return func
1634
1635    @propertycache
1636    def _flagfunc(self):
1637        return self._repo.dirstate.flagfunc(self._buildflagfunc)
1638
1639    def flags(self, path):
1640        try:
1641            return self._flagfunc(path)
1642        except OSError:
1643            return b''
1644
1645    def filectx(self, path, filelog=None):
1646        """get a file context from the working directory"""
1647        return workingfilectx(
1648            self._repo, path, workingctx=self, filelog=filelog
1649        )
1650
1651    def dirty(self, missing=False, merge=True, branch=True):
1652        """check whether a working directory is modified"""
1653        # check subrepos first
1654        for s in sorted(self.substate):
1655            if self.sub(s).dirty(missing=missing):
1656                return True
1657        # check current working dir
1658        return (
1659            (merge and self.p2())
1660            or (branch and self.branch() != self.p1().branch())
1661            or self.modified()
1662            or self.added()
1663            or self.removed()
1664            or (missing and self.deleted())
1665        )
1666
1667    def add(self, list, prefix=b""):
1668        with self._repo.wlock():
1669            ui, ds = self._repo.ui, self._repo.dirstate
1670            uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1671            rejected = []
1672            lstat = self._repo.wvfs.lstat
1673            for f in list:
1674                # ds.pathto() returns an absolute file when this is invoked from
1675                # the keyword extension.  That gets flagged as non-portable on
1676                # Windows, since it contains the drive letter and colon.
1677                scmutil.checkportable(ui, os.path.join(prefix, f))
1678                try:
1679                    st = lstat(f)
1680                except OSError:
1681                    ui.warn(_(b"%s does not exist!\n") % uipath(f))
1682                    rejected.append(f)
1683                    continue
1684                limit = ui.configbytes(b'ui', b'large-file-limit')
1685                if limit != 0 and st.st_size > limit:
1686                    ui.warn(
1687                        _(
1688                            b"%s: up to %d MB of RAM may be required "
1689                            b"to manage this file\n"
1690                            b"(use 'hg revert %s' to cancel the "
1691                            b"pending addition)\n"
1692                        )
1693                        % (f, 3 * st.st_size // 1000000, uipath(f))
1694                    )
1695                if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1696                    ui.warn(
1697                        _(
1698                            b"%s not added: only files and symlinks "
1699                            b"supported currently\n"
1700                        )
1701                        % uipath(f)
1702                    )
1703                    rejected.append(f)
1704                elif not ds.set_tracked(f):
1705                    ui.warn(_(b"%s already tracked!\n") % uipath(f))
1706            return rejected
1707
1708    def forget(self, files, prefix=b""):
1709        with self._repo.wlock():
1710            ds = self._repo.dirstate
1711            uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1712            rejected = []
1713            for f in files:
1714                if not ds.set_untracked(f):
1715                    self._repo.ui.warn(_(b"%s not tracked!\n") % uipath(f))
1716                    rejected.append(f)
1717            return rejected
1718
1719    def copy(self, source, dest):
1720        try:
1721            st = self._repo.wvfs.lstat(dest)
1722        except OSError as err:
1723            if err.errno != errno.ENOENT:
1724                raise
1725            self._repo.ui.warn(
1726                _(b"%s does not exist!\n") % self._repo.dirstate.pathto(dest)
1727            )
1728            return
1729        if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1730            self._repo.ui.warn(
1731                _(b"copy failed: %s is not a file or a symbolic link\n")
1732                % self._repo.dirstate.pathto(dest)
1733            )
1734        else:
1735            with self._repo.wlock():
1736                ds = self._repo.dirstate
1737                ds.set_tracked(dest)
1738                ds.copy(source, dest)
1739
1740    def match(
1741        self,
1742        pats=None,
1743        include=None,
1744        exclude=None,
1745        default=b'glob',
1746        listsubrepos=False,
1747        badfn=None,
1748        cwd=None,
1749    ):
1750        r = self._repo
1751        if not cwd:
1752            cwd = r.getcwd()
1753
1754        # Only a case insensitive filesystem needs magic to translate user input
1755        # to actual case in the filesystem.
1756        icasefs = not util.fscasesensitive(r.root)
1757        return matchmod.match(
1758            r.root,
1759            cwd,
1760            pats,
1761            include,
1762            exclude,
1763            default,
1764            auditor=r.auditor,
1765            ctx=self,
1766            listsubrepos=listsubrepos,
1767            badfn=badfn,
1768            icasefs=icasefs,
1769        )
1770
1771    def _filtersuspectsymlink(self, files):
1772        if not files or self._repo.dirstate._checklink:
1773            return files
1774
1775        # Symlink placeholders may get non-symlink-like contents
1776        # via user error or dereferencing by NFS or Samba servers,
1777        # so we filter out any placeholders that don't look like a
1778        # symlink
1779        sane = []
1780        for f in files:
1781            if self.flags(f) == b'l':
1782                d = self[f].data()
1783                if (
1784                    d == b''
1785                    or len(d) >= 1024
1786                    or b'\n' in d
1787                    or stringutil.binary(d)
1788                ):
1789                    self._repo.ui.debug(
1790                        b'ignoring suspect symlink placeholder "%s"\n' % f
1791                    )
1792                    continue
1793            sane.append(f)
1794        return sane
1795
1796    def _checklookup(self, files):
1797        # check for any possibly clean files
1798        if not files:
1799            return [], [], []
1800
1801        modified = []
1802        deleted = []
1803        fixup = []
1804        pctx = self._parents[0]
1805        # do a full compare of any files that might have changed
1806        for f in sorted(files):
1807            try:
1808                # This will return True for a file that got replaced by a
1809                # directory in the interim, but fixing that is pretty hard.
1810                if (
1811                    f not in pctx
1812                    or self.flags(f) != pctx.flags(f)
1813                    or pctx[f].cmp(self[f])
1814                ):
1815                    modified.append(f)
1816                else:
1817                    fixup.append(f)
1818            except (IOError, OSError):
1819                # A file become inaccessible in between? Mark it as deleted,
1820                # matching dirstate behavior (issue5584).
1821                # The dirstate has more complex behavior around whether a
1822                # missing file matches a directory, etc, but we don't need to
1823                # bother with that: if f has made it to this point, we're sure
1824                # it's in the dirstate.
1825                deleted.append(f)
1826
1827        return modified, deleted, fixup
1828
1829    def _poststatusfixup(self, status, fixup):
1830        """update dirstate for files that are actually clean"""
1831        poststatus = self._repo.postdsstatus()
1832        if fixup or poststatus or self._repo.dirstate._dirty:
1833            try:
1834                oldid = self._repo.dirstate.identity()
1835
1836                # updating the dirstate is optional
1837                # so we don't wait on the lock
1838                # wlock can invalidate the dirstate, so cache normal _after_
1839                # taking the lock
1840                with self._repo.wlock(False):
1841                    dirstate = self._repo.dirstate
1842                    if dirstate.identity() == oldid:
1843                        if fixup:
1844                            if dirstate.pendingparentchange():
1845                                normal = lambda f: dirstate.update_file(
1846                                    f, p1_tracked=True, wc_tracked=True
1847                                )
1848                            else:
1849                                normal = dirstate.set_clean
1850                            for f in fixup:
1851                                normal(f)
1852                            # write changes out explicitly, because nesting
1853                            # wlock at runtime may prevent 'wlock.release()'
1854                            # after this block from doing so for subsequent
1855                            # changing files
1856                            tr = self._repo.currenttransaction()
1857                            self._repo.dirstate.write(tr)
1858
1859                        if poststatus:
1860                            for ps in poststatus:
1861                                ps(self, status)
1862                    else:
1863                        # in this case, writing changes out breaks
1864                        # consistency, because .hg/dirstate was
1865                        # already changed simultaneously after last
1866                        # caching (see also issue5584 for detail)
1867                        self._repo.ui.debug(
1868                            b'skip updating dirstate: identity mismatch\n'
1869                        )
1870            except error.LockError:
1871                pass
1872            finally:
1873                # Even if the wlock couldn't be grabbed, clear out the list.
1874                self._repo.clearpostdsstatus()
1875
1876    def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
1877        '''Gets the status from the dirstate -- internal use only.'''
1878        subrepos = []
1879        if b'.hgsub' in self:
1880            subrepos = sorted(self.substate)
1881        cmp, s = self._repo.dirstate.status(
1882            match, subrepos, ignored=ignored, clean=clean, unknown=unknown
1883        )
1884
1885        # check for any possibly clean files
1886        fixup = []
1887        if cmp:
1888            modified2, deleted2, fixup = self._checklookup(cmp)
1889            s.modified.extend(modified2)
1890            s.deleted.extend(deleted2)
1891
1892            if fixup and clean:
1893                s.clean.extend(fixup)
1894
1895        self._poststatusfixup(s, fixup)
1896
1897        if match.always():
1898            # cache for performance
1899            if s.unknown or s.ignored or s.clean:
1900                # "_status" is cached with list*=False in the normal route
1901                self._status = scmutil.status(
1902                    s.modified, s.added, s.removed, s.deleted, [], [], []
1903                )
1904            else:
1905                self._status = s
1906
1907        return s
1908
1909    @propertycache
1910    def _copies(self):
1911        p1copies = {}
1912        p2copies = {}
1913        parents = self._repo.dirstate.parents()
1914        p1manifest = self._repo[parents[0]].manifest()
1915        p2manifest = self._repo[parents[1]].manifest()
1916        changedset = set(self.added()) | set(self.modified())
1917        narrowmatch = self._repo.narrowmatch()
1918        for dst, src in self._repo.dirstate.copies().items():
1919            if dst not in changedset or not narrowmatch(dst):
1920                continue
1921            if src in p1manifest:
1922                p1copies[dst] = src
1923            elif src in p2manifest:
1924                p2copies[dst] = src
1925        return p1copies, p2copies
1926
1927    @propertycache
1928    def _manifest(self):
1929        """generate a manifest corresponding to the values in self._status
1930
1931        This reuse the file nodeid from parent, but we use special node
1932        identifiers for added and modified files. This is used by manifests
1933        merge to see that files are different and by update logic to avoid
1934        deleting newly added files.
1935        """
1936        return self._buildstatusmanifest(self._status)
1937
1938    def _buildstatusmanifest(self, status):
1939        """Builds a manifest that includes the given status results."""
1940        parents = self.parents()
1941
1942        man = parents[0].manifest().copy()
1943
1944        ff = self._flagfunc
1945        for i, l in (
1946            (self._repo.nodeconstants.addednodeid, status.added),
1947            (self._repo.nodeconstants.modifiednodeid, status.modified),
1948        ):
1949            for f in l:
1950                man[f] = i
1951                try:
1952                    man.setflag(f, ff(f))
1953                except OSError:
1954                    pass
1955
1956        for f in status.deleted + status.removed:
1957            if f in man:
1958                del man[f]
1959
1960        return man
1961
1962    def _buildstatus(
1963        self, other, s, match, listignored, listclean, listunknown
1964    ):
1965        """build a status with respect to another context
1966
1967        This includes logic for maintaining the fast path of status when
1968        comparing the working directory against its parent, which is to skip
1969        building a new manifest if self (working directory) is not comparing
1970        against its parent (repo['.']).
1971        """
1972        s = self._dirstatestatus(match, listignored, listclean, listunknown)
1973        # Filter out symlinks that, in the case of FAT32 and NTFS filesystems,
1974        # might have accidentally ended up with the entire contents of the file
1975        # they are supposed to be linking to.
1976        s.modified[:] = self._filtersuspectsymlink(s.modified)
1977        if other != self._repo[b'.']:
1978            s = super(workingctx, self)._buildstatus(
1979                other, s, match, listignored, listclean, listunknown
1980            )
1981        return s
1982
1983    def _matchstatus(self, other, match):
1984        """override the match method with a filter for directory patterns
1985
1986        We use inheritance to customize the match.bad method only in cases of
1987        workingctx since it belongs only to the working directory when
1988        comparing against the parent changeset.
1989
1990        If we aren't comparing against the working directory's parent, then we
1991        just use the default match object sent to us.
1992        """
1993        if other != self._repo[b'.']:
1994
1995            def bad(f, msg):
1996                # 'f' may be a directory pattern from 'match.files()',
1997                # so 'f not in ctx1' is not enough
1998                if f not in other and not other.hasdir(f):
1999                    self._repo.ui.warn(
2000                        b'%s: %s\n' % (self._repo.dirstate.pathto(f), msg)
2001                    )
2002
2003            match.bad = bad
2004        return match
2005
2006    def walk(self, match):
2007        '''Generates matching file names.'''
2008        return sorted(
2009            self._repo.dirstate.walk(
2010                self._repo.narrowmatch(match),
2011                subrepos=sorted(self.substate),
2012                unknown=True,
2013                ignored=False,
2014            )
2015        )
2016
2017    def matches(self, match):
2018        match = self._repo.narrowmatch(match)
2019        ds = self._repo.dirstate
2020        return sorted(f for f in ds.matches(match) if ds.get_entry(f).tracked)
2021
2022    def markcommitted(self, node):
2023        with self._repo.dirstate.parentchange():
2024            for f in self.modified() + self.added():
2025                self._repo.dirstate.update_file(
2026                    f, p1_tracked=True, wc_tracked=True
2027                )
2028            for f in self.removed():
2029                self._repo.dirstate.update_file(
2030                    f, p1_tracked=False, wc_tracked=False
2031                )
2032            self._repo.dirstate.setparents(node)
2033            self._repo._quick_access_changeid_invalidate()
2034
2035            sparse.aftercommit(self._repo, node)
2036
2037        # write changes out explicitly, because nesting wlock at
2038        # runtime may prevent 'wlock.release()' in 'repo.commit()'
2039        # from immediately doing so for subsequent changing files
2040        self._repo.dirstate.write(self._repo.currenttransaction())
2041
2042    def mergestate(self, clean=False):
2043        if clean:
2044            return mergestatemod.mergestate.clean(self._repo)
2045        return mergestatemod.mergestate.read(self._repo)
2046
2047
2048class committablefilectx(basefilectx):
2049    """A committablefilectx provides common functionality for a file context
2050    that wants the ability to commit, e.g. workingfilectx or memfilectx."""
2051
2052    def __init__(self, repo, path, filelog=None, ctx=None):
2053        self._repo = repo
2054        self._path = path
2055        self._changeid = None
2056        self._filerev = self._filenode = None
2057
2058        if filelog is not None:
2059            self._filelog = filelog
2060        if ctx:
2061            self._changectx = ctx
2062
2063    def __nonzero__(self):
2064        return True
2065
2066    __bool__ = __nonzero__
2067
2068    def linkrev(self):
2069        # linked to self._changectx no matter if file is modified or not
2070        return self.rev()
2071
2072    def renamed(self):
2073        path = self.copysource()
2074        if not path:
2075            return None
2076        return (
2077            path,
2078            self._changectx._parents[0]._manifest.get(
2079                path, self._repo.nodeconstants.nullid
2080            ),
2081        )
2082
2083    def parents(self):
2084        '''return parent filectxs, following copies if necessary'''
2085
2086        def filenode(ctx, path):
2087            return ctx._manifest.get(path, self._repo.nodeconstants.nullid)
2088
2089        path = self._path
2090        fl = self._filelog
2091        pcl = self._changectx._parents
2092        renamed = self.renamed()
2093
2094        if renamed:
2095            pl = [renamed + (None,)]
2096        else:
2097            pl = [(path, filenode(pcl[0], path), fl)]
2098
2099        for pc in pcl[1:]:
2100            pl.append((path, filenode(pc, path), fl))
2101
2102        return [
2103            self._parentfilectx(p, fileid=n, filelog=l)
2104            for p, n, l in pl
2105            if n != self._repo.nodeconstants.nullid
2106        ]
2107
2108    def children(self):
2109        return []
2110
2111
2112class workingfilectx(committablefilectx):
2113    """A workingfilectx object makes access to data related to a particular
2114    file in the working directory convenient."""
2115
2116    def __init__(self, repo, path, filelog=None, workingctx=None):
2117        super(workingfilectx, self).__init__(repo, path, filelog, workingctx)
2118
2119    @propertycache
2120    def _changectx(self):
2121        return workingctx(self._repo)
2122
2123    def data(self):
2124        return self._repo.wread(self._path)
2125
2126    def copysource(self):
2127        return self._repo.dirstate.copied(self._path)
2128
2129    def size(self):
2130        return self._repo.wvfs.lstat(self._path).st_size
2131
2132    def lstat(self):
2133        return self._repo.wvfs.lstat(self._path)
2134
2135    def date(self):
2136        t, tz = self._changectx.date()
2137        try:
2138            return (self._repo.wvfs.lstat(self._path)[stat.ST_MTIME], tz)
2139        except OSError as err:
2140            if err.errno != errno.ENOENT:
2141                raise
2142            return (t, tz)
2143
2144    def exists(self):
2145        return self._repo.wvfs.exists(self._path)
2146
2147    def lexists(self):
2148        return self._repo.wvfs.lexists(self._path)
2149
2150    def audit(self):
2151        return self._repo.wvfs.audit(self._path)
2152
2153    def cmp(self, fctx):
2154        """compare with other file context
2155
2156        returns True if different than fctx.
2157        """
2158        # fctx should be a filectx (not a workingfilectx)
2159        # invert comparison to reuse the same code path
2160        return fctx.cmp(self)
2161
2162    def remove(self, ignoremissing=False):
2163        """wraps unlink for a repo's working directory"""
2164        rmdir = self._repo.ui.configbool(b'experimental', b'removeemptydirs')
2165        self._repo.wvfs.unlinkpath(
2166            self._path, ignoremissing=ignoremissing, rmdir=rmdir
2167        )
2168
2169    def write(self, data, flags, backgroundclose=False, **kwargs):
2170        """wraps repo.wwrite"""
2171        return self._repo.wwrite(
2172            self._path, data, flags, backgroundclose=backgroundclose, **kwargs
2173        )
2174
2175    def markcopied(self, src):
2176        """marks this file a copy of `src`"""
2177        self._repo.dirstate.copy(src, self._path)
2178
2179    def clearunknown(self):
2180        """Removes conflicting items in the working directory so that
2181        ``write()`` can be called successfully.
2182        """
2183        wvfs = self._repo.wvfs
2184        f = self._path
2185        wvfs.audit(f)
2186        if self._repo.ui.configbool(
2187            b'experimental', b'merge.checkpathconflicts'
2188        ):
2189            # remove files under the directory as they should already be
2190            # warned and backed up
2191            if wvfs.isdir(f) and not wvfs.islink(f):
2192                wvfs.rmtree(f, forcibly=True)
2193            for p in reversed(list(pathutil.finddirs(f))):
2194                if wvfs.isfileorlink(p):
2195                    wvfs.unlink(p)
2196                    break
2197        else:
2198            # don't remove files if path conflicts are not processed
2199            if wvfs.isdir(f) and not wvfs.islink(f):
2200                wvfs.removedirs(f)
2201
2202    def setflags(self, l, x):
2203        self._repo.wvfs.setflags(self._path, l, x)
2204
2205
2206class overlayworkingctx(committablectx):
2207    """Wraps another mutable context with a write-back cache that can be
2208    converted into a commit context.
2209
2210    self._cache[path] maps to a dict with keys: {
2211        'exists': bool?
2212        'date': date?
2213        'data': str?
2214        'flags': str?
2215        'copied': str? (path or None)
2216    }
2217    If `exists` is True, `flags` must be non-None and 'date' is non-None. If it
2218    is `False`, the file was deleted.
2219    """
2220
2221    def __init__(self, repo):
2222        super(overlayworkingctx, self).__init__(repo)
2223        self.clean()
2224
2225    def setbase(self, wrappedctx):
2226        self._wrappedctx = wrappedctx
2227        self._parents = [wrappedctx]
2228        # Drop old manifest cache as it is now out of date.
2229        # This is necessary when, e.g., rebasing several nodes with one
2230        # ``overlayworkingctx`` (e.g. with --collapse).
2231        util.clearcachedproperty(self, b'_manifest')
2232
2233    def setparents(self, p1node, p2node=None):
2234        if p2node is None:
2235            p2node = self._repo.nodeconstants.nullid
2236        assert p1node == self._wrappedctx.node()
2237        self._parents = [self._wrappedctx, self._repo.unfiltered()[p2node]]
2238
2239    def data(self, path):
2240        if self.isdirty(path):
2241            if self._cache[path][b'exists']:
2242                if self._cache[path][b'data'] is not None:
2243                    return self._cache[path][b'data']
2244                else:
2245                    # Must fallback here, too, because we only set flags.
2246                    return self._wrappedctx[path].data()
2247            else:
2248                raise error.ProgrammingError(
2249                    b"No such file or directory: %s" % path
2250                )
2251        else:
2252            return self._wrappedctx[path].data()
2253
2254    @propertycache
2255    def _manifest(self):
2256        parents = self.parents()
2257        man = parents[0].manifest().copy()
2258
2259        flag = self._flagfunc
2260        for path in self.added():
2261            man[path] = self._repo.nodeconstants.addednodeid
2262            man.setflag(path, flag(path))
2263        for path in self.modified():
2264            man[path] = self._repo.nodeconstants.modifiednodeid
2265            man.setflag(path, flag(path))
2266        for path in self.removed():
2267            del man[path]
2268        return man
2269
2270    @propertycache
2271    def _flagfunc(self):
2272        def f(path):
2273            return self._cache[path][b'flags']
2274
2275        return f
2276
2277    def files(self):
2278        return sorted(self.added() + self.modified() + self.removed())
2279
2280    def modified(self):
2281        return [
2282            f
2283            for f in self._cache.keys()
2284            if self._cache[f][b'exists'] and self._existsinparent(f)
2285        ]
2286
2287    def added(self):
2288        return [
2289            f
2290            for f in self._cache.keys()
2291            if self._cache[f][b'exists'] and not self._existsinparent(f)
2292        ]
2293
2294    def removed(self):
2295        return [
2296            f
2297            for f in self._cache.keys()
2298            if not self._cache[f][b'exists'] and self._existsinparent(f)
2299        ]
2300
2301    def p1copies(self):
2302        copies = {}
2303        narrowmatch = self._repo.narrowmatch()
2304        for f in self._cache.keys():
2305            if not narrowmatch(f):
2306                continue
2307            copies.pop(f, None)  # delete if it exists
2308            source = self._cache[f][b'copied']
2309            if source:
2310                copies[f] = source
2311        return copies
2312
2313    def p2copies(self):
2314        copies = {}
2315        narrowmatch = self._repo.narrowmatch()
2316        for f in self._cache.keys():
2317            if not narrowmatch(f):
2318                continue
2319            copies.pop(f, None)  # delete if it exists
2320            source = self._cache[f][b'copied']
2321            if source:
2322                copies[f] = source
2323        return copies
2324
2325    def isinmemory(self):
2326        return True
2327
2328    def filedate(self, path):
2329        if self.isdirty(path):
2330            return self._cache[path][b'date']
2331        else:
2332            return self._wrappedctx[path].date()
2333
2334    def markcopied(self, path, origin):
2335        self._markdirty(
2336            path,
2337            exists=True,
2338            date=self.filedate(path),
2339            flags=self.flags(path),
2340            copied=origin,
2341        )
2342
2343    def copydata(self, path):
2344        if self.isdirty(path):
2345            return self._cache[path][b'copied']
2346        else:
2347            return None
2348
2349    def flags(self, path):
2350        if self.isdirty(path):
2351            if self._cache[path][b'exists']:
2352                return self._cache[path][b'flags']
2353            else:
2354                raise error.ProgrammingError(
2355                    b"No such file or directory: %s" % path
2356                )
2357        else:
2358            return self._wrappedctx[path].flags()
2359
2360    def __contains__(self, key):
2361        if key in self._cache:
2362            return self._cache[key][b'exists']
2363        return key in self.p1()
2364
2365    def _existsinparent(self, path):
2366        try:
2367            # ``commitctx` raises a ``ManifestLookupError`` if a path does not
2368            # exist, unlike ``workingctx``, which returns a ``workingfilectx``
2369            # with an ``exists()`` function.
2370            self._wrappedctx[path]
2371            return True
2372        except error.ManifestLookupError:
2373            return False
2374
2375    def _auditconflicts(self, path):
2376        """Replicates conflict checks done by wvfs.write().
2377
2378        Since we never write to the filesystem and never call `applyupdates` in
2379        IMM, we'll never check that a path is actually writable -- e.g., because
2380        it adds `a/foo`, but `a` is actually a file in the other commit.
2381        """
2382
2383        def fail(path, component):
2384            # p1() is the base and we're receiving "writes" for p2()'s
2385            # files.
2386            if b'l' in self.p1()[component].flags():
2387                raise error.Abort(
2388                    b"error: %s conflicts with symlink %s "
2389                    b"in %d." % (path, component, self.p1().rev())
2390                )
2391            else:
2392                raise error.Abort(
2393                    b"error: '%s' conflicts with file '%s' in "
2394                    b"%d." % (path, component, self.p1().rev())
2395                )
2396
2397        # Test that each new directory to be created to write this path from p2
2398        # is not a file in p1.
2399        components = path.split(b'/')
2400        for i in pycompat.xrange(len(components)):
2401            component = b"/".join(components[0:i])
2402            if component in self:
2403                fail(path, component)
2404
2405        # Test the other direction -- that this path from p2 isn't a directory
2406        # in p1 (test that p1 doesn't have any paths matching `path/*`).
2407        match = self.match([path], default=b'path')
2408        mfiles = list(self.p1().manifest().walk(match))
2409        if len(mfiles) > 0:
2410            if len(mfiles) == 1 and mfiles[0] == path:
2411                return
2412            # omit the files which are deleted in current IMM wctx
2413            mfiles = [m for m in mfiles if m in self]
2414            if not mfiles:
2415                return
2416            raise error.Abort(
2417                b"error: file '%s' cannot be written because "
2418                b" '%s/' is a directory in %s (containing %d "
2419                b"entries: %s)"
2420                % (path, path, self.p1(), len(mfiles), b', '.join(mfiles))
2421            )
2422
2423    def write(self, path, data, flags=b'', **kwargs):
2424        if data is None:
2425            raise error.ProgrammingError(b"data must be non-None")
2426        self._auditconflicts(path)
2427        self._markdirty(
2428            path, exists=True, data=data, date=dateutil.makedate(), flags=flags
2429        )
2430
2431    def setflags(self, path, l, x):
2432        flag = b''
2433        if l:
2434            flag = b'l'
2435        elif x:
2436            flag = b'x'
2437        self._markdirty(path, exists=True, date=dateutil.makedate(), flags=flag)
2438
2439    def remove(self, path):
2440        self._markdirty(path, exists=False)
2441
2442    def exists(self, path):
2443        """exists behaves like `lexists`, but needs to follow symlinks and
2444        return False if they are broken.
2445        """
2446        if self.isdirty(path):
2447            # If this path exists and is a symlink, "follow" it by calling
2448            # exists on the destination path.
2449            if (
2450                self._cache[path][b'exists']
2451                and b'l' in self._cache[path][b'flags']
2452            ):
2453                return self.exists(self._cache[path][b'data'].strip())
2454            else:
2455                return self._cache[path][b'exists']
2456
2457        return self._existsinparent(path)
2458
2459    def lexists(self, path):
2460        """lexists returns True if the path exists"""
2461        if self.isdirty(path):
2462            return self._cache[path][b'exists']
2463
2464        return self._existsinparent(path)
2465
2466    def size(self, path):
2467        if self.isdirty(path):
2468            if self._cache[path][b'exists']:
2469                return len(self._cache[path][b'data'])
2470            else:
2471                raise error.ProgrammingError(
2472                    b"No such file or directory: %s" % path
2473                )
2474        return self._wrappedctx[path].size()
2475
2476    def tomemctx(
2477        self,
2478        text,
2479        branch=None,
2480        extra=None,
2481        date=None,
2482        parents=None,
2483        user=None,
2484        editor=None,
2485    ):
2486        """Converts this ``overlayworkingctx`` into a ``memctx`` ready to be
2487        committed.
2488
2489        ``text`` is the commit message.
2490        ``parents`` (optional) are rev numbers.
2491        """
2492        # Default parents to the wrapped context if not passed.
2493        if parents is None:
2494            parents = self.parents()
2495            if len(parents) == 1:
2496                parents = (parents[0], None)
2497
2498        # ``parents`` is passed as rev numbers; convert to ``commitctxs``.
2499        if parents[1] is None:
2500            parents = (self._repo[parents[0]], None)
2501        else:
2502            parents = (self._repo[parents[0]], self._repo[parents[1]])
2503
2504        files = self.files()
2505
2506        def getfile(repo, memctx, path):
2507            if self._cache[path][b'exists']:
2508                return memfilectx(
2509                    repo,
2510                    memctx,
2511                    path,
2512                    self._cache[path][b'data'],
2513                    b'l' in self._cache[path][b'flags'],
2514                    b'x' in self._cache[path][b'flags'],
2515                    self._cache[path][b'copied'],
2516                )
2517            else:
2518                # Returning None, but including the path in `files`, is
2519                # necessary for memctx to register a deletion.
2520                return None
2521
2522        if branch is None:
2523            branch = self._wrappedctx.branch()
2524
2525        return memctx(
2526            self._repo,
2527            parents,
2528            text,
2529            files,
2530            getfile,
2531            date=date,
2532            extra=extra,
2533            user=user,
2534            branch=branch,
2535            editor=editor,
2536        )
2537
2538    def tomemctx_for_amend(self, precursor):
2539        extra = precursor.extra().copy()
2540        extra[b'amend_source'] = precursor.hex()
2541        return self.tomemctx(
2542            text=precursor.description(),
2543            branch=precursor.branch(),
2544            extra=extra,
2545            date=precursor.date(),
2546            user=precursor.user(),
2547        )
2548
2549    def isdirty(self, path):
2550        return path in self._cache
2551
2552    def clean(self):
2553        self._mergestate = None
2554        self._cache = {}
2555
2556    def _compact(self):
2557        """Removes keys from the cache that are actually clean, by comparing
2558        them with the underlying context.
2559
2560        This can occur during the merge process, e.g. by passing --tool :local
2561        to resolve a conflict.
2562        """
2563        keys = []
2564        # This won't be perfect, but can help performance significantly when
2565        # using things like remotefilelog.
2566        scmutil.prefetchfiles(
2567            self.repo(),
2568            [
2569                (
2570                    self.p1().rev(),
2571                    scmutil.matchfiles(self.repo(), self._cache.keys()),
2572                )
2573            ],
2574        )
2575
2576        for path in self._cache.keys():
2577            cache = self._cache[path]
2578            try:
2579                underlying = self._wrappedctx[path]
2580                if (
2581                    underlying.data() == cache[b'data']
2582                    and underlying.flags() == cache[b'flags']
2583                ):
2584                    keys.append(path)
2585            except error.ManifestLookupError:
2586                # Path not in the underlying manifest (created).
2587                continue
2588
2589        for path in keys:
2590            del self._cache[path]
2591        return keys
2592
2593    def _markdirty(
2594        self, path, exists, data=None, date=None, flags=b'', copied=None
2595    ):
2596        # data not provided, let's see if we already have some; if not, let's
2597        # grab it from our underlying context, so that we always have data if
2598        # the file is marked as existing.
2599        if exists and data is None:
2600            oldentry = self._cache.get(path) or {}
2601            data = oldentry.get(b'data')
2602            if data is None:
2603                data = self._wrappedctx[path].data()
2604
2605        self._cache[path] = {
2606            b'exists': exists,
2607            b'data': data,
2608            b'date': date,
2609            b'flags': flags,
2610            b'copied': copied,
2611        }
2612        util.clearcachedproperty(self, b'_manifest')
2613
2614    def filectx(self, path, filelog=None):
2615        return overlayworkingfilectx(
2616            self._repo, path, parent=self, filelog=filelog
2617        )
2618
2619    def mergestate(self, clean=False):
2620        if clean or self._mergestate is None:
2621            self._mergestate = mergestatemod.memmergestate(self._repo)
2622        return self._mergestate
2623
2624
2625class overlayworkingfilectx(committablefilectx):
2626    """Wrap a ``workingfilectx`` but intercepts all writes into an in-memory
2627    cache, which can be flushed through later by calling ``flush()``."""
2628
2629    def __init__(self, repo, path, filelog=None, parent=None):
2630        super(overlayworkingfilectx, self).__init__(repo, path, filelog, parent)
2631        self._repo = repo
2632        self._parent = parent
2633        self._path = path
2634
2635    def cmp(self, fctx):
2636        return self.data() != fctx.data()
2637
2638    def changectx(self):
2639        return self._parent
2640
2641    def data(self):
2642        return self._parent.data(self._path)
2643
2644    def date(self):
2645        return self._parent.filedate(self._path)
2646
2647    def exists(self):
2648        return self.lexists()
2649
2650    def lexists(self):
2651        return self._parent.exists(self._path)
2652
2653    def copysource(self):
2654        return self._parent.copydata(self._path)
2655
2656    def size(self):
2657        return self._parent.size(self._path)
2658
2659    def markcopied(self, origin):
2660        self._parent.markcopied(self._path, origin)
2661
2662    def audit(self):
2663        pass
2664
2665    def flags(self):
2666        return self._parent.flags(self._path)
2667
2668    def setflags(self, islink, isexec):
2669        return self._parent.setflags(self._path, islink, isexec)
2670
2671    def write(self, data, flags, backgroundclose=False, **kwargs):
2672        return self._parent.write(self._path, data, flags, **kwargs)
2673
2674    def remove(self, ignoremissing=False):
2675        return self._parent.remove(self._path)
2676
2677    def clearunknown(self):
2678        pass
2679
2680
2681class workingcommitctx(workingctx):
2682    """A workingcommitctx object makes access to data related to
2683    the revision being committed convenient.
2684
2685    This hides changes in the working directory, if they aren't
2686    committed in this context.
2687    """
2688
2689    def __init__(
2690        self, repo, changes, text=b"", user=None, date=None, extra=None
2691    ):
2692        super(workingcommitctx, self).__init__(
2693            repo, text, user, date, extra, changes
2694        )
2695
2696    def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
2697        """Return matched files only in ``self._status``
2698
2699        Uncommitted files appear "clean" via this context, even if
2700        they aren't actually so in the working directory.
2701        """
2702        if clean:
2703            clean = [f for f in self._manifest if f not in self._changedset]
2704        else:
2705            clean = []
2706        return scmutil.status(
2707            [f for f in self._status.modified if match(f)],
2708            [f for f in self._status.added if match(f)],
2709            [f for f in self._status.removed if match(f)],
2710            [],
2711            [],
2712            [],
2713            clean,
2714        )
2715
2716    @propertycache
2717    def _changedset(self):
2718        """Return the set of files changed in this context"""
2719        changed = set(self._status.modified)
2720        changed.update(self._status.added)
2721        changed.update(self._status.removed)
2722        return changed
2723
2724
2725def makecachingfilectxfn(func):
2726    """Create a filectxfn that caches based on the path.
2727
2728    We can't use util.cachefunc because it uses all arguments as the cache
2729    key and this creates a cycle since the arguments include the repo and
2730    memctx.
2731    """
2732    cache = {}
2733
2734    def getfilectx(repo, memctx, path):
2735        if path not in cache:
2736            cache[path] = func(repo, memctx, path)
2737        return cache[path]
2738
2739    return getfilectx
2740
2741
2742def memfilefromctx(ctx):
2743    """Given a context return a memfilectx for ctx[path]
2744
2745    This is a convenience method for building a memctx based on another
2746    context.
2747    """
2748
2749    def getfilectx(repo, memctx, path):
2750        fctx = ctx[path]
2751        copysource = fctx.copysource()
2752        return memfilectx(
2753            repo,
2754            memctx,
2755            path,
2756            fctx.data(),
2757            islink=fctx.islink(),
2758            isexec=fctx.isexec(),
2759            copysource=copysource,
2760        )
2761
2762    return getfilectx
2763
2764
2765def memfilefrompatch(patchstore):
2766    """Given a patch (e.g. patchstore object) return a memfilectx
2767
2768    This is a convenience method for building a memctx based on a patchstore.
2769    """
2770
2771    def getfilectx(repo, memctx, path):
2772        data, mode, copysource = patchstore.getfile(path)
2773        if data is None:
2774            return None
2775        islink, isexec = mode
2776        return memfilectx(
2777            repo,
2778            memctx,
2779            path,
2780            data,
2781            islink=islink,
2782            isexec=isexec,
2783            copysource=copysource,
2784        )
2785
2786    return getfilectx
2787
2788
2789class memctx(committablectx):
2790    """Use memctx to perform in-memory commits via localrepo.commitctx().
2791
2792    Revision information is supplied at initialization time while
2793    related files data and is made available through a callback
2794    mechanism.  'repo' is the current localrepo, 'parents' is a
2795    sequence of two parent revisions identifiers (pass None for every
2796    missing parent), 'text' is the commit message and 'files' lists
2797    names of files touched by the revision (normalized and relative to
2798    repository root).
2799
2800    filectxfn(repo, memctx, path) is a callable receiving the
2801    repository, the current memctx object and the normalized path of
2802    requested file, relative to repository root. It is fired by the
2803    commit function for every file in 'files', but calls order is
2804    undefined. If the file is available in the revision being
2805    committed (updated or added), filectxfn returns a memfilectx
2806    object. If the file was removed, filectxfn return None for recent
2807    Mercurial. Moved files are represented by marking the source file
2808    removed and the new file added with copy information (see
2809    memfilectx).
2810
2811    user receives the committer name and defaults to current
2812    repository username, date is the commit date in any format
2813    supported by dateutil.parsedate() and defaults to current date, extra
2814    is a dictionary of metadata or is left empty.
2815    """
2816
2817    # Mercurial <= 3.1 expects the filectxfn to raise IOError for missing files.
2818    # Extensions that need to retain compatibility across Mercurial 3.1 can use
2819    # this field to determine what to do in filectxfn.
2820    _returnnoneformissingfiles = True
2821
2822    def __init__(
2823        self,
2824        repo,
2825        parents,
2826        text,
2827        files,
2828        filectxfn,
2829        user=None,
2830        date=None,
2831        extra=None,
2832        branch=None,
2833        editor=None,
2834    ):
2835        super(memctx, self).__init__(
2836            repo, text, user, date, extra, branch=branch
2837        )
2838        self._rev = None
2839        self._node = None
2840        parents = [(p or self._repo.nodeconstants.nullid) for p in parents]
2841        p1, p2 = parents
2842        self._parents = [self._repo[p] for p in (p1, p2)]
2843        files = sorted(set(files))
2844        self._files = files
2845        self.substate = {}
2846
2847        if isinstance(filectxfn, patch.filestore):
2848            filectxfn = memfilefrompatch(filectxfn)
2849        elif not callable(filectxfn):
2850            # if store is not callable, wrap it in a function
2851            filectxfn = memfilefromctx(filectxfn)
2852
2853        # memoizing increases performance for e.g. vcs convert scenarios.
2854        self._filectxfn = makecachingfilectxfn(filectxfn)
2855
2856        if editor:
2857            self._text = editor(self._repo, self, [])
2858            self._repo.savecommitmessage(self._text)
2859
2860    def filectx(self, path, filelog=None):
2861        """get a file context from the working directory
2862
2863        Returns None if file doesn't exist and should be removed."""
2864        return self._filectxfn(self._repo, self, path)
2865
2866    def commit(self):
2867        """commit context to the repo"""
2868        return self._repo.commitctx(self)
2869
2870    @propertycache
2871    def _manifest(self):
2872        """generate a manifest based on the return values of filectxfn"""
2873
2874        # keep this simple for now; just worry about p1
2875        pctx = self._parents[0]
2876        man = pctx.manifest().copy()
2877
2878        for f in self._status.modified:
2879            man[f] = self._repo.nodeconstants.modifiednodeid
2880
2881        for f in self._status.added:
2882            man[f] = self._repo.nodeconstants.addednodeid
2883
2884        for f in self._status.removed:
2885            if f in man:
2886                del man[f]
2887
2888        return man
2889
2890    @propertycache
2891    def _status(self):
2892        """Calculate exact status from ``files`` specified at construction"""
2893        man1 = self.p1().manifest()
2894        p2 = self._parents[1]
2895        # "1 < len(self._parents)" can't be used for checking
2896        # existence of the 2nd parent, because "memctx._parents" is
2897        # explicitly initialized by the list, of which length is 2.
2898        if p2.rev() != nullrev:
2899            man2 = p2.manifest()
2900            managing = lambda f: f in man1 or f in man2
2901        else:
2902            managing = lambda f: f in man1
2903
2904        modified, added, removed = [], [], []
2905        for f in self._files:
2906            if not managing(f):
2907                added.append(f)
2908            elif self[f]:
2909                modified.append(f)
2910            else:
2911                removed.append(f)
2912
2913        return scmutil.status(modified, added, removed, [], [], [], [])
2914
2915    def parents(self):
2916        if self._parents[1].rev() == nullrev:
2917            return [self._parents[0]]
2918        return self._parents
2919
2920
2921class memfilectx(committablefilectx):
2922    """memfilectx represents an in-memory file to commit.
2923
2924    See memctx and committablefilectx for more details.
2925    """
2926
2927    def __init__(
2928        self,
2929        repo,
2930        changectx,
2931        path,
2932        data,
2933        islink=False,
2934        isexec=False,
2935        copysource=None,
2936    ):
2937        """
2938        path is the normalized file path relative to repository root.
2939        data is the file content as a string.
2940        islink is True if the file is a symbolic link.
2941        isexec is True if the file is executable.
2942        copied is the source file path if current file was copied in the
2943        revision being committed, or None."""
2944        super(memfilectx, self).__init__(repo, path, None, changectx)
2945        self._data = data
2946        if islink:
2947            self._flags = b'l'
2948        elif isexec:
2949            self._flags = b'x'
2950        else:
2951            self._flags = b''
2952        self._copysource = copysource
2953
2954    def copysource(self):
2955        return self._copysource
2956
2957    def cmp(self, fctx):
2958        return self.data() != fctx.data()
2959
2960    def data(self):
2961        return self._data
2962
2963    def remove(self, ignoremissing=False):
2964        """wraps unlink for a repo's working directory"""
2965        # need to figure out what to do here
2966        del self._changectx[self._path]
2967
2968    def write(self, data, flags, **kwargs):
2969        """wraps repo.wwrite"""
2970        self._data = data
2971
2972
2973class metadataonlyctx(committablectx):
2974    """Like memctx but it's reusing the manifest of different commit.
2975    Intended to be used by lightweight operations that are creating
2976    metadata-only changes.
2977
2978    Revision information is supplied at initialization time.  'repo' is the
2979    current localrepo, 'ctx' is original revision which manifest we're reuisng
2980    'parents' is a sequence of two parent revisions identifiers (pass None for
2981    every missing parent), 'text' is the commit.
2982
2983    user receives the committer name and defaults to current repository
2984    username, date is the commit date in any format supported by
2985    dateutil.parsedate() and defaults to current date, extra is a dictionary of
2986    metadata or is left empty.
2987    """
2988
2989    def __init__(
2990        self,
2991        repo,
2992        originalctx,
2993        parents=None,
2994        text=None,
2995        user=None,
2996        date=None,
2997        extra=None,
2998        editor=None,
2999    ):
3000        if text is None:
3001            text = originalctx.description()
3002        super(metadataonlyctx, self).__init__(repo, text, user, date, extra)
3003        self._rev = None
3004        self._node = None
3005        self._originalctx = originalctx
3006        self._manifestnode = originalctx.manifestnode()
3007        if parents is None:
3008            parents = originalctx.parents()
3009        else:
3010            parents = [repo[p] for p in parents if p is not None]
3011        parents = parents[:]
3012        while len(parents) < 2:
3013            parents.append(repo[nullrev])
3014        p1, p2 = self._parents = parents
3015
3016        # sanity check to ensure that the reused manifest parents are
3017        # manifests of our commit parents
3018        mp1, mp2 = self.manifestctx().parents
3019        if p1 != self._repo.nodeconstants.nullid and p1.manifestnode() != mp1:
3020            raise RuntimeError(
3021                r"can't reuse the manifest: its p1 "
3022                r"doesn't match the new ctx p1"
3023            )
3024        if p2 != self._repo.nodeconstants.nullid and p2.manifestnode() != mp2:
3025            raise RuntimeError(
3026                r"can't reuse the manifest: "
3027                r"its p2 doesn't match the new ctx p2"
3028            )
3029
3030        self._files = originalctx.files()
3031        self.substate = {}
3032
3033        if editor:
3034            self._text = editor(self._repo, self, [])
3035            self._repo.savecommitmessage(self._text)
3036
3037    def manifestnode(self):
3038        return self._manifestnode
3039
3040    @property
3041    def _manifestctx(self):
3042        return self._repo.manifestlog[self._manifestnode]
3043
3044    def filectx(self, path, filelog=None):
3045        return self._originalctx.filectx(path, filelog=filelog)
3046
3047    def commit(self):
3048        """commit context to the repo"""
3049        return self._repo.commitctx(self)
3050
3051    @property
3052    def _manifest(self):
3053        return self._originalctx.manifest()
3054
3055    @propertycache
3056    def _status(self):
3057        """Calculate exact status from ``files`` specified in the ``origctx``
3058        and parents manifests.
3059        """
3060        man1 = self.p1().manifest()
3061        p2 = self._parents[1]
3062        # "1 < len(self._parents)" can't be used for checking
3063        # existence of the 2nd parent, because "metadataonlyctx._parents" is
3064        # explicitly initialized by the list, of which length is 2.
3065        if p2.rev() != nullrev:
3066            man2 = p2.manifest()
3067            managing = lambda f: f in man1 or f in man2
3068        else:
3069            managing = lambda f: f in man1
3070
3071        modified, added, removed = [], [], []
3072        for f in self._files:
3073            if not managing(f):
3074                added.append(f)
3075            elif f in self:
3076                modified.append(f)
3077            else:
3078                removed.append(f)
3079
3080        return scmutil.status(modified, added, removed, [], [], [], [])
3081
3082
3083class arbitraryfilectx(object):
3084    """Allows you to use filectx-like functions on a file in an arbitrary
3085    location on disk, possibly not in the working directory.
3086    """
3087
3088    def __init__(self, path, repo=None):
3089        # Repo is optional because contrib/simplemerge uses this class.
3090        self._repo = repo
3091        self._path = path
3092
3093    def cmp(self, fctx):
3094        # filecmp follows symlinks whereas `cmp` should not, so skip the fast
3095        # path if either side is a symlink.
3096        symlinks = b'l' in self.flags() or b'l' in fctx.flags()
3097        if not symlinks and isinstance(fctx, workingfilectx) and self._repo:
3098            # Add a fast-path for merge if both sides are disk-backed.
3099            # Note that filecmp uses the opposite return values (True if same)
3100            # from our cmp functions (True if different).
3101            return not filecmp.cmp(self.path(), self._repo.wjoin(fctx.path()))
3102        return self.data() != fctx.data()
3103
3104    def path(self):
3105        return self._path
3106
3107    def flags(self):
3108        return b''
3109
3110    def data(self):
3111        return util.readfile(self._path)
3112
3113    def decodeddata(self):
3114        with open(self._path, b"rb") as f:
3115            return f.read()
3116
3117    def remove(self):
3118        util.unlink(self._path)
3119
3120    def write(self, data, flags, **kwargs):
3121        assert not flags
3122        with open(self._path, b"wb") as f:
3123            f.write(data)
3124