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