##// END OF EJS Templates
sparse: add comment for an if condition which I tried to refactor...
Pulkit Goyal -
r45852:6a8eafae default
parent child Browse files
Show More
@@ -1,828 +1,834 b''
1 # sparse.py - functionality for sparse checkouts
1 # sparse.py - functionality for sparse checkouts
2 #
2 #
3 # Copyright 2014 Facebook, Inc.
3 # Copyright 2014 Facebook, 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 os
10 import os
11
11
12 from .i18n import _
12 from .i18n import _
13 from .node import (
13 from .node import (
14 hex,
14 hex,
15 nullid,
15 nullid,
16 )
16 )
17 from . import (
17 from . import (
18 error,
18 error,
19 match as matchmod,
19 match as matchmod,
20 merge as mergemod,
20 merge as mergemod,
21 mergestate as mergestatemod,
21 mergestate as mergestatemod,
22 pathutil,
22 pathutil,
23 pycompat,
23 pycompat,
24 scmutil,
24 scmutil,
25 util,
25 util,
26 )
26 )
27 from .utils import hashutil
27 from .utils import hashutil
28
28
29 # Whether sparse features are enabled. This variable is intended to be
29 # Whether sparse features are enabled. This variable is intended to be
30 # temporary to facilitate porting sparse to core. It should eventually be
30 # temporary to facilitate porting sparse to core. It should eventually be
31 # a per-repo option, possibly a repo requirement.
31 # a per-repo option, possibly a repo requirement.
32 enabled = False
32 enabled = False
33
33
34
34
35 def parseconfig(ui, raw, action):
35 def parseconfig(ui, raw, action):
36 """Parse sparse config file content.
36 """Parse sparse config file content.
37
37
38 action is the command which is trigerring this read, can be narrow, sparse
38 action is the command which is trigerring this read, can be narrow, sparse
39
39
40 Returns a tuple of includes, excludes, and profiles.
40 Returns a tuple of includes, excludes, and profiles.
41 """
41 """
42 includes = set()
42 includes = set()
43 excludes = set()
43 excludes = set()
44 profiles = set()
44 profiles = set()
45 current = None
45 current = None
46 havesection = False
46 havesection = False
47
47
48 for line in raw.split(b'\n'):
48 for line in raw.split(b'\n'):
49 line = line.strip()
49 line = line.strip()
50 if not line or line.startswith(b'#'):
50 if not line or line.startswith(b'#'):
51 # empty or comment line, skip
51 # empty or comment line, skip
52 continue
52 continue
53 elif line.startswith(b'%include '):
53 elif line.startswith(b'%include '):
54 line = line[9:].strip()
54 line = line[9:].strip()
55 if line:
55 if line:
56 profiles.add(line)
56 profiles.add(line)
57 elif line == b'[include]':
57 elif line == b'[include]':
58 if havesection and current != includes:
58 if havesection and current != includes:
59 # TODO pass filename into this API so we can report it.
59 # TODO pass filename into this API so we can report it.
60 raise error.Abort(
60 raise error.Abort(
61 _(
61 _(
62 b'%(action)s config cannot have includes '
62 b'%(action)s config cannot have includes '
63 b'after excludes'
63 b'after excludes'
64 )
64 )
65 % {b'action': action}
65 % {b'action': action}
66 )
66 )
67 havesection = True
67 havesection = True
68 current = includes
68 current = includes
69 continue
69 continue
70 elif line == b'[exclude]':
70 elif line == b'[exclude]':
71 havesection = True
71 havesection = True
72 current = excludes
72 current = excludes
73 elif line:
73 elif line:
74 if current is None:
74 if current is None:
75 raise error.Abort(
75 raise error.Abort(
76 _(
76 _(
77 b'%(action)s config entry outside of '
77 b'%(action)s config entry outside of '
78 b'section: %(line)s'
78 b'section: %(line)s'
79 )
79 )
80 % {b'action': action, b'line': line},
80 % {b'action': action, b'line': line},
81 hint=_(
81 hint=_(
82 b'add an [include] or [exclude] line '
82 b'add an [include] or [exclude] line '
83 b'to declare the entry type'
83 b'to declare the entry type'
84 ),
84 ),
85 )
85 )
86
86
87 if line.strip().startswith(b'/'):
87 if line.strip().startswith(b'/'):
88 ui.warn(
88 ui.warn(
89 _(
89 _(
90 b'warning: %(action)s profile cannot use'
90 b'warning: %(action)s profile cannot use'
91 b' paths starting with /, ignoring %(line)s\n'
91 b' paths starting with /, ignoring %(line)s\n'
92 )
92 )
93 % {b'action': action, b'line': line}
93 % {b'action': action, b'line': line}
94 )
94 )
95 continue
95 continue
96 current.add(line)
96 current.add(line)
97
97
98 return includes, excludes, profiles
98 return includes, excludes, profiles
99
99
100
100
101 # Exists as separate function to facilitate monkeypatching.
101 # Exists as separate function to facilitate monkeypatching.
102 def readprofile(repo, profile, changeid):
102 def readprofile(repo, profile, changeid):
103 """Resolve the raw content of a sparse profile file."""
103 """Resolve the raw content of a sparse profile file."""
104 # TODO add some kind of cache here because this incurs a manifest
104 # TODO add some kind of cache here because this incurs a manifest
105 # resolve and can be slow.
105 # resolve and can be slow.
106 return repo.filectx(profile, changeid=changeid).data()
106 return repo.filectx(profile, changeid=changeid).data()
107
107
108
108
109 def patternsforrev(repo, rev):
109 def patternsforrev(repo, rev):
110 """Obtain sparse checkout patterns for the given rev.
110 """Obtain sparse checkout patterns for the given rev.
111
111
112 Returns a tuple of iterables representing includes, excludes, and
112 Returns a tuple of iterables representing includes, excludes, and
113 patterns.
113 patterns.
114 """
114 """
115 # Feature isn't enabled. No-op.
115 # Feature isn't enabled. No-op.
116 if not enabled:
116 if not enabled:
117 return set(), set(), set()
117 return set(), set(), set()
118
118
119 raw = repo.vfs.tryread(b'sparse')
119 raw = repo.vfs.tryread(b'sparse')
120 if not raw:
120 if not raw:
121 return set(), set(), set()
121 return set(), set(), set()
122
122
123 if rev is None:
123 if rev is None:
124 raise error.Abort(
124 raise error.Abort(
125 _(b'cannot parse sparse patterns from working directory')
125 _(b'cannot parse sparse patterns from working directory')
126 )
126 )
127
127
128 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
128 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
129 ctx = repo[rev]
129 ctx = repo[rev]
130
130
131 if profiles:
131 if profiles:
132 visited = set()
132 visited = set()
133 while profiles:
133 while profiles:
134 profile = profiles.pop()
134 profile = profiles.pop()
135 if profile in visited:
135 if profile in visited:
136 continue
136 continue
137
137
138 visited.add(profile)
138 visited.add(profile)
139
139
140 try:
140 try:
141 raw = readprofile(repo, profile, rev)
141 raw = readprofile(repo, profile, rev)
142 except error.ManifestLookupError:
142 except error.ManifestLookupError:
143 msg = (
143 msg = (
144 b"warning: sparse profile '%s' not found "
144 b"warning: sparse profile '%s' not found "
145 b"in rev %s - ignoring it\n" % (profile, ctx)
145 b"in rev %s - ignoring it\n" % (profile, ctx)
146 )
146 )
147 # experimental config: sparse.missingwarning
147 # experimental config: sparse.missingwarning
148 if repo.ui.configbool(b'sparse', b'missingwarning'):
148 if repo.ui.configbool(b'sparse', b'missingwarning'):
149 repo.ui.warn(msg)
149 repo.ui.warn(msg)
150 else:
150 else:
151 repo.ui.debug(msg)
151 repo.ui.debug(msg)
152 continue
152 continue
153
153
154 pincludes, pexcludes, subprofs = parseconfig(
154 pincludes, pexcludes, subprofs = parseconfig(
155 repo.ui, raw, b'sparse'
155 repo.ui, raw, b'sparse'
156 )
156 )
157 includes.update(pincludes)
157 includes.update(pincludes)
158 excludes.update(pexcludes)
158 excludes.update(pexcludes)
159 profiles.update(subprofs)
159 profiles.update(subprofs)
160
160
161 profiles = visited
161 profiles = visited
162
162
163 if includes:
163 if includes:
164 includes.add(b'.hg*')
164 includes.add(b'.hg*')
165
165
166 return includes, excludes, profiles
166 return includes, excludes, profiles
167
167
168
168
169 def activeconfig(repo):
169 def activeconfig(repo):
170 """Determine the active sparse config rules.
170 """Determine the active sparse config rules.
171
171
172 Rules are constructed by reading the current sparse config and bringing in
172 Rules are constructed by reading the current sparse config and bringing in
173 referenced profiles from parents of the working directory.
173 referenced profiles from parents of the working directory.
174 """
174 """
175 revs = [
175 revs = [
176 repo.changelog.rev(node)
176 repo.changelog.rev(node)
177 for node in repo.dirstate.parents()
177 for node in repo.dirstate.parents()
178 if node != nullid
178 if node != nullid
179 ]
179 ]
180
180
181 allincludes = set()
181 allincludes = set()
182 allexcludes = set()
182 allexcludes = set()
183 allprofiles = set()
183 allprofiles = set()
184
184
185 for rev in revs:
185 for rev in revs:
186 includes, excludes, profiles = patternsforrev(repo, rev)
186 includes, excludes, profiles = patternsforrev(repo, rev)
187 allincludes |= includes
187 allincludes |= includes
188 allexcludes |= excludes
188 allexcludes |= excludes
189 allprofiles |= profiles
189 allprofiles |= profiles
190
190
191 return allincludes, allexcludes, allprofiles
191 return allincludes, allexcludes, allprofiles
192
192
193
193
194 def configsignature(repo, includetemp=True):
194 def configsignature(repo, includetemp=True):
195 """Obtain the signature string for the current sparse configuration.
195 """Obtain the signature string for the current sparse configuration.
196
196
197 This is used to construct a cache key for matchers.
197 This is used to construct a cache key for matchers.
198 """
198 """
199 cache = repo._sparsesignaturecache
199 cache = repo._sparsesignaturecache
200
200
201 signature = cache.get(b'signature')
201 signature = cache.get(b'signature')
202
202
203 if includetemp:
203 if includetemp:
204 tempsignature = cache.get(b'tempsignature')
204 tempsignature = cache.get(b'tempsignature')
205 else:
205 else:
206 tempsignature = b'0'
206 tempsignature = b'0'
207
207
208 if signature is None or (includetemp and tempsignature is None):
208 if signature is None or (includetemp and tempsignature is None):
209 signature = hex(hashutil.sha1(repo.vfs.tryread(b'sparse')).digest())
209 signature = hex(hashutil.sha1(repo.vfs.tryread(b'sparse')).digest())
210 cache[b'signature'] = signature
210 cache[b'signature'] = signature
211
211
212 if includetemp:
212 if includetemp:
213 raw = repo.vfs.tryread(b'tempsparse')
213 raw = repo.vfs.tryread(b'tempsparse')
214 tempsignature = hex(hashutil.sha1(raw).digest())
214 tempsignature = hex(hashutil.sha1(raw).digest())
215 cache[b'tempsignature'] = tempsignature
215 cache[b'tempsignature'] = tempsignature
216
216
217 return b'%s %s' % (signature, tempsignature)
217 return b'%s %s' % (signature, tempsignature)
218
218
219
219
220 def writeconfig(repo, includes, excludes, profiles):
220 def writeconfig(repo, includes, excludes, profiles):
221 """Write the sparse config file given a sparse configuration."""
221 """Write the sparse config file given a sparse configuration."""
222 with repo.vfs(b'sparse', b'wb') as fh:
222 with repo.vfs(b'sparse', b'wb') as fh:
223 for p in sorted(profiles):
223 for p in sorted(profiles):
224 fh.write(b'%%include %s\n' % p)
224 fh.write(b'%%include %s\n' % p)
225
225
226 if includes:
226 if includes:
227 fh.write(b'[include]\n')
227 fh.write(b'[include]\n')
228 for i in sorted(includes):
228 for i in sorted(includes):
229 fh.write(i)
229 fh.write(i)
230 fh.write(b'\n')
230 fh.write(b'\n')
231
231
232 if excludes:
232 if excludes:
233 fh.write(b'[exclude]\n')
233 fh.write(b'[exclude]\n')
234 for e in sorted(excludes):
234 for e in sorted(excludes):
235 fh.write(e)
235 fh.write(e)
236 fh.write(b'\n')
236 fh.write(b'\n')
237
237
238 repo._sparsesignaturecache.clear()
238 repo._sparsesignaturecache.clear()
239
239
240
240
241 def readtemporaryincludes(repo):
241 def readtemporaryincludes(repo):
242 raw = repo.vfs.tryread(b'tempsparse')
242 raw = repo.vfs.tryread(b'tempsparse')
243 if not raw:
243 if not raw:
244 return set()
244 return set()
245
245
246 return set(raw.split(b'\n'))
246 return set(raw.split(b'\n'))
247
247
248
248
249 def writetemporaryincludes(repo, includes):
249 def writetemporaryincludes(repo, includes):
250 repo.vfs.write(b'tempsparse', b'\n'.join(sorted(includes)))
250 repo.vfs.write(b'tempsparse', b'\n'.join(sorted(includes)))
251 repo._sparsesignaturecache.clear()
251 repo._sparsesignaturecache.clear()
252
252
253
253
254 def addtemporaryincludes(repo, additional):
254 def addtemporaryincludes(repo, additional):
255 includes = readtemporaryincludes(repo)
255 includes = readtemporaryincludes(repo)
256 for i in additional:
256 for i in additional:
257 includes.add(i)
257 includes.add(i)
258 writetemporaryincludes(repo, includes)
258 writetemporaryincludes(repo, includes)
259
259
260
260
261 def prunetemporaryincludes(repo):
261 def prunetemporaryincludes(repo):
262 if not enabled or not repo.vfs.exists(b'tempsparse'):
262 if not enabled or not repo.vfs.exists(b'tempsparse'):
263 return
263 return
264
264
265 s = repo.status()
265 s = repo.status()
266 if s.modified or s.added or s.removed or s.deleted:
266 if s.modified or s.added or s.removed or s.deleted:
267 # Still have pending changes. Don't bother trying to prune.
267 # Still have pending changes. Don't bother trying to prune.
268 return
268 return
269
269
270 sparsematch = matcher(repo, includetemp=False)
270 sparsematch = matcher(repo, includetemp=False)
271 dirstate = repo.dirstate
271 dirstate = repo.dirstate
272 actions = []
272 actions = []
273 dropped = []
273 dropped = []
274 tempincludes = readtemporaryincludes(repo)
274 tempincludes = readtemporaryincludes(repo)
275 for file in tempincludes:
275 for file in tempincludes:
276 if file in dirstate and not sparsematch(file):
276 if file in dirstate and not sparsematch(file):
277 message = _(b'dropping temporarily included sparse files')
277 message = _(b'dropping temporarily included sparse files')
278 actions.append((file, None, message))
278 actions.append((file, None, message))
279 dropped.append(file)
279 dropped.append(file)
280
280
281 typeactions = mergemod.emptyactions()
281 typeactions = mergemod.emptyactions()
282 typeactions[b'r'] = actions
282 typeactions[b'r'] = actions
283 mergemod.applyupdates(
283 mergemod.applyupdates(
284 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
284 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
285 )
285 )
286
286
287 # Fix dirstate
287 # Fix dirstate
288 for file in dropped:
288 for file in dropped:
289 dirstate.drop(file)
289 dirstate.drop(file)
290
290
291 repo.vfs.unlink(b'tempsparse')
291 repo.vfs.unlink(b'tempsparse')
292 repo._sparsesignaturecache.clear()
292 repo._sparsesignaturecache.clear()
293 msg = _(
293 msg = _(
294 b'cleaned up %d temporarily added file(s) from the '
294 b'cleaned up %d temporarily added file(s) from the '
295 b'sparse checkout\n'
295 b'sparse checkout\n'
296 )
296 )
297 repo.ui.status(msg % len(tempincludes))
297 repo.ui.status(msg % len(tempincludes))
298
298
299
299
300 def forceincludematcher(matcher, includes):
300 def forceincludematcher(matcher, includes):
301 """Returns a matcher that returns true for any of the forced includes
301 """Returns a matcher that returns true for any of the forced includes
302 before testing against the actual matcher."""
302 before testing against the actual matcher."""
303 kindpats = [(b'path', include, b'') for include in includes]
303 kindpats = [(b'path', include, b'') for include in includes]
304 includematcher = matchmod.includematcher(b'', kindpats)
304 includematcher = matchmod.includematcher(b'', kindpats)
305 return matchmod.unionmatcher([includematcher, matcher])
305 return matchmod.unionmatcher([includematcher, matcher])
306
306
307
307
308 def matcher(repo, revs=None, includetemp=True):
308 def matcher(repo, revs=None, includetemp=True):
309 """Obtain a matcher for sparse working directories for the given revs.
309 """Obtain a matcher for sparse working directories for the given revs.
310
310
311 If multiple revisions are specified, the matcher is the union of all
311 If multiple revisions are specified, the matcher is the union of all
312 revs.
312 revs.
313
313
314 ``includetemp`` indicates whether to use the temporary sparse profile.
314 ``includetemp`` indicates whether to use the temporary sparse profile.
315 """
315 """
316 # If sparse isn't enabled, sparse matcher matches everything.
316 # If sparse isn't enabled, sparse matcher matches everything.
317 if not enabled:
317 if not enabled:
318 return matchmod.always()
318 return matchmod.always()
319
319
320 if not revs or revs == [None]:
320 if not revs or revs == [None]:
321 revs = [
321 revs = [
322 repo.changelog.rev(node)
322 repo.changelog.rev(node)
323 for node in repo.dirstate.parents()
323 for node in repo.dirstate.parents()
324 if node != nullid
324 if node != nullid
325 ]
325 ]
326
326
327 signature = configsignature(repo, includetemp=includetemp)
327 signature = configsignature(repo, includetemp=includetemp)
328
328
329 key = b'%s %s' % (signature, b' '.join(map(pycompat.bytestr, revs)))
329 key = b'%s %s' % (signature, b' '.join(map(pycompat.bytestr, revs)))
330
330
331 result = repo._sparsematchercache.get(key)
331 result = repo._sparsematchercache.get(key)
332 if result:
332 if result:
333 return result
333 return result
334
334
335 matchers = []
335 matchers = []
336 for rev in revs:
336 for rev in revs:
337 try:
337 try:
338 includes, excludes, profiles = patternsforrev(repo, rev)
338 includes, excludes, profiles = patternsforrev(repo, rev)
339
339
340 if includes or excludes:
340 if includes or excludes:
341 matcher = matchmod.match(
341 matcher = matchmod.match(
342 repo.root,
342 repo.root,
343 b'',
343 b'',
344 [],
344 [],
345 include=includes,
345 include=includes,
346 exclude=excludes,
346 exclude=excludes,
347 default=b'relpath',
347 default=b'relpath',
348 )
348 )
349 matchers.append(matcher)
349 matchers.append(matcher)
350 except IOError:
350 except IOError:
351 pass
351 pass
352
352
353 if not matchers:
353 if not matchers:
354 result = matchmod.always()
354 result = matchmod.always()
355 elif len(matchers) == 1:
355 elif len(matchers) == 1:
356 result = matchers[0]
356 result = matchers[0]
357 else:
357 else:
358 result = matchmod.unionmatcher(matchers)
358 result = matchmod.unionmatcher(matchers)
359
359
360 if includetemp:
360 if includetemp:
361 tempincludes = readtemporaryincludes(repo)
361 tempincludes = readtemporaryincludes(repo)
362 result = forceincludematcher(result, tempincludes)
362 result = forceincludematcher(result, tempincludes)
363
363
364 repo._sparsematchercache[key] = result
364 repo._sparsematchercache[key] = result
365
365
366 return result
366 return result
367
367
368
368
369 def filterupdatesactions(repo, wctx, mctx, branchmerge, mresult):
369 def filterupdatesactions(repo, wctx, mctx, branchmerge, mresult):
370 """Filter updates to only lay out files that match the sparse rules."""
370 """Filter updates to only lay out files that match the sparse rules."""
371 if not enabled:
371 if not enabled:
372 return
372 return
373
373
374 oldrevs = [pctx.rev() for pctx in wctx.parents()]
374 oldrevs = [pctx.rev() for pctx in wctx.parents()]
375 oldsparsematch = matcher(repo, oldrevs)
375 oldsparsematch = matcher(repo, oldrevs)
376
376
377 if oldsparsematch.always():
377 if oldsparsematch.always():
378 return
378 return
379
379
380 files = set()
380 files = set()
381 prunedactions = {}
381 prunedactions = {}
382
382
383 if branchmerge:
383 if branchmerge:
384 # If we're merging, use the wctx filter, since we're merging into
384 # If we're merging, use the wctx filter, since we're merging into
385 # the wctx.
385 # the wctx.
386 sparsematch = matcher(repo, [wctx.p1().rev()])
386 sparsematch = matcher(repo, [wctx.p1().rev()])
387 else:
387 else:
388 # If we're updating, use the target context's filter, since we're
388 # If we're updating, use the target context's filter, since we're
389 # moving to the target context.
389 # moving to the target context.
390 sparsematch = matcher(repo, [mctx.rev()])
390 sparsematch = matcher(repo, [mctx.rev()])
391
391
392 temporaryfiles = []
392 temporaryfiles = []
393 for file, action in pycompat.iteritems(mresult.actions):
393 for file, action in pycompat.iteritems(mresult.actions):
394 type, args, msg = action
394 type, args, msg = action
395 files.add(file)
395 files.add(file)
396 if sparsematch(file):
396 if sparsematch(file):
397 prunedactions[file] = action
397 prunedactions[file] = action
398 elif type == mergestatemod.ACTION_MERGE:
398 elif type == mergestatemod.ACTION_MERGE:
399 temporaryfiles.append(file)
399 temporaryfiles.append(file)
400 prunedactions[file] = action
400 prunedactions[file] = action
401 elif branchmerge:
401 elif branchmerge:
402 if type != mergestatemod.ACTION_KEEP:
402 if type != mergestatemod.ACTION_KEEP:
403 temporaryfiles.append(file)
403 temporaryfiles.append(file)
404 prunedactions[file] = action
404 prunedactions[file] = action
405 elif type == mergestatemod.ACTION_FORGET:
405 elif type == mergestatemod.ACTION_FORGET:
406 prunedactions[file] = action
406 prunedactions[file] = action
407 elif file in wctx:
407 elif file in wctx:
408 prunedactions[file] = (mergestatemod.ACTION_REMOVE, args, msg)
408 prunedactions[file] = (mergestatemod.ACTION_REMOVE, args, msg)
409
409
410 # in case or rename on one side, it is possible that f1 might not
411 # be present in sparse checkout we should include it
412 # TODO: should we do the same for f2?
413 # exists as a separate check because file can be in sparse and hence
414 # if we try to club this condition in above `elif type == ACTION_MERGE`
415 # it won't be triggered
410 if branchmerge and type == mergestatemod.ACTION_MERGE:
416 if branchmerge and type == mergestatemod.ACTION_MERGE:
411 f1, f2, fa, move, anc = args
417 f1, f2, fa, move, anc = args
412 if not sparsematch(f1):
418 if not sparsematch(f1):
413 temporaryfiles.append(f1)
419 temporaryfiles.append(f1)
414
420
415 if len(temporaryfiles) > 0:
421 if len(temporaryfiles) > 0:
416 repo.ui.status(
422 repo.ui.status(
417 _(
423 _(
418 b'temporarily included %d file(s) in the sparse '
424 b'temporarily included %d file(s) in the sparse '
419 b'checkout for merging\n'
425 b'checkout for merging\n'
420 )
426 )
421 % len(temporaryfiles)
427 % len(temporaryfiles)
422 )
428 )
423 addtemporaryincludes(repo, temporaryfiles)
429 addtemporaryincludes(repo, temporaryfiles)
424
430
425 # Add the new files to the working copy so they can be merged, etc
431 # Add the new files to the working copy so they can be merged, etc
426 actions = []
432 actions = []
427 message = b'temporarily adding to sparse checkout'
433 message = b'temporarily adding to sparse checkout'
428 wctxmanifest = repo[None].manifest()
434 wctxmanifest = repo[None].manifest()
429 for file in temporaryfiles:
435 for file in temporaryfiles:
430 if file in wctxmanifest:
436 if file in wctxmanifest:
431 fctx = repo[None][file]
437 fctx = repo[None][file]
432 actions.append((file, (fctx.flags(), False), message))
438 actions.append((file, (fctx.flags(), False), message))
433
439
434 typeactions = mergemod.emptyactions()
440 typeactions = mergemod.emptyactions()
435 typeactions[mergestatemod.ACTION_GET] = actions
441 typeactions[mergestatemod.ACTION_GET] = actions
436 mergemod.applyupdates(
442 mergemod.applyupdates(
437 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
443 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
438 )
444 )
439
445
440 dirstate = repo.dirstate
446 dirstate = repo.dirstate
441 for file, flags, msg in actions:
447 for file, flags, msg in actions:
442 dirstate.normal(file)
448 dirstate.normal(file)
443
449
444 profiles = activeconfig(repo)[2]
450 profiles = activeconfig(repo)[2]
445 changedprofiles = profiles & files
451 changedprofiles = profiles & files
446 # If an active profile changed during the update, refresh the checkout.
452 # If an active profile changed during the update, refresh the checkout.
447 # Don't do this during a branch merge, since all incoming changes should
453 # Don't do this during a branch merge, since all incoming changes should
448 # have been handled by the temporary includes above.
454 # have been handled by the temporary includes above.
449 if changedprofiles and not branchmerge:
455 if changedprofiles and not branchmerge:
450 mf = mctx.manifest()
456 mf = mctx.manifest()
451 for file in mf:
457 for file in mf:
452 old = oldsparsematch(file)
458 old = oldsparsematch(file)
453 new = sparsematch(file)
459 new = sparsematch(file)
454 if not old and new:
460 if not old and new:
455 flags = mf.flags(file)
461 flags = mf.flags(file)
456 prunedactions[file] = (
462 prunedactions[file] = (
457 mergestatemod.ACTION_GET,
463 mergestatemod.ACTION_GET,
458 (flags, False),
464 (flags, False),
459 b'',
465 b'',
460 )
466 )
461 elif old and not new:
467 elif old and not new:
462 prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'')
468 prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'')
463
469
464 mresult.setactions(prunedactions)
470 mresult.setactions(prunedactions)
465
471
466
472
467 def refreshwdir(repo, origstatus, origsparsematch, force=False):
473 def refreshwdir(repo, origstatus, origsparsematch, force=False):
468 """Refreshes working directory by taking sparse config into account.
474 """Refreshes working directory by taking sparse config into account.
469
475
470 The old status and sparse matcher is compared against the current sparse
476 The old status and sparse matcher is compared against the current sparse
471 matcher.
477 matcher.
472
478
473 Will abort if a file with pending changes is being excluded or included
479 Will abort if a file with pending changes is being excluded or included
474 unless ``force`` is True.
480 unless ``force`` is True.
475 """
481 """
476 # Verify there are no pending changes
482 # Verify there are no pending changes
477 pending = set()
483 pending = set()
478 pending.update(origstatus.modified)
484 pending.update(origstatus.modified)
479 pending.update(origstatus.added)
485 pending.update(origstatus.added)
480 pending.update(origstatus.removed)
486 pending.update(origstatus.removed)
481 sparsematch = matcher(repo)
487 sparsematch = matcher(repo)
482 abort = False
488 abort = False
483
489
484 for f in pending:
490 for f in pending:
485 if not sparsematch(f):
491 if not sparsematch(f):
486 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
492 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
487 abort = not force
493 abort = not force
488
494
489 if abort:
495 if abort:
490 raise error.Abort(
496 raise error.Abort(
491 _(b'could not update sparseness due to pending changes')
497 _(b'could not update sparseness due to pending changes')
492 )
498 )
493
499
494 # Calculate actions
500 # Calculate actions
495 dirstate = repo.dirstate
501 dirstate = repo.dirstate
496 ctx = repo[b'.']
502 ctx = repo[b'.']
497 added = []
503 added = []
498 lookup = []
504 lookup = []
499 dropped = []
505 dropped = []
500 mf = ctx.manifest()
506 mf = ctx.manifest()
501 files = set(mf)
507 files = set(mf)
502
508
503 actions = {}
509 actions = {}
504
510
505 for file in files:
511 for file in files:
506 old = origsparsematch(file)
512 old = origsparsematch(file)
507 new = sparsematch(file)
513 new = sparsematch(file)
508 # Add files that are newly included, or that don't exist in
514 # Add files that are newly included, or that don't exist in
509 # the dirstate yet.
515 # the dirstate yet.
510 if (new and not old) or (old and new and not file in dirstate):
516 if (new and not old) or (old and new and not file in dirstate):
511 fl = mf.flags(file)
517 fl = mf.flags(file)
512 if repo.wvfs.exists(file):
518 if repo.wvfs.exists(file):
513 actions[file] = (b'e', (fl,), b'')
519 actions[file] = (b'e', (fl,), b'')
514 lookup.append(file)
520 lookup.append(file)
515 else:
521 else:
516 actions[file] = (b'g', (fl, False), b'')
522 actions[file] = (b'g', (fl, False), b'')
517 added.append(file)
523 added.append(file)
518 # Drop files that are newly excluded, or that still exist in
524 # Drop files that are newly excluded, or that still exist in
519 # the dirstate.
525 # the dirstate.
520 elif (old and not new) or (not old and not new and file in dirstate):
526 elif (old and not new) or (not old and not new and file in dirstate):
521 dropped.append(file)
527 dropped.append(file)
522 if file not in pending:
528 if file not in pending:
523 actions[file] = (b'r', [], b'')
529 actions[file] = (b'r', [], b'')
524
530
525 # Verify there are no pending changes in newly included files
531 # Verify there are no pending changes in newly included files
526 abort = False
532 abort = False
527 for file in lookup:
533 for file in lookup:
528 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
534 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
529 abort = not force
535 abort = not force
530 if abort:
536 if abort:
531 raise error.Abort(
537 raise error.Abort(
532 _(
538 _(
533 b'cannot change sparseness due to pending '
539 b'cannot change sparseness due to pending '
534 b'changes (delete the files or use '
540 b'changes (delete the files or use '
535 b'--force to bring them back dirty)'
541 b'--force to bring them back dirty)'
536 )
542 )
537 )
543 )
538
544
539 # Check for files that were only in the dirstate.
545 # Check for files that were only in the dirstate.
540 for file, state in pycompat.iteritems(dirstate):
546 for file, state in pycompat.iteritems(dirstate):
541 if not file in files:
547 if not file in files:
542 old = origsparsematch(file)
548 old = origsparsematch(file)
543 new = sparsematch(file)
549 new = sparsematch(file)
544 if old and not new:
550 if old and not new:
545 dropped.append(file)
551 dropped.append(file)
546
552
547 # Apply changes to disk
553 # Apply changes to disk
548 typeactions = mergemod.emptyactions()
554 typeactions = mergemod.emptyactions()
549 for f, (m, args, msg) in pycompat.iteritems(actions):
555 for f, (m, args, msg) in pycompat.iteritems(actions):
550 typeactions[m].append((f, args, msg))
556 typeactions[m].append((f, args, msg))
551
557
552 mergemod.applyupdates(
558 mergemod.applyupdates(
553 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
559 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
554 )
560 )
555
561
556 # Fix dirstate
562 # Fix dirstate
557 for file in added:
563 for file in added:
558 dirstate.normal(file)
564 dirstate.normal(file)
559
565
560 for file in dropped:
566 for file in dropped:
561 dirstate.drop(file)
567 dirstate.drop(file)
562
568
563 for file in lookup:
569 for file in lookup:
564 # File exists on disk, and we're bringing it back in an unknown state.
570 # File exists on disk, and we're bringing it back in an unknown state.
565 dirstate.normallookup(file)
571 dirstate.normallookup(file)
566
572
567 return added, dropped, lookup
573 return added, dropped, lookup
568
574
569
575
570 def aftercommit(repo, node):
576 def aftercommit(repo, node):
571 """Perform actions after a working directory commit."""
577 """Perform actions after a working directory commit."""
572 # This function is called unconditionally, even if sparse isn't
578 # This function is called unconditionally, even if sparse isn't
573 # enabled.
579 # enabled.
574 ctx = repo[node]
580 ctx = repo[node]
575
581
576 profiles = patternsforrev(repo, ctx.rev())[2]
582 profiles = patternsforrev(repo, ctx.rev())[2]
577
583
578 # profiles will only have data if sparse is enabled.
584 # profiles will only have data if sparse is enabled.
579 if profiles & set(ctx.files()):
585 if profiles & set(ctx.files()):
580 origstatus = repo.status()
586 origstatus = repo.status()
581 origsparsematch = matcher(repo)
587 origsparsematch = matcher(repo)
582 refreshwdir(repo, origstatus, origsparsematch, force=True)
588 refreshwdir(repo, origstatus, origsparsematch, force=True)
583
589
584 prunetemporaryincludes(repo)
590 prunetemporaryincludes(repo)
585
591
586
592
587 def _updateconfigandrefreshwdir(
593 def _updateconfigandrefreshwdir(
588 repo, includes, excludes, profiles, force=False, removing=False
594 repo, includes, excludes, profiles, force=False, removing=False
589 ):
595 ):
590 """Update the sparse config and working directory state."""
596 """Update the sparse config and working directory state."""
591 raw = repo.vfs.tryread(b'sparse')
597 raw = repo.vfs.tryread(b'sparse')
592 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
598 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
593
599
594 oldstatus = repo.status()
600 oldstatus = repo.status()
595 oldmatch = matcher(repo)
601 oldmatch = matcher(repo)
596 oldrequires = set(repo.requirements)
602 oldrequires = set(repo.requirements)
597
603
598 # TODO remove this try..except once the matcher integrates better
604 # TODO remove this try..except once the matcher integrates better
599 # with dirstate. We currently have to write the updated config
605 # with dirstate. We currently have to write the updated config
600 # because that will invalidate the matcher cache and force a
606 # because that will invalidate the matcher cache and force a
601 # re-read. We ideally want to update the cached matcher on the
607 # re-read. We ideally want to update the cached matcher on the
602 # repo instance then flush the new config to disk once wdir is
608 # repo instance then flush the new config to disk once wdir is
603 # updated. But this requires massive rework to matcher() and its
609 # updated. But this requires massive rework to matcher() and its
604 # consumers.
610 # consumers.
605
611
606 if b'exp-sparse' in oldrequires and removing:
612 if b'exp-sparse' in oldrequires and removing:
607 repo.requirements.discard(b'exp-sparse')
613 repo.requirements.discard(b'exp-sparse')
608 scmutil.writereporequirements(repo)
614 scmutil.writereporequirements(repo)
609 elif b'exp-sparse' not in oldrequires:
615 elif b'exp-sparse' not in oldrequires:
610 repo.requirements.add(b'exp-sparse')
616 repo.requirements.add(b'exp-sparse')
611 scmutil.writereporequirements(repo)
617 scmutil.writereporequirements(repo)
612
618
613 try:
619 try:
614 writeconfig(repo, includes, excludes, profiles)
620 writeconfig(repo, includes, excludes, profiles)
615 return refreshwdir(repo, oldstatus, oldmatch, force=force)
621 return refreshwdir(repo, oldstatus, oldmatch, force=force)
616 except Exception:
622 except Exception:
617 if repo.requirements != oldrequires:
623 if repo.requirements != oldrequires:
618 repo.requirements.clear()
624 repo.requirements.clear()
619 repo.requirements |= oldrequires
625 repo.requirements |= oldrequires
620 scmutil.writereporequirements(repo)
626 scmutil.writereporequirements(repo)
621 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
627 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
622 raise
628 raise
623
629
624
630
625 def clearrules(repo, force=False):
631 def clearrules(repo, force=False):
626 """Clears include/exclude rules from the sparse config.
632 """Clears include/exclude rules from the sparse config.
627
633
628 The remaining sparse config only has profiles, if defined. The working
634 The remaining sparse config only has profiles, if defined. The working
629 directory is refreshed, as needed.
635 directory is refreshed, as needed.
630 """
636 """
631 with repo.wlock():
637 with repo.wlock():
632 raw = repo.vfs.tryread(b'sparse')
638 raw = repo.vfs.tryread(b'sparse')
633 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
639 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
634
640
635 if not includes and not excludes:
641 if not includes and not excludes:
636 return
642 return
637
643
638 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
644 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
639
645
640
646
641 def importfromfiles(repo, opts, paths, force=False):
647 def importfromfiles(repo, opts, paths, force=False):
642 """Import sparse config rules from files.
648 """Import sparse config rules from files.
643
649
644 The updated sparse config is written out and the working directory
650 The updated sparse config is written out and the working directory
645 is refreshed, as needed.
651 is refreshed, as needed.
646 """
652 """
647 with repo.wlock():
653 with repo.wlock():
648 # read current configuration
654 # read current configuration
649 raw = repo.vfs.tryread(b'sparse')
655 raw = repo.vfs.tryread(b'sparse')
650 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
656 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
651 aincludes, aexcludes, aprofiles = activeconfig(repo)
657 aincludes, aexcludes, aprofiles = activeconfig(repo)
652
658
653 # Import rules on top; only take in rules that are not yet
659 # Import rules on top; only take in rules that are not yet
654 # part of the active rules.
660 # part of the active rules.
655 changed = False
661 changed = False
656 for p in paths:
662 for p in paths:
657 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
663 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
658 raw = fh.read()
664 raw = fh.read()
659
665
660 iincludes, iexcludes, iprofiles = parseconfig(
666 iincludes, iexcludes, iprofiles = parseconfig(
661 repo.ui, raw, b'sparse'
667 repo.ui, raw, b'sparse'
662 )
668 )
663 oldsize = len(includes) + len(excludes) + len(profiles)
669 oldsize = len(includes) + len(excludes) + len(profiles)
664 includes.update(iincludes - aincludes)
670 includes.update(iincludes - aincludes)
665 excludes.update(iexcludes - aexcludes)
671 excludes.update(iexcludes - aexcludes)
666 profiles.update(iprofiles - aprofiles)
672 profiles.update(iprofiles - aprofiles)
667 if len(includes) + len(excludes) + len(profiles) > oldsize:
673 if len(includes) + len(excludes) + len(profiles) > oldsize:
668 changed = True
674 changed = True
669
675
670 profilecount = includecount = excludecount = 0
676 profilecount = includecount = excludecount = 0
671 fcounts = (0, 0, 0)
677 fcounts = (0, 0, 0)
672
678
673 if changed:
679 if changed:
674 profilecount = len(profiles - aprofiles)
680 profilecount = len(profiles - aprofiles)
675 includecount = len(includes - aincludes)
681 includecount = len(includes - aincludes)
676 excludecount = len(excludes - aexcludes)
682 excludecount = len(excludes - aexcludes)
677
683
678 fcounts = map(
684 fcounts = map(
679 len,
685 len,
680 _updateconfigandrefreshwdir(
686 _updateconfigandrefreshwdir(
681 repo, includes, excludes, profiles, force=force
687 repo, includes, excludes, profiles, force=force
682 ),
688 ),
683 )
689 )
684
690
685 printchanges(
691 printchanges(
686 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
692 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
687 )
693 )
688
694
689
695
690 def updateconfig(
696 def updateconfig(
691 repo,
697 repo,
692 pats,
698 pats,
693 opts,
699 opts,
694 include=False,
700 include=False,
695 exclude=False,
701 exclude=False,
696 reset=False,
702 reset=False,
697 delete=False,
703 delete=False,
698 enableprofile=False,
704 enableprofile=False,
699 disableprofile=False,
705 disableprofile=False,
700 force=False,
706 force=False,
701 usereporootpaths=False,
707 usereporootpaths=False,
702 ):
708 ):
703 """Perform a sparse config update.
709 """Perform a sparse config update.
704
710
705 Only one of the actions may be performed.
711 Only one of the actions may be performed.
706
712
707 The new config is written out and a working directory refresh is performed.
713 The new config is written out and a working directory refresh is performed.
708 """
714 """
709 with repo.wlock():
715 with repo.wlock():
710 raw = repo.vfs.tryread(b'sparse')
716 raw = repo.vfs.tryread(b'sparse')
711 oldinclude, oldexclude, oldprofiles = parseconfig(
717 oldinclude, oldexclude, oldprofiles = parseconfig(
712 repo.ui, raw, b'sparse'
718 repo.ui, raw, b'sparse'
713 )
719 )
714
720
715 if reset:
721 if reset:
716 newinclude = set()
722 newinclude = set()
717 newexclude = set()
723 newexclude = set()
718 newprofiles = set()
724 newprofiles = set()
719 else:
725 else:
720 newinclude = set(oldinclude)
726 newinclude = set(oldinclude)
721 newexclude = set(oldexclude)
727 newexclude = set(oldexclude)
722 newprofiles = set(oldprofiles)
728 newprofiles = set(oldprofiles)
723
729
724 if any(os.path.isabs(pat) for pat in pats):
730 if any(os.path.isabs(pat) for pat in pats):
725 raise error.Abort(_(b'paths cannot be absolute'))
731 raise error.Abort(_(b'paths cannot be absolute'))
726
732
727 if not usereporootpaths:
733 if not usereporootpaths:
728 # let's treat paths as relative to cwd
734 # let's treat paths as relative to cwd
729 root, cwd = repo.root, repo.getcwd()
735 root, cwd = repo.root, repo.getcwd()
730 abspats = []
736 abspats = []
731 for kindpat in pats:
737 for kindpat in pats:
732 kind, pat = matchmod._patsplit(kindpat, None)
738 kind, pat = matchmod._patsplit(kindpat, None)
733 if kind in matchmod.cwdrelativepatternkinds or kind is None:
739 if kind in matchmod.cwdrelativepatternkinds or kind is None:
734 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
740 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
735 root, cwd, pat
741 root, cwd, pat
736 )
742 )
737 abspats.append(ap)
743 abspats.append(ap)
738 else:
744 else:
739 abspats.append(kindpat)
745 abspats.append(kindpat)
740 pats = abspats
746 pats = abspats
741
747
742 if include:
748 if include:
743 newinclude.update(pats)
749 newinclude.update(pats)
744 elif exclude:
750 elif exclude:
745 newexclude.update(pats)
751 newexclude.update(pats)
746 elif enableprofile:
752 elif enableprofile:
747 newprofiles.update(pats)
753 newprofiles.update(pats)
748 elif disableprofile:
754 elif disableprofile:
749 newprofiles.difference_update(pats)
755 newprofiles.difference_update(pats)
750 elif delete:
756 elif delete:
751 newinclude.difference_update(pats)
757 newinclude.difference_update(pats)
752 newexclude.difference_update(pats)
758 newexclude.difference_update(pats)
753
759
754 profilecount = len(newprofiles - oldprofiles) - len(
760 profilecount = len(newprofiles - oldprofiles) - len(
755 oldprofiles - newprofiles
761 oldprofiles - newprofiles
756 )
762 )
757 includecount = len(newinclude - oldinclude) - len(
763 includecount = len(newinclude - oldinclude) - len(
758 oldinclude - newinclude
764 oldinclude - newinclude
759 )
765 )
760 excludecount = len(newexclude - oldexclude) - len(
766 excludecount = len(newexclude - oldexclude) - len(
761 oldexclude - newexclude
767 oldexclude - newexclude
762 )
768 )
763
769
764 fcounts = map(
770 fcounts = map(
765 len,
771 len,
766 _updateconfigandrefreshwdir(
772 _updateconfigandrefreshwdir(
767 repo,
773 repo,
768 newinclude,
774 newinclude,
769 newexclude,
775 newexclude,
770 newprofiles,
776 newprofiles,
771 force=force,
777 force=force,
772 removing=reset,
778 removing=reset,
773 ),
779 ),
774 )
780 )
775
781
776 printchanges(
782 printchanges(
777 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
783 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
778 )
784 )
779
785
780
786
781 def printchanges(
787 def printchanges(
782 ui,
788 ui,
783 opts,
789 opts,
784 profilecount=0,
790 profilecount=0,
785 includecount=0,
791 includecount=0,
786 excludecount=0,
792 excludecount=0,
787 added=0,
793 added=0,
788 dropped=0,
794 dropped=0,
789 conflicting=0,
795 conflicting=0,
790 ):
796 ):
791 """Print output summarizing sparse config changes."""
797 """Print output summarizing sparse config changes."""
792 with ui.formatter(b'sparse', opts) as fm:
798 with ui.formatter(b'sparse', opts) as fm:
793 fm.startitem()
799 fm.startitem()
794 fm.condwrite(
800 fm.condwrite(
795 ui.verbose,
801 ui.verbose,
796 b'profiles_added',
802 b'profiles_added',
797 _(b'Profiles changed: %d\n'),
803 _(b'Profiles changed: %d\n'),
798 profilecount,
804 profilecount,
799 )
805 )
800 fm.condwrite(
806 fm.condwrite(
801 ui.verbose,
807 ui.verbose,
802 b'include_rules_added',
808 b'include_rules_added',
803 _(b'Include rules changed: %d\n'),
809 _(b'Include rules changed: %d\n'),
804 includecount,
810 includecount,
805 )
811 )
806 fm.condwrite(
812 fm.condwrite(
807 ui.verbose,
813 ui.verbose,
808 b'exclude_rules_added',
814 b'exclude_rules_added',
809 _(b'Exclude rules changed: %d\n'),
815 _(b'Exclude rules changed: %d\n'),
810 excludecount,
816 excludecount,
811 )
817 )
812
818
813 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
819 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
814 # files are added or removed outside of the templating formatter
820 # files are added or removed outside of the templating formatter
815 # framework. No point in repeating ourselves in that case.
821 # framework. No point in repeating ourselves in that case.
816 if not fm.isplain():
822 if not fm.isplain():
817 fm.condwrite(
823 fm.condwrite(
818 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
824 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
819 )
825 )
820 fm.condwrite(
826 fm.condwrite(
821 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
827 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
822 )
828 )
823 fm.condwrite(
829 fm.condwrite(
824 ui.verbose,
830 ui.verbose,
825 b'files_conflicting',
831 b'files_conflicting',
826 _(b'Files conflicting: %d\n'),
832 _(b'Files conflicting: %d\n'),
827 conflicting,
833 conflicting,
828 )
834 )
General Comments 0
You need to be logged in to leave comments. Login now