Mercurial > hg
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 |