comparison hgext/narrow/narrowspec.py @ 36079:a2a6e724d61a

narrow: import experimental extension from narrowhg revision cb51d673e9c5 Adjustments: * renamed src to hgext/narrow * marked extension experimental * added correct copyright header where it was missing * updated hgrc extension enable line in library.sh * renamed library.sh to narrow-library.sh * dropped all files from repo root as they're not interesting * dropped test-pyflakes.t, test-check-code.t and test-check-py3-compat.t * renamed remaining tests to all be test-narrow-* when they didn't already * fixed test-narrow-expanddirstate.t to refer to narrow and not narrowhg * fixed tests that wanted `update -C .` instead of `merge --abort` * corrected a two-space indent in narrowspec.py * added a missing _() in narrowcommands.py * fixed imports to pass the import checker * narrow only adds its --include and --exclude to clone if sparse isn't enabled to avoid breaking test-duplicateoptions.py. This is a kludge, and we'll need to come up with a better solution in the future. These were more or less the minimum to import something that would pass tests and not create a bunch of files we'll never use. Changes I intend to make as followups: * rework the test-narrow-*-tree.t tests to use the new testcases functionality in run-tests.py * remove lots of monkeypatches of core things Differential Revision: https://phab.mercurial-scm.org/D1974
author Augie Fackler <augie@google.com>
date Mon, 29 Jan 2018 16:19:33 -0500
parents
children 9c55bbc29dcf
comparison
equal deleted inserted replaced
36078:7f68235f23ff 36079:a2a6e724d61a
1 # narrowspec.py - methods for working with a narrow view of a repository
2 #
3 # Copyright 2017 Google, Inc.
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
8 from __future__ import absolute_import
9
10 import errno
11
12 from mercurial.i18n import _
13 from mercurial import (
14 error,
15 match as matchmod,
16 util,
17 )
18
19 from .. import (
20 share,
21 )
22
23 FILENAME = 'narrowspec'
24
25 def _parsestoredpatterns(text):
26 """Parses the narrowspec format that's stored on disk."""
27 patlist = None
28 includepats = []
29 excludepats = []
30 for l in text.splitlines():
31 if l == '[includes]':
32 if patlist is None:
33 patlist = includepats
34 else:
35 raise error.Abort(_('narrowspec includes section must appear '
36 'at most once, before excludes'))
37 elif l == '[excludes]':
38 if patlist is not excludepats:
39 patlist = excludepats
40 else:
41 raise error.Abort(_('narrowspec excludes section must appear '
42 'at most once'))
43 else:
44 patlist.append(l)
45
46 return set(includepats), set(excludepats)
47
48 def parseserverpatterns(text):
49 """Parses the narrowspec format that's returned by the server."""
50 includepats = set()
51 excludepats = set()
52
53 # We get one entry per line, in the format "<key> <value>".
54 # It's OK for value to contain other spaces.
55 for kp in (l.split(' ', 1) for l in text.splitlines()):
56 if len(kp) != 2:
57 raise error.Abort(_('Invalid narrowspec pattern line: "%s"') % kp)
58 key = kp[0]
59 pat = kp[1]
60 if key == 'include':
61 includepats.add(pat)
62 elif key == 'exclude':
63 excludepats.add(pat)
64 else:
65 raise error.Abort(_('Invalid key "%s" in server response') % key)
66
67 return includepats, excludepats
68
69 def normalizesplitpattern(kind, pat):
70 """Returns the normalized version of a pattern and kind.
71
72 Returns a tuple with the normalized kind and normalized pattern.
73 """
74 pat = pat.rstrip('/')
75 _validatepattern(pat)
76 return kind, pat
77
78 def _numlines(s):
79 """Returns the number of lines in s, including ending empty lines."""
80 # We use splitlines because it is Unicode-friendly and thus Python 3
81 # compatible. However, it does not count empty lines at the end, so trick
82 # it by adding a character at the end.
83 return len((s + 'x').splitlines())
84
85 def _validatepattern(pat):
86 """Validates the pattern and aborts if it is invalid."""
87
88 # We use newlines as separators in the narrowspec file, so don't allow them
89 # in patterns.
90 if _numlines(pat) > 1:
91 raise error.Abort('newlines are not allowed in narrowspec paths')
92
93 components = pat.split('/')
94 if '.' in components or '..' in components:
95 raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
96
97 def normalizepattern(pattern, defaultkind='path'):
98 """Returns the normalized version of a text-format pattern.
99
100 If the pattern has no kind, the default will be added.
101 """
102 kind, pat = matchmod._patsplit(pattern, defaultkind)
103 return '%s:%s' % normalizesplitpattern(kind, pat)
104
105 def parsepatterns(pats):
106 """Parses a list of patterns into a typed pattern set."""
107 return set(normalizepattern(p) for p in pats)
108
109 def format(includes, excludes):
110 output = '[includes]\n'
111 for i in sorted(includes - excludes):
112 output += i + '\n'
113 output += '[excludes]\n'
114 for e in sorted(excludes):
115 output += e + '\n'
116 return output
117
118 def match(root, include=None, exclude=None):
119 if not include:
120 # Passing empty include and empty exclude to matchmod.match()
121 # gives a matcher that matches everything, so explicitly use
122 # the nevermatcher.
123 return matchmod.never(root, '')
124 return matchmod.match(root, '', [], include=include or [],
125 exclude=exclude or [])
126
127 def needsexpansion(includes):
128 return [i for i in includes if i.startswith('include:')]
129
130 def load(repo):
131 if repo.shared():
132 repo = share._getsrcrepo(repo)
133 try:
134 spec = repo.vfs.read(FILENAME)
135 except IOError as e:
136 # Treat "narrowspec does not exist" the same as "narrowspec file exists
137 # and is empty".
138 if e.errno == errno.ENOENT:
139 # Without this the next call to load will use the cached
140 # non-existence of the file, which can cause some odd issues.
141 repo.invalidate(clearfilecache=True)
142 return set(), set()
143 raise
144 return _parsestoredpatterns(spec)
145
146 def save(repo, includepats, excludepats):
147 spec = format(includepats, excludepats)
148 if repo.shared():
149 repo = share._getsrcrepo(repo)
150 repo.vfs.write(FILENAME, spec)
151
152 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes,
153 invalid_includes=None):
154 r""" Restricts the patterns according to repo settings,
155 results in a logical AND operation
156
157 :param req_includes: requested includes
158 :param req_excludes: requested excludes
159 :param repo_includes: repo includes
160 :param repo_excludes: repo excludes
161 :param invalid_includes: an array to collect invalid includes
162 :return: include and exclude patterns
163
164 >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
165 (set(['f1']), {})
166 >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
167 (set(['f1']), {})
168 >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
169 (set(['f1/fc1']), {})
170 >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
171 ([], set(['path:.']))
172 >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
173 (set(['f2/fc2']), {})
174 >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
175 ([], set(['path:.']))
176 >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
177 (set(['f1/$non_exitent_var']), {})
178 """
179 res_excludes = req_excludes.copy()
180 res_excludes.update(repo_excludes)
181 if not req_includes:
182 res_includes = set(repo_includes)
183 elif 'path:.' not in repo_includes:
184 res_includes = []
185 for req_include in req_includes:
186 req_include = util.expandpath(util.normpath(req_include))
187 if req_include in repo_includes:
188 res_includes.append(req_include)
189 continue
190 valid = False
191 for repo_include in repo_includes:
192 if req_include.startswith(repo_include + '/'):
193 valid = True
194 res_includes.append(req_include)
195 break
196 if not valid and invalid_includes is not None:
197 invalid_includes.append(req_include)
198 if len(res_includes) == 0:
199 res_excludes = {'path:.'}
200 else:
201 res_includes = set(res_includes)
202 else:
203 res_includes = set(req_includes)
204 return res_includes, res_excludes