##// END OF EJS Templates
narrow: extract helper for parsing narrowspec file...
Martin von Zweigbergk -
r40726:efd0f792 default
parent child Browse files
Show More
@@ -1,225 +1,228
1 1 # narrowspec.py - methods for working with a narrow view of a repository
2 2 #
3 3 # Copyright 2017 Google, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 match as matchmod,
16 16 repository,
17 17 sparse,
18 18 util,
19 19 )
20 20
21 21 FILENAME = 'narrowspec'
22 22
23 23 # Pattern prefixes that are allowed in narrow patterns. This list MUST
24 24 # only contain patterns that are fast and safe to evaluate. Keep in mind
25 25 # that patterns are supplied by clients and executed on remote servers
26 26 # as part of wire protocol commands. That means that changes to this
27 27 # data structure influence the wire protocol and should not be taken
28 28 # lightly - especially removals.
29 29 VALID_PREFIXES = (
30 30 b'path:',
31 31 b'rootfilesin:',
32 32 )
33 33
34 34 def normalizesplitpattern(kind, pat):
35 35 """Returns the normalized version of a pattern and kind.
36 36
37 37 Returns a tuple with the normalized kind and normalized pattern.
38 38 """
39 39 pat = pat.rstrip('/')
40 40 _validatepattern(pat)
41 41 return kind, pat
42 42
43 43 def _numlines(s):
44 44 """Returns the number of lines in s, including ending empty lines."""
45 45 # We use splitlines because it is Unicode-friendly and thus Python 3
46 46 # compatible. However, it does not count empty lines at the end, so trick
47 47 # it by adding a character at the end.
48 48 return len((s + 'x').splitlines())
49 49
50 50 def _validatepattern(pat):
51 51 """Validates the pattern and aborts if it is invalid.
52 52
53 53 Patterns are stored in the narrowspec as newline-separated
54 54 POSIX-style bytestring paths. There's no escaping.
55 55 """
56 56
57 57 # We use newlines as separators in the narrowspec file, so don't allow them
58 58 # in patterns.
59 59 if _numlines(pat) > 1:
60 60 raise error.Abort(_('newlines are not allowed in narrowspec paths'))
61 61
62 62 components = pat.split('/')
63 63 if '.' in components or '..' in components:
64 64 raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
65 65
66 66 def normalizepattern(pattern, defaultkind='path'):
67 67 """Returns the normalized version of a text-format pattern.
68 68
69 69 If the pattern has no kind, the default will be added.
70 70 """
71 71 kind, pat = matchmod._patsplit(pattern, defaultkind)
72 72 return '%s:%s' % normalizesplitpattern(kind, pat)
73 73
74 74 def parsepatterns(pats):
75 75 """Parses an iterable of patterns into a typed pattern set.
76 76
77 77 Patterns are assumed to be ``path:`` if no prefix is present.
78 78 For safety and performance reasons, only some prefixes are allowed.
79 79 See ``validatepatterns()``.
80 80
81 81 This function should be used on patterns that come from the user to
82 82 normalize and validate them to the internal data structure used for
83 83 representing patterns.
84 84 """
85 85 res = {normalizepattern(orig) for orig in pats}
86 86 validatepatterns(res)
87 87 return res
88 88
89 89 def validatepatterns(pats):
90 90 """Validate that patterns are in the expected data structure and format.
91 91
92 92 And that is a set of normalized patterns beginning with ``path:`` or
93 93 ``rootfilesin:``.
94 94
95 95 This function should be used to validate internal data structures
96 96 and patterns that are loaded from sources that use the internal,
97 97 prefixed pattern representation (but can't necessarily be fully trusted).
98 98 """
99 99 if not isinstance(pats, set):
100 100 raise error.ProgrammingError('narrow patterns should be a set; '
101 101 'got %r' % pats)
102 102
103 103 for pat in pats:
104 104 if not pat.startswith(VALID_PREFIXES):
105 105 # Use a Mercurial exception because this can happen due to user
106 106 # bugs (e.g. manually updating spec file).
107 107 raise error.Abort(_('invalid prefix on narrow pattern: %s') % pat,
108 108 hint=_('narrow patterns must begin with one of '
109 109 'the following: %s') %
110 110 ', '.join(VALID_PREFIXES))
111 111
112 112 def format(includes, excludes):
113 113 output = '[include]\n'
114 114 for i in sorted(includes - excludes):
115 115 output += i + '\n'
116 116 output += '[exclude]\n'
117 117 for e in sorted(excludes):
118 118 output += e + '\n'
119 119 return output
120 120
121 121 def match(root, include=None, exclude=None):
122 122 if not include:
123 123 # Passing empty include and empty exclude to matchmod.match()
124 124 # gives a matcher that matches everything, so explicitly use
125 125 # the nevermatcher.
126 126 return matchmod.never(root, '')
127 127 return matchmod.match(root, '', [], include=include or [],
128 128 exclude=exclude or [])
129 129
130 def parseconfig(ui, spec):
131 # maybe we should care about the profiles returned too
132 includepats, excludepats, profiles = sparse.parseconfig(ui, spec, 'narrow')
133 if profiles:
134 raise error.Abort(_("including other spec files using '%include' is not"
135 " supported in narrowspec"))
136
137 validatepatterns(includepats)
138 validatepatterns(excludepats)
139
140 return includepats, excludepats
141
130 142 def load(repo):
131 143 try:
132 144 spec = repo.svfs.read(FILENAME)
133 145 except IOError as e:
134 146 # Treat "narrowspec does not exist" the same as "narrowspec file exists
135 147 # and is empty".
136 148 if e.errno == errno.ENOENT:
137 149 return set(), set()
138 150 raise
139 # maybe we should care about the profiles returned too
140 includepats, excludepats, profiles = sparse.parseconfig(repo.ui, spec,
141 'narrow')
142 if profiles:
143 raise error.Abort(_("including other spec files using '%include' is not"
144 " supported in narrowspec"))
145 151
146 validatepatterns(includepats)
147 validatepatterns(excludepats)
148
149 return includepats, excludepats
152 return parseconfig(repo.ui, spec)
150 153
151 154 def save(repo, includepats, excludepats):
152 155 validatepatterns(includepats)
153 156 validatepatterns(excludepats)
154 157 spec = format(includepats, excludepats)
155 158 repo.svfs.write(FILENAME, spec)
156 159
157 160 def savebackup(repo, backupname):
158 161 if repository.NARROW_REQUIREMENT not in repo.requirements:
159 162 return
160 163 vfs = repo.vfs
161 164 vfs.tryunlink(backupname)
162 165 util.copyfile(repo.svfs.join(FILENAME), vfs.join(backupname), hardlink=True)
163 166
164 167 def restorebackup(repo, backupname):
165 168 if repository.NARROW_REQUIREMENT not in repo.requirements:
166 169 return
167 170 util.rename(repo.vfs.join(backupname), repo.svfs.join(FILENAME))
168 171
169 172 def clearbackup(repo, backupname):
170 173 if repository.NARROW_REQUIREMENT not in repo.requirements:
171 174 return
172 175 repo.vfs.unlink(backupname)
173 176
174 177 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
175 178 r""" Restricts the patterns according to repo settings,
176 179 results in a logical AND operation
177 180
178 181 :param req_includes: requested includes
179 182 :param req_excludes: requested excludes
180 183 :param repo_includes: repo includes
181 184 :param repo_excludes: repo excludes
182 185 :return: include patterns, exclude patterns, and invalid include patterns.
183 186
184 187 >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
185 188 (set(['f1']), {}, [])
186 189 >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
187 190 (set(['f1']), {}, [])
188 191 >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
189 192 (set(['f1/fc1']), {}, [])
190 193 >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
191 194 ([], set(['path:.']), [])
192 195 >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
193 196 (set(['f2/fc2']), {}, [])
194 197 >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
195 198 ([], set(['path:.']), [])
196 199 >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
197 200 (set(['f1/$non_exitent_var']), {}, [])
198 201 """
199 202 res_excludes = set(req_excludes)
200 203 res_excludes.update(repo_excludes)
201 204 invalid_includes = []
202 205 if not req_includes:
203 206 res_includes = set(repo_includes)
204 207 elif 'path:.' not in repo_includes:
205 208 res_includes = []
206 209 for req_include in req_includes:
207 210 req_include = util.expandpath(util.normpath(req_include))
208 211 if req_include in repo_includes:
209 212 res_includes.append(req_include)
210 213 continue
211 214 valid = False
212 215 for repo_include in repo_includes:
213 216 if req_include.startswith(repo_include + '/'):
214 217 valid = True
215 218 res_includes.append(req_include)
216 219 break
217 220 if not valid:
218 221 invalid_includes.append(req_include)
219 222 if len(res_includes) == 0:
220 223 res_excludes = {'path:.'}
221 224 else:
222 225 res_includes = set(res_includes)
223 226 else:
224 227 res_includes = set(req_includes)
225 228 return res_includes, res_excludes, invalid_includes
General Comments 0
You need to be logged in to leave comments. Login now