##// END OF EJS Templates
narrow: enforce that narrow spec is written within a transaction
marmoute -
r51087:6794f927 default
parent child Browse files
Show More
@@ -1,384 +1,386 b''
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 import weakref
9 9
10 10 from .i18n import _
11 11 from .pycompat import getattr
12 12 from . import (
13 13 error,
14 14 match as matchmod,
15 15 merge,
16 16 mergestate as mergestatemod,
17 17 scmutil,
18 18 sparse,
19 19 util,
20 20 )
21 21
22 22 # The file in .hg/store/ that indicates which paths exit in the store
23 23 FILENAME = b'narrowspec'
24 24 # The file in .hg/ that indicates which paths exit in the dirstate
25 25 DIRSTATE_FILENAME = b'narrowspec.dirstate'
26 26
27 27 # Pattern prefixes that are allowed in narrow patterns. This list MUST
28 28 # only contain patterns that are fast and safe to evaluate. Keep in mind
29 29 # that patterns are supplied by clients and executed on remote servers
30 30 # as part of wire protocol commands. That means that changes to this
31 31 # data structure influence the wire protocol and should not be taken
32 32 # lightly - especially removals.
33 33 VALID_PREFIXES = (
34 34 b'path:',
35 35 b'rootfilesin:',
36 36 )
37 37
38 38
39 39 def normalizesplitpattern(kind, pat):
40 40 """Returns the normalized version of a pattern and kind.
41 41
42 42 Returns a tuple with the normalized kind and normalized pattern.
43 43 """
44 44 pat = pat.rstrip(b'/')
45 45 _validatepattern(pat)
46 46 return kind, pat
47 47
48 48
49 49 def _numlines(s):
50 50 """Returns the number of lines in s, including ending empty lines."""
51 51 # We use splitlines because it is Unicode-friendly and thus Python 3
52 52 # compatible. However, it does not count empty lines at the end, so trick
53 53 # it by adding a character at the end.
54 54 return len((s + b'x').splitlines())
55 55
56 56
57 57 def _validatepattern(pat):
58 58 """Validates the pattern and aborts if it is invalid.
59 59
60 60 Patterns are stored in the narrowspec as newline-separated
61 61 POSIX-style bytestring paths. There's no escaping.
62 62 """
63 63
64 64 # We use newlines as separators in the narrowspec file, so don't allow them
65 65 # in patterns.
66 66 if _numlines(pat) > 1:
67 67 raise error.Abort(_(b'newlines are not allowed in narrowspec paths'))
68 68
69 69 components = pat.split(b'/')
70 70 if b'.' in components or b'..' in components:
71 71 raise error.Abort(
72 72 _(b'"." and ".." are not allowed in narrowspec paths')
73 73 )
74 74
75 75
76 76 def normalizepattern(pattern, defaultkind=b'path'):
77 77 """Returns the normalized version of a text-format pattern.
78 78
79 79 If the pattern has no kind, the default will be added.
80 80 """
81 81 kind, pat = matchmod._patsplit(pattern, defaultkind)
82 82 return b'%s:%s' % normalizesplitpattern(kind, pat)
83 83
84 84
85 85 def parsepatterns(pats):
86 86 """Parses an iterable of patterns into a typed pattern set.
87 87
88 88 Patterns are assumed to be ``path:`` if no prefix is present.
89 89 For safety and performance reasons, only some prefixes are allowed.
90 90 See ``validatepatterns()``.
91 91
92 92 This function should be used on patterns that come from the user to
93 93 normalize and validate them to the internal data structure used for
94 94 representing patterns.
95 95 """
96 96 res = {normalizepattern(orig) for orig in pats}
97 97 validatepatterns(res)
98 98 return res
99 99
100 100
101 101 def validatepatterns(pats):
102 102 """Validate that patterns are in the expected data structure and format.
103 103
104 104 And that is a set of normalized patterns beginning with ``path:`` or
105 105 ``rootfilesin:``.
106 106
107 107 This function should be used to validate internal data structures
108 108 and patterns that are loaded from sources that use the internal,
109 109 prefixed pattern representation (but can't necessarily be fully trusted).
110 110 """
111 111 with util.timedcm('narrowspec.validatepatterns(pats size=%d)', len(pats)):
112 112 if not isinstance(pats, set):
113 113 raise error.ProgrammingError(
114 114 b'narrow patterns should be a set; got %r' % pats
115 115 )
116 116
117 117 for pat in pats:
118 118 if not pat.startswith(VALID_PREFIXES):
119 119 # Use a Mercurial exception because this can happen due to user
120 120 # bugs (e.g. manually updating spec file).
121 121 raise error.Abort(
122 122 _(b'invalid prefix on narrow pattern: %s') % pat,
123 123 hint=_(
124 124 b'narrow patterns must begin with one of '
125 125 b'the following: %s'
126 126 )
127 127 % b', '.join(VALID_PREFIXES),
128 128 )
129 129
130 130
131 131 def format(includes, excludes):
132 132 output = b'[include]\n'
133 133 for i in sorted(includes - excludes):
134 134 output += i + b'\n'
135 135 output += b'[exclude]\n'
136 136 for e in sorted(excludes):
137 137 output += e + b'\n'
138 138 return output
139 139
140 140
141 141 def match(root, include=None, exclude=None):
142 142 if not include:
143 143 # Passing empty include and empty exclude to matchmod.match()
144 144 # gives a matcher that matches everything, so explicitly use
145 145 # the nevermatcher.
146 146 return matchmod.never()
147 147 return matchmod.match(
148 148 root, b'', [], include=include or [], exclude=exclude or []
149 149 )
150 150
151 151
152 152 def parseconfig(ui, spec):
153 153 # maybe we should care about the profiles returned too
154 154 includepats, excludepats, profiles = sparse.parseconfig(ui, spec, b'narrow')
155 155 if profiles:
156 156 raise error.Abort(
157 157 _(
158 158 b"including other spec files using '%include' is not"
159 159 b" supported in narrowspec"
160 160 )
161 161 )
162 162
163 163 validatepatterns(includepats)
164 164 validatepatterns(excludepats)
165 165
166 166 return includepats, excludepats
167 167
168 168
169 169 def load(repo):
170 170 # Treat "narrowspec does not exist" the same as "narrowspec file exists
171 171 # and is empty".
172 172 spec = repo.svfs.tryread(FILENAME)
173 173 return parseconfig(repo.ui, spec)
174 174
175 175
176 176 def save(repo, includepats, excludepats):
177 177 repo = repo.unfiltered()
178 178
179 179 validatepatterns(includepats)
180 180 validatepatterns(excludepats)
181 181 spec = format(includepats, excludepats)
182 182
183 183 tr = repo.currenttransaction()
184 184 if tr is None:
185 repo.svfs.write(FILENAME, spec)
185 m = "changing narrow spec outside of a transaction"
186 raise error.ProgrammingError(m)
186 187 else:
187 188 # the roundtrip is sometime different
188 189 # not taking any chance for now
189 190 value = parseconfig(repo.ui, spec)
190 191 reporef = weakref.ref(repo)
191 192
192 193 def clean_pending(tr):
193 194 r = reporef()
194 195 if r is not None:
195 196 r._pending_narrow_pats = None
196 197
197 198 tr.addpostclose(b'narrow-spec', clean_pending)
198 199 tr.addabort(b'narrow-spec', clean_pending)
199 200 repo._pending_narrow_pats = value
200 201
201 202 def write_spec(f):
202 203 f.write(spec)
203 204
204 205 tr.addfilegenerator(
205 206 # XXX think about order at some point
206 207 b"narrow-spec",
207 208 (FILENAME,),
208 209 write_spec,
209 210 location=b'store',
210 211 )
211 212
212 213
213 214 def copytoworkingcopy(repo):
214 215 repo = repo.unfiltered()
215 216 tr = repo.currenttransaction()
216 217 spec = format(*repo.narrowpats)
217 218 if tr is None:
218 repo.vfs.write(DIRSTATE_FILENAME, spec)
219 m = "changing narrow spec outside of a transaction"
220 raise error.ProgrammingError(m)
219 221 else:
220 222
221 223 reporef = weakref.ref(repo)
222 224
223 225 def clean_pending(tr):
224 226 r = reporef()
225 227 if r is not None:
226 228 r._pending_narrow_pats_dirstate = None
227 229
228 230 tr.addpostclose(b'narrow-spec-dirstate', clean_pending)
229 231 tr.addabort(b'narrow-spec-dirstate', clean_pending)
230 232 repo._pending_narrow_pats_dirstate = repo.narrowpats
231 233
232 234 def write_spec(f):
233 235 f.write(spec)
234 236
235 237 tr.addfilegenerator(
236 238 # XXX think about order at some point
237 239 b"narrow-spec-dirstate",
238 240 (DIRSTATE_FILENAME,),
239 241 write_spec,
240 242 location=b'plain',
241 243 )
242 244
243 245
244 246 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
245 247 r"""Restricts the patterns according to repo settings,
246 248 results in a logical AND operation
247 249
248 250 :param req_includes: requested includes
249 251 :param req_excludes: requested excludes
250 252 :param repo_includes: repo includes
251 253 :param repo_excludes: repo excludes
252 254 :return: include patterns, exclude patterns, and invalid include patterns.
253 255 """
254 256 res_excludes = set(req_excludes)
255 257 res_excludes.update(repo_excludes)
256 258 invalid_includes = []
257 259 if not req_includes:
258 260 res_includes = set(repo_includes)
259 261 elif b'path:.' not in repo_includes:
260 262 res_includes = []
261 263 for req_include in req_includes:
262 264 req_include = util.expandpath(util.normpath(req_include))
263 265 if req_include in repo_includes:
264 266 res_includes.append(req_include)
265 267 continue
266 268 valid = False
267 269 for repo_include in repo_includes:
268 270 if req_include.startswith(repo_include + b'/'):
269 271 valid = True
270 272 res_includes.append(req_include)
271 273 break
272 274 if not valid:
273 275 invalid_includes.append(req_include)
274 276 if len(res_includes) == 0:
275 277 res_excludes = {b'path:.'}
276 278 else:
277 279 res_includes = set(res_includes)
278 280 else:
279 281 res_includes = set(req_includes)
280 282 return res_includes, res_excludes, invalid_includes
281 283
282 284
283 285 # These two are extracted for extensions (specifically for Google's CitC file
284 286 # system)
285 287 def _deletecleanfiles(repo, files):
286 288 for f in files:
287 289 repo.wvfs.unlinkpath(f)
288 290
289 291
290 292 def _writeaddedfiles(repo, pctx, files):
291 293 mresult = merge.mergeresult()
292 294 mf = repo[b'.'].manifest()
293 295 for f in files:
294 296 if not repo.wvfs.exists(f):
295 297 mresult.addfile(
296 298 f,
297 299 mergestatemod.ACTION_GET,
298 300 (mf.flags(f), False),
299 301 b"narrowspec updated",
300 302 )
301 303 merge.applyupdates(
302 304 repo,
303 305 mresult,
304 306 wctx=repo[None],
305 307 mctx=repo[b'.'],
306 308 overwrite=False,
307 309 wantfiledata=False,
308 310 )
309 311
310 312
311 313 def checkworkingcopynarrowspec(repo):
312 314 # Avoid infinite recursion when updating the working copy
313 315 if getattr(repo, '_updatingnarrowspec', False):
314 316 return
315 317 storespec = repo.narrowpats
316 318 wcspec = repo._pending_narrow_pats_dirstate
317 319 if wcspec is None:
318 320 oldspec = repo.vfs.tryread(DIRSTATE_FILENAME)
319 321 wcspec = parseconfig(repo.ui, oldspec)
320 322 if wcspec != storespec:
321 323 raise error.StateError(
322 324 _(b"working copy's narrowspec is stale"),
323 325 hint=_(b"run 'hg tracked --update-working-copy'"),
324 326 )
325 327
326 328
327 329 def updateworkingcopy(repo, assumeclean=False):
328 330 """updates the working copy and dirstate from the store narrowspec
329 331
330 332 When assumeclean=True, files that are not known to be clean will also
331 333 be deleted. It is then up to the caller to make sure they are clean.
332 334 """
333 335 old = repo._pending_narrow_pats_dirstate
334 336 if old is None:
335 337 oldspec = repo.vfs.tryread(DIRSTATE_FILENAME)
336 338 oldincludes, oldexcludes = parseconfig(repo.ui, oldspec)
337 339 else:
338 340 oldincludes, oldexcludes = old
339 341 newincludes, newexcludes = repo.narrowpats
340 342 repo._updatingnarrowspec = True
341 343
342 344 oldmatch = match(repo.root, include=oldincludes, exclude=oldexcludes)
343 345 newmatch = match(repo.root, include=newincludes, exclude=newexcludes)
344 346 addedmatch = matchmod.differencematcher(newmatch, oldmatch)
345 347 removedmatch = matchmod.differencematcher(oldmatch, newmatch)
346 348
347 349 assert repo.currentwlock() is not None
348 350 ds = repo.dirstate
349 351 with ds.running_status(repo):
350 352 lookup, status, _mtime_boundary = ds.status(
351 353 removedmatch,
352 354 subrepos=[],
353 355 ignored=True,
354 356 clean=True,
355 357 unknown=True,
356 358 )
357 359 trackeddirty = status.modified + status.added
358 360 clean = status.clean
359 361 if assumeclean:
360 362 clean.extend(lookup)
361 363 else:
362 364 trackeddirty.extend(lookup)
363 365 _deletecleanfiles(repo, clean)
364 366 uipathfn = scmutil.getuipathfn(repo)
365 367 for f in sorted(trackeddirty):
366 368 repo.ui.status(
367 369 _(b'not deleting possibly dirty file %s\n') % uipathfn(f)
368 370 )
369 371 for f in sorted(status.unknown):
370 372 repo.ui.status(_(b'not deleting unknown file %s\n') % uipathfn(f))
371 373 for f in sorted(status.ignored):
372 374 repo.ui.status(_(b'not deleting ignored file %s\n') % uipathfn(f))
373 375 for f in clean + trackeddirty:
374 376 ds.update_file(f, p1_tracked=False, wc_tracked=False)
375 377
376 378 pctx = repo[b'.']
377 379
378 380 # only update added files that are in the sparse checkout
379 381 addedmatch = matchmod.intersectmatchers(addedmatch, sparse.matcher(repo))
380 382 newfiles = [f for f in pctx.manifest().walk(addedmatch) if f not in ds]
381 383 for f in newfiles:
382 384 ds.update_file(f, p1_tracked=True, wc_tracked=True, possibly_dirty=True)
383 385 _writeaddedfiles(repo, pctx, newfiles)
384 386 repo._updatingnarrowspec = False
General Comments 0
You need to be logged in to leave comments. Login now