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