File: gitdirstate.py

package info (click to toggle)
hg-git 1.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 1,244 kB
  • sloc: python: 8,702; sh: 185; makefile: 23
file content (265 lines) | stat: -rw-r--r-- 9,147 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
import os
import stat
import re
import errno

from mercurial import (
    dirstate,
    error,
    exthelper,
    match as matchmod,
    pathutil,
    pycompat,
    util,
)

from mercurial.i18n import _

from . import git_handler
from . import gitrepo

eh = exthelper.exthelper()


def gignorepats(orig, lines, root=None):
    '''parse lines (iterable) of .gitignore text, returning a tuple of
    (patterns, parse errors). These patterns should be given to compile()
    to be validated and converted into a match function.'''
    syntaxes = {b're': b'relre:', b'regexp': b'relre:', b'glob': b'relglob:'}
    syntax = b'glob:'

    patterns = []
    warnings = []

    for line in lines:
        if b"#" in line:
            _commentre = re.compile(br'((^|[^\\])(\\\\)*)#.*')
            # remove comments prefixed by an even number of escapes
            line = _commentre.sub(br'\1', line)
            # fixup properly escaped comments that survived the above
            line = line.replace(b"\\#", b"#")
        line = line.rstrip()
        if not line:
            continue

        if line.startswith(b'!'):
            warnings.append(_(b"unsupported ignore pattern '%s'") % line)
            continue
        if re.match(br'(:?.*/)?\.hg(:?/|$)', line):
            continue
        rootprefix = b'%s/' % root if root else b''
        if line and line[0] in br'\/':
            line = line[1:]
            rootsuffixes = [b'']
        else:
            rootsuffixes = [b'', b'**/']
        for rootsuffix in rootsuffixes:
            pat = syntax + rootprefix + rootsuffix + line
            for s, rels in syntaxes.items():
                if line.startswith(rels):
                    pat = line
                    break
                elif line.startswith(s + b':'):
                    pat = rels + line[len(s) + 1 :]
                    break
            patterns.append(pat)

    return patterns, warnings


def gignore(root, files, ui, extrapatterns=None):
    allpats = []
    pats = [(f, [b'include:%s' % f]) for f in files]
    for f, patlist in pats:
        allpats.extend(patlist)

    if extrapatterns:
        allpats.extend(extrapatterns)
    if not allpats:
        return util.never
    try:
        ignorefunc = matchmod.match(root, b'', [], allpats)
    except error.Abort:
        ui.traceback()
        for f, patlist in pats:
            matchmod.match(root, b'', [], patlist)
        if extrapatterns:
            matchmod.match(root, b'', [], extrapatterns)
    return ignorefunc


class gitdirstate(dirstate.dirstate):
    @dirstate.rootcache(b'.hgignore')
    def _ignore(self):
        files = [self._join(b'.hgignore')]
        for name, path in self._ui.configitems(b"ui"):
            if name == b'ignore' or name.startswith(b'ignore.'):
                files.append(util.expandpath(path))
        patterns = []
        # Only use .gitignore if there's no .hgignore
        if not os.access(files[0], os.R_OK):
            for fn in self._finddotgitignores():
                d = os.path.dirname(fn)
                fn = self.pathto(fn)
                if not os.path.exists(fn):
                    continue
                fp = open(fn, 'rb')
                pats, warnings = gignorepats(None, fp, root=d)
                for warning in warnings:
                    self._ui.warn(b"%s: %s\n" % (fn, warning))
                patterns.extend(pats)
        return gignore(self._root, files, self._ui, extrapatterns=patterns)

    def _finddotgitignores(self):
        """A copy of dirstate.walk. This is called from the new _ignore method,
        which is called by dirstate.walk, which would cause infinite recursion,
        except _finddotgitignores calls the superclass _ignore directly."""
        match = matchmod.match(
            self._root, self.getcwd(), [b'relglob:.gitignore']
        )
        # TODO: need subrepos?
        subrepos = []
        unknown = True
        ignored = False

        def fwarn(f, msg):
            self._ui.warn(
                b'%s: %s\n'
                % (
                    self.pathto(f),
                    pycompat.sysbytes(msg),
                )
            )
            return False

        ignore = super()._ignore
        dirignore = self._dirignore
        if ignored:
            ignore = util.never
            dirignore = util.never
        elif not unknown:
            # if unknown and ignored are False, skip step 2
            ignore = util.always
            dirignore = util.always

        matchfn = match.matchfn
        matchalways = match.always()
        matchtdir = match.traversedir
        dmap = self._map
        lstat = os.lstat
        dirkind = stat.S_IFDIR
        regkind = stat.S_IFREG
        lnkkind = stat.S_IFLNK
        join = self._join

        exact = skipstep3 = False
        if matchfn == match.exact:  # match.exact
            exact = True
            dirignore = util.always  # skip step 2
        elif match.files() and not match.anypats():  # match.match, no patterns
            skipstep3 = True

        if not exact and self._checkcase:
            normalize = self._normalize
            skipstep3 = False
        else:
            normalize = None

        # step 1: find all explicit files
        results, work, dirsnotfound = self._walkexplicit(match, subrepos)

        skipstep3 = skipstep3 and not (work or dirsnotfound)
        work = [nd for nd, d in work if not dirignore(d)]
        wadd = work.append

        # step 2: visit subdirectories
        while work:
            nd = work.pop()
            skip = None
            if nd != b'':
                skip = b'.hg'
            try:
                entries = util.listdir(join(nd), stat=True, skip=skip)
            except OSError as inst:
                if inst.errno in (errno.EACCES, errno.ENOENT):
                    fwarn(nd, inst.strerror)
                    continue
                raise
            for f, kind, st in entries:
                if normalize:
                    nf = normalize(nd and (nd + b"/" + f) or f, True, True)
                else:
                    nf = nd and (nd + b"/" + f) or f
                if nf not in results:
                    if kind == dirkind:
                        if not ignore(nf):
                            if matchtdir:
                                matchtdir(nf)
                            wadd(nf)
                        if nf in dmap and (matchalways or matchfn(nf)):
                            results[nf] = None
                    elif kind == regkind or kind == lnkkind:
                        if nf in dmap:
                            if matchalways or matchfn(nf):
                                results[nf] = st
                        elif (matchalways or matchfn(nf)) and not ignore(nf):
                            results[nf] = st
                    elif nf in dmap and (matchalways or matchfn(nf)):
                        results[nf] = None

        for s in subrepos:
            del results[s]
        del results[b'.hg']

        # step 3: report unseen items in the dmap hash
        if not skipstep3 and not exact:
            if not results and matchalways:
                visit = dmap.keys()
            else:
                visit = [f for f in dmap if f not in results and matchfn(f)]
            visit.sort()

            if unknown:
                # unknown == True means we walked the full directory tree
                # above. So if a file is not seen it was either a) not matching
                # matchfn b) ignored, c) missing, or d) under a symlink
                # directory.
                audit_path = pathutil.pathauditor(self._root)

                for nf in iter(visit):
                    # Report ignored items in the dmap as long as they are not
                    # under a symlink directory.
                    if audit_path.check(nf):
                        try:
                            results[nf] = lstat(join(nf))
                        except OSError:
                            # file doesn't exist
                            results[nf] = None
                    else:
                        # It's either missing or under a symlink directory
                        results[nf] = None
            else:
                # We may not have walked the full directory tree above,
                # so stat everything we missed.
                nf = next(iter(visit))
                for st in util.statfiles([join(i) for i in visit]):
                    results[nf()] = st
        return results.keys()

    def _rust_status(self, *args, **kwargs):
        # intercept a rust status call and force the fallback,
        # otherwise our patching won't work
        if not os.path.lexists(self._join(b'.hgignore')):
            self._ui.debug(b'suppressing rust status to intercept gitignores\n')
            raise dirstate.rustmod.FallbackError
        else:
            return super()._rust_status(*args, **kwargs)


@eh.reposetup
def reposetup(ui, repo):
    if isinstance(repo, gitrepo.gitrepo):
        return

    if git_handler.has_gitrepo(repo):
        dirstate.dirstate = gitdirstate