##// END OF EJS Templates
sparse: replace merge action values with mergestate.ACTION_* constants...
Pulkit Goyal -
r45899:9320f668 default
parent child Browse files
Show More
@@ -1,829 +1,831 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 mresult = mergemod.mergeresult()
272 mresult = mergemod.mergeresult()
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 mresult.addfile(file, b'r', None, message)
278 mresult.addfile(file, mergestatemod.ACTION_REMOVE, None, message)
279 dropped.append(file)
279 dropped.append(file)
280
280
281 mergemod.applyupdates(
281 mergemod.applyupdates(
282 repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False
282 repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False
283 )
283 )
284
284
285 # Fix dirstate
285 # Fix dirstate
286 for file in dropped:
286 for file in dropped:
287 dirstate.drop(file)
287 dirstate.drop(file)
288
288
289 repo.vfs.unlink(b'tempsparse')
289 repo.vfs.unlink(b'tempsparse')
290 repo._sparsesignaturecache.clear()
290 repo._sparsesignaturecache.clear()
291 msg = _(
291 msg = _(
292 b'cleaned up %d temporarily added file(s) from the '
292 b'cleaned up %d temporarily added file(s) from the '
293 b'sparse checkout\n'
293 b'sparse checkout\n'
294 )
294 )
295 repo.ui.status(msg % len(tempincludes))
295 repo.ui.status(msg % len(tempincludes))
296
296
297
297
298 def forceincludematcher(matcher, includes):
298 def forceincludematcher(matcher, includes):
299 """Returns a matcher that returns true for any of the forced includes
299 """Returns a matcher that returns true for any of the forced includes
300 before testing against the actual matcher."""
300 before testing against the actual matcher."""
301 kindpats = [(b'path', include, b'') for include in includes]
301 kindpats = [(b'path', include, b'') for include in includes]
302 includematcher = matchmod.includematcher(b'', kindpats)
302 includematcher = matchmod.includematcher(b'', kindpats)
303 return matchmod.unionmatcher([includematcher, matcher])
303 return matchmod.unionmatcher([includematcher, matcher])
304
304
305
305
306 def matcher(repo, revs=None, includetemp=True):
306 def matcher(repo, revs=None, includetemp=True):
307 """Obtain a matcher for sparse working directories for the given revs.
307 """Obtain a matcher for sparse working directories for the given revs.
308
308
309 If multiple revisions are specified, the matcher is the union of all
309 If multiple revisions are specified, the matcher is the union of all
310 revs.
310 revs.
311
311
312 ``includetemp`` indicates whether to use the temporary sparse profile.
312 ``includetemp`` indicates whether to use the temporary sparse profile.
313 """
313 """
314 # If sparse isn't enabled, sparse matcher matches everything.
314 # If sparse isn't enabled, sparse matcher matches everything.
315 if not enabled:
315 if not enabled:
316 return matchmod.always()
316 return matchmod.always()
317
317
318 if not revs or revs == [None]:
318 if not revs or revs == [None]:
319 revs = [
319 revs = [
320 repo.changelog.rev(node)
320 repo.changelog.rev(node)
321 for node in repo.dirstate.parents()
321 for node in repo.dirstate.parents()
322 if node != nullid
322 if node != nullid
323 ]
323 ]
324
324
325 signature = configsignature(repo, includetemp=includetemp)
325 signature = configsignature(repo, includetemp=includetemp)
326
326
327 key = b'%s %s' % (signature, b' '.join(map(pycompat.bytestr, revs)))
327 key = b'%s %s' % (signature, b' '.join(map(pycompat.bytestr, revs)))
328
328
329 result = repo._sparsematchercache.get(key)
329 result = repo._sparsematchercache.get(key)
330 if result:
330 if result:
331 return result
331 return result
332
332
333 matchers = []
333 matchers = []
334 for rev in revs:
334 for rev in revs:
335 try:
335 try:
336 includes, excludes, profiles = patternsforrev(repo, rev)
336 includes, excludes, profiles = patternsforrev(repo, rev)
337
337
338 if includes or excludes:
338 if includes or excludes:
339 matcher = matchmod.match(
339 matcher = matchmod.match(
340 repo.root,
340 repo.root,
341 b'',
341 b'',
342 [],
342 [],
343 include=includes,
343 include=includes,
344 exclude=excludes,
344 exclude=excludes,
345 default=b'relpath',
345 default=b'relpath',
346 )
346 )
347 matchers.append(matcher)
347 matchers.append(matcher)
348 except IOError:
348 except IOError:
349 pass
349 pass
350
350
351 if not matchers:
351 if not matchers:
352 result = matchmod.always()
352 result = matchmod.always()
353 elif len(matchers) == 1:
353 elif len(matchers) == 1:
354 result = matchers[0]
354 result = matchers[0]
355 else:
355 else:
356 result = matchmod.unionmatcher(matchers)
356 result = matchmod.unionmatcher(matchers)
357
357
358 if includetemp:
358 if includetemp:
359 tempincludes = readtemporaryincludes(repo)
359 tempincludes = readtemporaryincludes(repo)
360 result = forceincludematcher(result, tempincludes)
360 result = forceincludematcher(result, tempincludes)
361
361
362 repo._sparsematchercache[key] = result
362 repo._sparsematchercache[key] = result
363
363
364 return result
364 return result
365
365
366
366
367 def filterupdatesactions(repo, wctx, mctx, branchmerge, mresult):
367 def filterupdatesactions(repo, wctx, mctx, branchmerge, mresult):
368 """Filter updates to only lay out files that match the sparse rules."""
368 """Filter updates to only lay out files that match the sparse rules."""
369 if not enabled:
369 if not enabled:
370 return
370 return
371
371
372 oldrevs = [pctx.rev() for pctx in wctx.parents()]
372 oldrevs = [pctx.rev() for pctx in wctx.parents()]
373 oldsparsematch = matcher(repo, oldrevs)
373 oldsparsematch = matcher(repo, oldrevs)
374
374
375 if oldsparsematch.always():
375 if oldsparsematch.always():
376 return
376 return
377
377
378 files = set()
378 files = set()
379 prunedactions = {}
379 prunedactions = {}
380
380
381 if branchmerge:
381 if branchmerge:
382 # If we're merging, use the wctx filter, since we're merging into
382 # If we're merging, use the wctx filter, since we're merging into
383 # the wctx.
383 # the wctx.
384 sparsematch = matcher(repo, [wctx.p1().rev()])
384 sparsematch = matcher(repo, [wctx.p1().rev()])
385 else:
385 else:
386 # If we're updating, use the target context's filter, since we're
386 # If we're updating, use the target context's filter, since we're
387 # moving to the target context.
387 # moving to the target context.
388 sparsematch = matcher(repo, [mctx.rev()])
388 sparsematch = matcher(repo, [mctx.rev()])
389
389
390 temporaryfiles = []
390 temporaryfiles = []
391 for file, action in pycompat.iteritems(mresult.actions):
391 for file, action in pycompat.iteritems(mresult.actions):
392 type, args, msg = action
392 type, args, msg = action
393 files.add(file)
393 files.add(file)
394 if sparsematch(file):
394 if sparsematch(file):
395 prunedactions[file] = action
395 prunedactions[file] = action
396 elif type == mergestatemod.ACTION_MERGE:
396 elif type == mergestatemod.ACTION_MERGE:
397 temporaryfiles.append(file)
397 temporaryfiles.append(file)
398 prunedactions[file] = action
398 prunedactions[file] = action
399 elif branchmerge:
399 elif branchmerge:
400 if type != mergestatemod.ACTION_KEEP:
400 if type != mergestatemod.ACTION_KEEP:
401 temporaryfiles.append(file)
401 temporaryfiles.append(file)
402 prunedactions[file] = action
402 prunedactions[file] = action
403 elif type == mergestatemod.ACTION_FORGET:
403 elif type == mergestatemod.ACTION_FORGET:
404 prunedactions[file] = action
404 prunedactions[file] = action
405 elif file in wctx:
405 elif file in wctx:
406 prunedactions[file] = (mergestatemod.ACTION_REMOVE, args, msg)
406 prunedactions[file] = (mergestatemod.ACTION_REMOVE, args, msg)
407
407
408 # in case or rename on one side, it is possible that f1 might not
408 # in case or rename on one side, it is possible that f1 might not
409 # be present in sparse checkout we should include it
409 # be present in sparse checkout we should include it
410 # TODO: should we do the same for f2?
410 # TODO: should we do the same for f2?
411 # exists as a separate check because file can be in sparse and hence
411 # exists as a separate check because file can be in sparse and hence
412 # if we try to club this condition in above `elif type == ACTION_MERGE`
412 # if we try to club this condition in above `elif type == ACTION_MERGE`
413 # it won't be triggered
413 # it won't be triggered
414 if branchmerge and type == mergestatemod.ACTION_MERGE:
414 if branchmerge and type == mergestatemod.ACTION_MERGE:
415 f1, f2, fa, move, anc = args
415 f1, f2, fa, move, anc = args
416 if not sparsematch(f1):
416 if not sparsematch(f1):
417 temporaryfiles.append(f1)
417 temporaryfiles.append(f1)
418
418
419 if len(temporaryfiles) > 0:
419 if len(temporaryfiles) > 0:
420 repo.ui.status(
420 repo.ui.status(
421 _(
421 _(
422 b'temporarily included %d file(s) in the sparse '
422 b'temporarily included %d file(s) in the sparse '
423 b'checkout for merging\n'
423 b'checkout for merging\n'
424 )
424 )
425 % len(temporaryfiles)
425 % len(temporaryfiles)
426 )
426 )
427 addtemporaryincludes(repo, temporaryfiles)
427 addtemporaryincludes(repo, temporaryfiles)
428
428
429 # Add the new files to the working copy so they can be merged, etc
429 # Add the new files to the working copy so they can be merged, etc
430 tmresult = mergemod.mergeresult()
430 tmresult = mergemod.mergeresult()
431 message = b'temporarily adding to sparse checkout'
431 message = b'temporarily adding to sparse checkout'
432 wctxmanifest = repo[None].manifest()
432 wctxmanifest = repo[None].manifest()
433 for file in temporaryfiles:
433 for file in temporaryfiles:
434 if file in wctxmanifest:
434 if file in wctxmanifest:
435 fctx = repo[None][file]
435 fctx = repo[None][file]
436 tmresult.addfile(
436 tmresult.addfile(
437 file,
437 file,
438 mergestatemod.ACTION_GET,
438 mergestatemod.ACTION_GET,
439 (fctx.flags(), False),
439 (fctx.flags(), False),
440 message,
440 message,
441 )
441 )
442
442
443 mergemod.applyupdates(
443 mergemod.applyupdates(
444 repo, tmresult, repo[None], repo[b'.'], False, wantfiledata=False
444 repo, tmresult, repo[None], repo[b'.'], False, wantfiledata=False
445 )
445 )
446
446
447 dirstate = repo.dirstate
447 dirstate = repo.dirstate
448 for file, flags, msg in tmresult.getactions([mergestatemod.ACTION_GET]):
448 for file, flags, msg in tmresult.getactions([mergestatemod.ACTION_GET]):
449 dirstate.normal(file)
449 dirstate.normal(file)
450
450
451 profiles = activeconfig(repo)[2]
451 profiles = activeconfig(repo)[2]
452 changedprofiles = profiles & files
452 changedprofiles = profiles & files
453 # If an active profile changed during the update, refresh the checkout.
453 # If an active profile changed during the update, refresh the checkout.
454 # Don't do this during a branch merge, since all incoming changes should
454 # Don't do this during a branch merge, since all incoming changes should
455 # have been handled by the temporary includes above.
455 # have been handled by the temporary includes above.
456 if changedprofiles and not branchmerge:
456 if changedprofiles and not branchmerge:
457 mf = mctx.manifest()
457 mf = mctx.manifest()
458 for file in mf:
458 for file in mf:
459 old = oldsparsematch(file)
459 old = oldsparsematch(file)
460 new = sparsematch(file)
460 new = sparsematch(file)
461 if not old and new:
461 if not old and new:
462 flags = mf.flags(file)
462 flags = mf.flags(file)
463 prunedactions[file] = (
463 prunedactions[file] = (
464 mergestatemod.ACTION_GET,
464 mergestatemod.ACTION_GET,
465 (flags, False),
465 (flags, False),
466 b'',
466 b'',
467 )
467 )
468 elif old and not new:
468 elif old and not new:
469 prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'')
469 prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'')
470
470
471 mresult.setactions(prunedactions)
471 mresult.setactions(prunedactions)
472
472
473
473
474 def refreshwdir(repo, origstatus, origsparsematch, force=False):
474 def refreshwdir(repo, origstatus, origsparsematch, force=False):
475 """Refreshes working directory by taking sparse config into account.
475 """Refreshes working directory by taking sparse config into account.
476
476
477 The old status and sparse matcher is compared against the current sparse
477 The old status and sparse matcher is compared against the current sparse
478 matcher.
478 matcher.
479
479
480 Will abort if a file with pending changes is being excluded or included
480 Will abort if a file with pending changes is being excluded or included
481 unless ``force`` is True.
481 unless ``force`` is True.
482 """
482 """
483 # Verify there are no pending changes
483 # Verify there are no pending changes
484 pending = set()
484 pending = set()
485 pending.update(origstatus.modified)
485 pending.update(origstatus.modified)
486 pending.update(origstatus.added)
486 pending.update(origstatus.added)
487 pending.update(origstatus.removed)
487 pending.update(origstatus.removed)
488 sparsematch = matcher(repo)
488 sparsematch = matcher(repo)
489 abort = False
489 abort = False
490
490
491 for f in pending:
491 for f in pending:
492 if not sparsematch(f):
492 if not sparsematch(f):
493 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
493 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
494 abort = not force
494 abort = not force
495
495
496 if abort:
496 if abort:
497 raise error.Abort(
497 raise error.Abort(
498 _(b'could not update sparseness due to pending changes')
498 _(b'could not update sparseness due to pending changes')
499 )
499 )
500
500
501 # Calculate merge result
501 # Calculate merge result
502 dirstate = repo.dirstate
502 dirstate = repo.dirstate
503 ctx = repo[b'.']
503 ctx = repo[b'.']
504 added = []
504 added = []
505 lookup = []
505 lookup = []
506 dropped = []
506 dropped = []
507 mf = ctx.manifest()
507 mf = ctx.manifest()
508 files = set(mf)
508 files = set(mf)
509 mresult = mergemod.mergeresult()
509 mresult = mergemod.mergeresult()
510
510
511 for file in files:
511 for file in files:
512 old = origsparsematch(file)
512 old = origsparsematch(file)
513 new = sparsematch(file)
513 new = sparsematch(file)
514 # 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
515 # the dirstate yet.
515 # the dirstate yet.
516 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):
517 fl = mf.flags(file)
517 fl = mf.flags(file)
518 if repo.wvfs.exists(file):
518 if repo.wvfs.exists(file):
519 mresult.addfile(file, b'e', (fl,), b'')
519 mresult.addfile(file, mergestatemod.ACTION_EXEC, (fl,), b'')
520 lookup.append(file)
520 lookup.append(file)
521 else:
521 else:
522 mresult.addfile(file, b'g', (fl, False), b'')
522 mresult.addfile(
523 file, mergestatemod.ACTION_GET, (fl, False), b''
524 )
523 added.append(file)
525 added.append(file)
524 # Drop files that are newly excluded, or that still exist in
526 # Drop files that are newly excluded, or that still exist in
525 # the dirstate.
527 # the dirstate.
526 elif (old and not new) or (not old and not new and file in dirstate):
528 elif (old and not new) or (not old and not new and file in dirstate):
527 dropped.append(file)
529 dropped.append(file)
528 if file not in pending:
530 if file not in pending:
529 mresult.addfile(file, b'r', [], b'')
531 mresult.addfile(file, mergestatemod.ACTION_REMOVE, [], b'')
530
532
531 # Verify there are no pending changes in newly included files
533 # Verify there are no pending changes in newly included files
532 abort = False
534 abort = False
533 for file in lookup:
535 for file in lookup:
534 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
536 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
535 abort = not force
537 abort = not force
536 if abort:
538 if abort:
537 raise error.Abort(
539 raise error.Abort(
538 _(
540 _(
539 b'cannot change sparseness due to pending '
541 b'cannot change sparseness due to pending '
540 b'changes (delete the files or use '
542 b'changes (delete the files or use '
541 b'--force to bring them back dirty)'
543 b'--force to bring them back dirty)'
542 )
544 )
543 )
545 )
544
546
545 # Check for files that were only in the dirstate.
547 # Check for files that were only in the dirstate.
546 for file, state in pycompat.iteritems(dirstate):
548 for file, state in pycompat.iteritems(dirstate):
547 if not file in files:
549 if not file in files:
548 old = origsparsematch(file)
550 old = origsparsematch(file)
549 new = sparsematch(file)
551 new = sparsematch(file)
550 if old and not new:
552 if old and not new:
551 dropped.append(file)
553 dropped.append(file)
552
554
553 mergemod.applyupdates(
555 mergemod.applyupdates(
554 repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False
556 repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False
555 )
557 )
556
558
557 # Fix dirstate
559 # Fix dirstate
558 for file in added:
560 for file in added:
559 dirstate.normal(file)
561 dirstate.normal(file)
560
562
561 for file in dropped:
563 for file in dropped:
562 dirstate.drop(file)
564 dirstate.drop(file)
563
565
564 for file in lookup:
566 for file in lookup:
565 # File exists on disk, and we're bringing it back in an unknown state.
567 # File exists on disk, and we're bringing it back in an unknown state.
566 dirstate.normallookup(file)
568 dirstate.normallookup(file)
567
569
568 return added, dropped, lookup
570 return added, dropped, lookup
569
571
570
572
571 def aftercommit(repo, node):
573 def aftercommit(repo, node):
572 """Perform actions after a working directory commit."""
574 """Perform actions after a working directory commit."""
573 # This function is called unconditionally, even if sparse isn't
575 # This function is called unconditionally, even if sparse isn't
574 # enabled.
576 # enabled.
575 ctx = repo[node]
577 ctx = repo[node]
576
578
577 profiles = patternsforrev(repo, ctx.rev())[2]
579 profiles = patternsforrev(repo, ctx.rev())[2]
578
580
579 # profiles will only have data if sparse is enabled.
581 # profiles will only have data if sparse is enabled.
580 if profiles & set(ctx.files()):
582 if profiles & set(ctx.files()):
581 origstatus = repo.status()
583 origstatus = repo.status()
582 origsparsematch = matcher(repo)
584 origsparsematch = matcher(repo)
583 refreshwdir(repo, origstatus, origsparsematch, force=True)
585 refreshwdir(repo, origstatus, origsparsematch, force=True)
584
586
585 prunetemporaryincludes(repo)
587 prunetemporaryincludes(repo)
586
588
587
589
588 def _updateconfigandrefreshwdir(
590 def _updateconfigandrefreshwdir(
589 repo, includes, excludes, profiles, force=False, removing=False
591 repo, includes, excludes, profiles, force=False, removing=False
590 ):
592 ):
591 """Update the sparse config and working directory state."""
593 """Update the sparse config and working directory state."""
592 raw = repo.vfs.tryread(b'sparse')
594 raw = repo.vfs.tryread(b'sparse')
593 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
595 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
594
596
595 oldstatus = repo.status()
597 oldstatus = repo.status()
596 oldmatch = matcher(repo)
598 oldmatch = matcher(repo)
597 oldrequires = set(repo.requirements)
599 oldrequires = set(repo.requirements)
598
600
599 # TODO remove this try..except once the matcher integrates better
601 # TODO remove this try..except once the matcher integrates better
600 # with dirstate. We currently have to write the updated config
602 # with dirstate. We currently have to write the updated config
601 # because that will invalidate the matcher cache and force a
603 # because that will invalidate the matcher cache and force a
602 # re-read. We ideally want to update the cached matcher on the
604 # re-read. We ideally want to update the cached matcher on the
603 # repo instance then flush the new config to disk once wdir is
605 # repo instance then flush the new config to disk once wdir is
604 # updated. But this requires massive rework to matcher() and its
606 # updated. But this requires massive rework to matcher() and its
605 # consumers.
607 # consumers.
606
608
607 if b'exp-sparse' in oldrequires and removing:
609 if b'exp-sparse' in oldrequires and removing:
608 repo.requirements.discard(b'exp-sparse')
610 repo.requirements.discard(b'exp-sparse')
609 scmutil.writereporequirements(repo)
611 scmutil.writereporequirements(repo)
610 elif b'exp-sparse' not in oldrequires:
612 elif b'exp-sparse' not in oldrequires:
611 repo.requirements.add(b'exp-sparse')
613 repo.requirements.add(b'exp-sparse')
612 scmutil.writereporequirements(repo)
614 scmutil.writereporequirements(repo)
613
615
614 try:
616 try:
615 writeconfig(repo, includes, excludes, profiles)
617 writeconfig(repo, includes, excludes, profiles)
616 return refreshwdir(repo, oldstatus, oldmatch, force=force)
618 return refreshwdir(repo, oldstatus, oldmatch, force=force)
617 except Exception:
619 except Exception:
618 if repo.requirements != oldrequires:
620 if repo.requirements != oldrequires:
619 repo.requirements.clear()
621 repo.requirements.clear()
620 repo.requirements |= oldrequires
622 repo.requirements |= oldrequires
621 scmutil.writereporequirements(repo)
623 scmutil.writereporequirements(repo)
622 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
624 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
623 raise
625 raise
624
626
625
627
626 def clearrules(repo, force=False):
628 def clearrules(repo, force=False):
627 """Clears include/exclude rules from the sparse config.
629 """Clears include/exclude rules from the sparse config.
628
630
629 The remaining sparse config only has profiles, if defined. The working
631 The remaining sparse config only has profiles, if defined. The working
630 directory is refreshed, as needed.
632 directory is refreshed, as needed.
631 """
633 """
632 with repo.wlock():
634 with repo.wlock():
633 raw = repo.vfs.tryread(b'sparse')
635 raw = repo.vfs.tryread(b'sparse')
634 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
636 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
635
637
636 if not includes and not excludes:
638 if not includes and not excludes:
637 return
639 return
638
640
639 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
641 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
640
642
641
643
642 def importfromfiles(repo, opts, paths, force=False):
644 def importfromfiles(repo, opts, paths, force=False):
643 """Import sparse config rules from files.
645 """Import sparse config rules from files.
644
646
645 The updated sparse config is written out and the working directory
647 The updated sparse config is written out and the working directory
646 is refreshed, as needed.
648 is refreshed, as needed.
647 """
649 """
648 with repo.wlock():
650 with repo.wlock():
649 # read current configuration
651 # read current configuration
650 raw = repo.vfs.tryread(b'sparse')
652 raw = repo.vfs.tryread(b'sparse')
651 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
653 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
652 aincludes, aexcludes, aprofiles = activeconfig(repo)
654 aincludes, aexcludes, aprofiles = activeconfig(repo)
653
655
654 # Import rules on top; only take in rules that are not yet
656 # Import rules on top; only take in rules that are not yet
655 # part of the active rules.
657 # part of the active rules.
656 changed = False
658 changed = False
657 for p in paths:
659 for p in paths:
658 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
660 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
659 raw = fh.read()
661 raw = fh.read()
660
662
661 iincludes, iexcludes, iprofiles = parseconfig(
663 iincludes, iexcludes, iprofiles = parseconfig(
662 repo.ui, raw, b'sparse'
664 repo.ui, raw, b'sparse'
663 )
665 )
664 oldsize = len(includes) + len(excludes) + len(profiles)
666 oldsize = len(includes) + len(excludes) + len(profiles)
665 includes.update(iincludes - aincludes)
667 includes.update(iincludes - aincludes)
666 excludes.update(iexcludes - aexcludes)
668 excludes.update(iexcludes - aexcludes)
667 profiles.update(iprofiles - aprofiles)
669 profiles.update(iprofiles - aprofiles)
668 if len(includes) + len(excludes) + len(profiles) > oldsize:
670 if len(includes) + len(excludes) + len(profiles) > oldsize:
669 changed = True
671 changed = True
670
672
671 profilecount = includecount = excludecount = 0
673 profilecount = includecount = excludecount = 0
672 fcounts = (0, 0, 0)
674 fcounts = (0, 0, 0)
673
675
674 if changed:
676 if changed:
675 profilecount = len(profiles - aprofiles)
677 profilecount = len(profiles - aprofiles)
676 includecount = len(includes - aincludes)
678 includecount = len(includes - aincludes)
677 excludecount = len(excludes - aexcludes)
679 excludecount = len(excludes - aexcludes)
678
680
679 fcounts = map(
681 fcounts = map(
680 len,
682 len,
681 _updateconfigandrefreshwdir(
683 _updateconfigandrefreshwdir(
682 repo, includes, excludes, profiles, force=force
684 repo, includes, excludes, profiles, force=force
683 ),
685 ),
684 )
686 )
685
687
686 printchanges(
688 printchanges(
687 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
689 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
688 )
690 )
689
691
690
692
691 def updateconfig(
693 def updateconfig(
692 repo,
694 repo,
693 pats,
695 pats,
694 opts,
696 opts,
695 include=False,
697 include=False,
696 exclude=False,
698 exclude=False,
697 reset=False,
699 reset=False,
698 delete=False,
700 delete=False,
699 enableprofile=False,
701 enableprofile=False,
700 disableprofile=False,
702 disableprofile=False,
701 force=False,
703 force=False,
702 usereporootpaths=False,
704 usereporootpaths=False,
703 ):
705 ):
704 """Perform a sparse config update.
706 """Perform a sparse config update.
705
707
706 Only one of the actions may be performed.
708 Only one of the actions may be performed.
707
709
708 The new config is written out and a working directory refresh is performed.
710 The new config is written out and a working directory refresh is performed.
709 """
711 """
710 with repo.wlock():
712 with repo.wlock():
711 raw = repo.vfs.tryread(b'sparse')
713 raw = repo.vfs.tryread(b'sparse')
712 oldinclude, oldexclude, oldprofiles = parseconfig(
714 oldinclude, oldexclude, oldprofiles = parseconfig(
713 repo.ui, raw, b'sparse'
715 repo.ui, raw, b'sparse'
714 )
716 )
715
717
716 if reset:
718 if reset:
717 newinclude = set()
719 newinclude = set()
718 newexclude = set()
720 newexclude = set()
719 newprofiles = set()
721 newprofiles = set()
720 else:
722 else:
721 newinclude = set(oldinclude)
723 newinclude = set(oldinclude)
722 newexclude = set(oldexclude)
724 newexclude = set(oldexclude)
723 newprofiles = set(oldprofiles)
725 newprofiles = set(oldprofiles)
724
726
725 if any(os.path.isabs(pat) for pat in pats):
727 if any(os.path.isabs(pat) for pat in pats):
726 raise error.Abort(_(b'paths cannot be absolute'))
728 raise error.Abort(_(b'paths cannot be absolute'))
727
729
728 if not usereporootpaths:
730 if not usereporootpaths:
729 # let's treat paths as relative to cwd
731 # let's treat paths as relative to cwd
730 root, cwd = repo.root, repo.getcwd()
732 root, cwd = repo.root, repo.getcwd()
731 abspats = []
733 abspats = []
732 for kindpat in pats:
734 for kindpat in pats:
733 kind, pat = matchmod._patsplit(kindpat, None)
735 kind, pat = matchmod._patsplit(kindpat, None)
734 if kind in matchmod.cwdrelativepatternkinds or kind is None:
736 if kind in matchmod.cwdrelativepatternkinds or kind is None:
735 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
737 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
736 root, cwd, pat
738 root, cwd, pat
737 )
739 )
738 abspats.append(ap)
740 abspats.append(ap)
739 else:
741 else:
740 abspats.append(kindpat)
742 abspats.append(kindpat)
741 pats = abspats
743 pats = abspats
742
744
743 if include:
745 if include:
744 newinclude.update(pats)
746 newinclude.update(pats)
745 elif exclude:
747 elif exclude:
746 newexclude.update(pats)
748 newexclude.update(pats)
747 elif enableprofile:
749 elif enableprofile:
748 newprofiles.update(pats)
750 newprofiles.update(pats)
749 elif disableprofile:
751 elif disableprofile:
750 newprofiles.difference_update(pats)
752 newprofiles.difference_update(pats)
751 elif delete:
753 elif delete:
752 newinclude.difference_update(pats)
754 newinclude.difference_update(pats)
753 newexclude.difference_update(pats)
755 newexclude.difference_update(pats)
754
756
755 profilecount = len(newprofiles - oldprofiles) - len(
757 profilecount = len(newprofiles - oldprofiles) - len(
756 oldprofiles - newprofiles
758 oldprofiles - newprofiles
757 )
759 )
758 includecount = len(newinclude - oldinclude) - len(
760 includecount = len(newinclude - oldinclude) - len(
759 oldinclude - newinclude
761 oldinclude - newinclude
760 )
762 )
761 excludecount = len(newexclude - oldexclude) - len(
763 excludecount = len(newexclude - oldexclude) - len(
762 oldexclude - newexclude
764 oldexclude - newexclude
763 )
765 )
764
766
765 fcounts = map(
767 fcounts = map(
766 len,
768 len,
767 _updateconfigandrefreshwdir(
769 _updateconfigandrefreshwdir(
768 repo,
770 repo,
769 newinclude,
771 newinclude,
770 newexclude,
772 newexclude,
771 newprofiles,
773 newprofiles,
772 force=force,
774 force=force,
773 removing=reset,
775 removing=reset,
774 ),
776 ),
775 )
777 )
776
778
777 printchanges(
779 printchanges(
778 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
780 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
779 )
781 )
780
782
781
783
782 def printchanges(
784 def printchanges(
783 ui,
785 ui,
784 opts,
786 opts,
785 profilecount=0,
787 profilecount=0,
786 includecount=0,
788 includecount=0,
787 excludecount=0,
789 excludecount=0,
788 added=0,
790 added=0,
789 dropped=0,
791 dropped=0,
790 conflicting=0,
792 conflicting=0,
791 ):
793 ):
792 """Print output summarizing sparse config changes."""
794 """Print output summarizing sparse config changes."""
793 with ui.formatter(b'sparse', opts) as fm:
795 with ui.formatter(b'sparse', opts) as fm:
794 fm.startitem()
796 fm.startitem()
795 fm.condwrite(
797 fm.condwrite(
796 ui.verbose,
798 ui.verbose,
797 b'profiles_added',
799 b'profiles_added',
798 _(b'Profiles changed: %d\n'),
800 _(b'Profiles changed: %d\n'),
799 profilecount,
801 profilecount,
800 )
802 )
801 fm.condwrite(
803 fm.condwrite(
802 ui.verbose,
804 ui.verbose,
803 b'include_rules_added',
805 b'include_rules_added',
804 _(b'Include rules changed: %d\n'),
806 _(b'Include rules changed: %d\n'),
805 includecount,
807 includecount,
806 )
808 )
807 fm.condwrite(
809 fm.condwrite(
808 ui.verbose,
810 ui.verbose,
809 b'exclude_rules_added',
811 b'exclude_rules_added',
810 _(b'Exclude rules changed: %d\n'),
812 _(b'Exclude rules changed: %d\n'),
811 excludecount,
813 excludecount,
812 )
814 )
813
815
814 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
816 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
815 # files are added or removed outside of the templating formatter
817 # files are added or removed outside of the templating formatter
816 # framework. No point in repeating ourselves in that case.
818 # framework. No point in repeating ourselves in that case.
817 if not fm.isplain():
819 if not fm.isplain():
818 fm.condwrite(
820 fm.condwrite(
819 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
821 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
820 )
822 )
821 fm.condwrite(
823 fm.condwrite(
822 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
824 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
823 )
825 )
824 fm.condwrite(
826 fm.condwrite(
825 ui.verbose,
827 ui.verbose,
826 b'files_conflicting',
828 b'files_conflicting',
827 _(b'Files conflicting: %d\n'),
829 _(b'Files conflicting: %d\n'),
828 conflicting,
830 conflicting,
829 )
831 )
General Comments 0
You need to be logged in to leave comments. Login now