##// END OF EJS Templates
sparse: use `update_file` instead of `normal` during `applyupdates`...
marmoute -
r48509:a0e79084 default
parent child Browse files
Show More
@@ -1,838 +1,838 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 with repo.dirstate.parentchange():
442 with repo.dirstate.parentchange():
443 mergemod.applyupdates(
443 mergemod.applyupdates(
444 repo,
444 repo,
445 tmresult,
445 tmresult,
446 repo[None],
446 repo[None],
447 repo[b'.'],
447 repo[b'.'],
448 False,
448 False,
449 wantfiledata=False,
449 wantfiledata=False,
450 )
450 )
451
451
452 dirstate = repo.dirstate
452 dirstate = repo.dirstate
453 for file, flags, msg in tmresult.getactions(
453 for file, flags, msg in tmresult.getactions(
454 [mergestatemod.ACTION_GET]
454 [mergestatemod.ACTION_GET]
455 ):
455 ):
456 dirstate.normal(file)
456 dirstate.update_file(file, p1_tracked=True, wc_tracked=True)
457
457
458 profiles = activeconfig(repo)[2]
458 profiles = activeconfig(repo)[2]
459 changedprofiles = profiles & files
459 changedprofiles = profiles & files
460 # If an active profile changed during the update, refresh the checkout.
460 # If an active profile changed during the update, refresh the checkout.
461 # Don't do this during a branch merge, since all incoming changes should
461 # Don't do this during a branch merge, since all incoming changes should
462 # have been handled by the temporary includes above.
462 # have been handled by the temporary includes above.
463 if changedprofiles and not branchmerge:
463 if changedprofiles and not branchmerge:
464 mf = mctx.manifest()
464 mf = mctx.manifest()
465 for file in mf:
465 for file in mf:
466 old = oldsparsematch(file)
466 old = oldsparsematch(file)
467 new = sparsematch(file)
467 new = sparsematch(file)
468 if not old and new:
468 if not old and new:
469 flags = mf.flags(file)
469 flags = mf.flags(file)
470 prunedactions[file] = (
470 prunedactions[file] = (
471 mergestatemod.ACTION_GET,
471 mergestatemod.ACTION_GET,
472 (flags, False),
472 (flags, False),
473 b'',
473 b'',
474 )
474 )
475 elif old and not new:
475 elif old and not new:
476 prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'')
476 prunedactions[file] = (mergestatemod.ACTION_REMOVE, [], b'')
477
477
478 mresult.setactions(prunedactions)
478 mresult.setactions(prunedactions)
479
479
480
480
481 def refreshwdir(repo, origstatus, origsparsematch, force=False):
481 def refreshwdir(repo, origstatus, origsparsematch, force=False):
482 """Refreshes working directory by taking sparse config into account.
482 """Refreshes working directory by taking sparse config into account.
483
483
484 The old status and sparse matcher is compared against the current sparse
484 The old status and sparse matcher is compared against the current sparse
485 matcher.
485 matcher.
486
486
487 Will abort if a file with pending changes is being excluded or included
487 Will abort if a file with pending changes is being excluded or included
488 unless ``force`` is True.
488 unless ``force`` is True.
489 """
489 """
490 # Verify there are no pending changes
490 # Verify there are no pending changes
491 pending = set()
491 pending = set()
492 pending.update(origstatus.modified)
492 pending.update(origstatus.modified)
493 pending.update(origstatus.added)
493 pending.update(origstatus.added)
494 pending.update(origstatus.removed)
494 pending.update(origstatus.removed)
495 sparsematch = matcher(repo)
495 sparsematch = matcher(repo)
496 abort = False
496 abort = False
497
497
498 for f in pending:
498 for f in pending:
499 if not sparsematch(f):
499 if not sparsematch(f):
500 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
500 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
501 abort = not force
501 abort = not force
502
502
503 if abort:
503 if abort:
504 raise error.Abort(
504 raise error.Abort(
505 _(b'could not update sparseness due to pending changes')
505 _(b'could not update sparseness due to pending changes')
506 )
506 )
507
507
508 # Calculate merge result
508 # Calculate merge result
509 dirstate = repo.dirstate
509 dirstate = repo.dirstate
510 ctx = repo[b'.']
510 ctx = repo[b'.']
511 added = []
511 added = []
512 lookup = []
512 lookup = []
513 dropped = []
513 dropped = []
514 mf = ctx.manifest()
514 mf = ctx.manifest()
515 files = set(mf)
515 files = set(mf)
516 mresult = mergemod.mergeresult()
516 mresult = mergemod.mergeresult()
517
517
518 for file in files:
518 for file in files:
519 old = origsparsematch(file)
519 old = origsparsematch(file)
520 new = sparsematch(file)
520 new = sparsematch(file)
521 # Add files that are newly included, or that don't exist in
521 # Add files that are newly included, or that don't exist in
522 # the dirstate yet.
522 # the dirstate yet.
523 if (new and not old) or (old and new and not file in dirstate):
523 if (new and not old) or (old and new and not file in dirstate):
524 fl = mf.flags(file)
524 fl = mf.flags(file)
525 if repo.wvfs.exists(file):
525 if repo.wvfs.exists(file):
526 mresult.addfile(file, mergestatemod.ACTION_EXEC, (fl,), b'')
526 mresult.addfile(file, mergestatemod.ACTION_EXEC, (fl,), b'')
527 lookup.append(file)
527 lookup.append(file)
528 else:
528 else:
529 mresult.addfile(
529 mresult.addfile(
530 file, mergestatemod.ACTION_GET, (fl, False), b''
530 file, mergestatemod.ACTION_GET, (fl, False), b''
531 )
531 )
532 added.append(file)
532 added.append(file)
533 # Drop files that are newly excluded, or that still exist in
533 # Drop files that are newly excluded, or that still exist in
534 # the dirstate.
534 # the dirstate.
535 elif (old and not new) or (not old and not new and file in dirstate):
535 elif (old and not new) or (not old and not new and file in dirstate):
536 dropped.append(file)
536 dropped.append(file)
537 if file not in pending:
537 if file not in pending:
538 mresult.addfile(file, mergestatemod.ACTION_REMOVE, [], b'')
538 mresult.addfile(file, mergestatemod.ACTION_REMOVE, [], b'')
539
539
540 # Verify there are no pending changes in newly included files
540 # Verify there are no pending changes in newly included files
541 abort = False
541 abort = False
542 for file in lookup:
542 for file in lookup:
543 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
543 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
544 abort = not force
544 abort = not force
545 if abort:
545 if abort:
546 raise error.Abort(
546 raise error.Abort(
547 _(
547 _(
548 b'cannot change sparseness due to pending '
548 b'cannot change sparseness due to pending '
549 b'changes (delete the files or use '
549 b'changes (delete the files or use '
550 b'--force to bring them back dirty)'
550 b'--force to bring them back dirty)'
551 )
551 )
552 )
552 )
553
553
554 # Check for files that were only in the dirstate.
554 # Check for files that were only in the dirstate.
555 for file, state in pycompat.iteritems(dirstate):
555 for file, state in pycompat.iteritems(dirstate):
556 if not file in files:
556 if not file in files:
557 old = origsparsematch(file)
557 old = origsparsematch(file)
558 new = sparsematch(file)
558 new = sparsematch(file)
559 if old and not new:
559 if old and not new:
560 dropped.append(file)
560 dropped.append(file)
561
561
562 mergemod.applyupdates(
562 mergemod.applyupdates(
563 repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False
563 repo, mresult, repo[None], repo[b'.'], False, wantfiledata=False
564 )
564 )
565
565
566 # Fix dirstate
566 # Fix dirstate
567 for file in added:
567 for file in added:
568 dirstate.normal(file)
568 dirstate.normal(file)
569
569
570 for file in dropped:
570 for file in dropped:
571 dirstate.drop(file)
571 dirstate.drop(file)
572
572
573 for file in lookup:
573 for file in lookup:
574 # File exists on disk, and we're bringing it back in an unknown state.
574 # File exists on disk, and we're bringing it back in an unknown state.
575 dirstate.normallookup(file)
575 dirstate.normallookup(file)
576
576
577 return added, dropped, lookup
577 return added, dropped, lookup
578
578
579
579
580 def aftercommit(repo, node):
580 def aftercommit(repo, node):
581 """Perform actions after a working directory commit."""
581 """Perform actions after a working directory commit."""
582 # This function is called unconditionally, even if sparse isn't
582 # This function is called unconditionally, even if sparse isn't
583 # enabled.
583 # enabled.
584 ctx = repo[node]
584 ctx = repo[node]
585
585
586 profiles = patternsforrev(repo, ctx.rev())[2]
586 profiles = patternsforrev(repo, ctx.rev())[2]
587
587
588 # profiles will only have data if sparse is enabled.
588 # profiles will only have data if sparse is enabled.
589 if profiles & set(ctx.files()):
589 if profiles & set(ctx.files()):
590 origstatus = repo.status()
590 origstatus = repo.status()
591 origsparsematch = matcher(repo)
591 origsparsematch = matcher(repo)
592 refreshwdir(repo, origstatus, origsparsematch, force=True)
592 refreshwdir(repo, origstatus, origsparsematch, force=True)
593
593
594 prunetemporaryincludes(repo)
594 prunetemporaryincludes(repo)
595
595
596
596
597 def _updateconfigandrefreshwdir(
597 def _updateconfigandrefreshwdir(
598 repo, includes, excludes, profiles, force=False, removing=False
598 repo, includes, excludes, profiles, force=False, removing=False
599 ):
599 ):
600 """Update the sparse config and working directory state."""
600 """Update the sparse config and working directory state."""
601 raw = repo.vfs.tryread(b'sparse')
601 raw = repo.vfs.tryread(b'sparse')
602 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
602 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
603
603
604 oldstatus = repo.status()
604 oldstatus = repo.status()
605 oldmatch = matcher(repo)
605 oldmatch = matcher(repo)
606 oldrequires = set(repo.requirements)
606 oldrequires = set(repo.requirements)
607
607
608 # TODO remove this try..except once the matcher integrates better
608 # TODO remove this try..except once the matcher integrates better
609 # with dirstate. We currently have to write the updated config
609 # with dirstate. We currently have to write the updated config
610 # because that will invalidate the matcher cache and force a
610 # because that will invalidate the matcher cache and force a
611 # re-read. We ideally want to update the cached matcher on the
611 # re-read. We ideally want to update the cached matcher on the
612 # repo instance then flush the new config to disk once wdir is
612 # repo instance then flush the new config to disk once wdir is
613 # updated. But this requires massive rework to matcher() and its
613 # updated. But this requires massive rework to matcher() and its
614 # consumers.
614 # consumers.
615
615
616 if requirements.SPARSE_REQUIREMENT in oldrequires and removing:
616 if requirements.SPARSE_REQUIREMENT in oldrequires and removing:
617 repo.requirements.discard(requirements.SPARSE_REQUIREMENT)
617 repo.requirements.discard(requirements.SPARSE_REQUIREMENT)
618 scmutil.writereporequirements(repo)
618 scmutil.writereporequirements(repo)
619 elif requirements.SPARSE_REQUIREMENT not in oldrequires:
619 elif requirements.SPARSE_REQUIREMENT not in oldrequires:
620 repo.requirements.add(requirements.SPARSE_REQUIREMENT)
620 repo.requirements.add(requirements.SPARSE_REQUIREMENT)
621 scmutil.writereporequirements(repo)
621 scmutil.writereporequirements(repo)
622
622
623 try:
623 try:
624 writeconfig(repo, includes, excludes, profiles)
624 writeconfig(repo, includes, excludes, profiles)
625 return refreshwdir(repo, oldstatus, oldmatch, force=force)
625 return refreshwdir(repo, oldstatus, oldmatch, force=force)
626 except Exception:
626 except Exception:
627 if repo.requirements != oldrequires:
627 if repo.requirements != oldrequires:
628 repo.requirements.clear()
628 repo.requirements.clear()
629 repo.requirements |= oldrequires
629 repo.requirements |= oldrequires
630 scmutil.writereporequirements(repo)
630 scmutil.writereporequirements(repo)
631 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
631 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
632 raise
632 raise
633
633
634
634
635 def clearrules(repo, force=False):
635 def clearrules(repo, force=False):
636 """Clears include/exclude rules from the sparse config.
636 """Clears include/exclude rules from the sparse config.
637
637
638 The remaining sparse config only has profiles, if defined. The working
638 The remaining sparse config only has profiles, if defined. The working
639 directory is refreshed, as needed.
639 directory is refreshed, as needed.
640 """
640 """
641 with repo.wlock(), repo.dirstate.parentchange():
641 with repo.wlock(), repo.dirstate.parentchange():
642 raw = repo.vfs.tryread(b'sparse')
642 raw = repo.vfs.tryread(b'sparse')
643 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
643 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
644
644
645 if not includes and not excludes:
645 if not includes and not excludes:
646 return
646 return
647
647
648 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
648 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
649
649
650
650
651 def importfromfiles(repo, opts, paths, force=False):
651 def importfromfiles(repo, opts, paths, force=False):
652 """Import sparse config rules from files.
652 """Import sparse config rules from files.
653
653
654 The updated sparse config is written out and the working directory
654 The updated sparse config is written out and the working directory
655 is refreshed, as needed.
655 is refreshed, as needed.
656 """
656 """
657 with repo.wlock(), repo.dirstate.parentchange():
657 with repo.wlock(), repo.dirstate.parentchange():
658 # read current configuration
658 # read current configuration
659 raw = repo.vfs.tryread(b'sparse')
659 raw = repo.vfs.tryread(b'sparse')
660 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
660 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
661 aincludes, aexcludes, aprofiles = activeconfig(repo)
661 aincludes, aexcludes, aprofiles = activeconfig(repo)
662
662
663 # Import rules on top; only take in rules that are not yet
663 # Import rules on top; only take in rules that are not yet
664 # part of the active rules.
664 # part of the active rules.
665 changed = False
665 changed = False
666 for p in paths:
666 for p in paths:
667 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
667 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
668 raw = fh.read()
668 raw = fh.read()
669
669
670 iincludes, iexcludes, iprofiles = parseconfig(
670 iincludes, iexcludes, iprofiles = parseconfig(
671 repo.ui, raw, b'sparse'
671 repo.ui, raw, b'sparse'
672 )
672 )
673 oldsize = len(includes) + len(excludes) + len(profiles)
673 oldsize = len(includes) + len(excludes) + len(profiles)
674 includes.update(iincludes - aincludes)
674 includes.update(iincludes - aincludes)
675 excludes.update(iexcludes - aexcludes)
675 excludes.update(iexcludes - aexcludes)
676 profiles.update(iprofiles - aprofiles)
676 profiles.update(iprofiles - aprofiles)
677 if len(includes) + len(excludes) + len(profiles) > oldsize:
677 if len(includes) + len(excludes) + len(profiles) > oldsize:
678 changed = True
678 changed = True
679
679
680 profilecount = includecount = excludecount = 0
680 profilecount = includecount = excludecount = 0
681 fcounts = (0, 0, 0)
681 fcounts = (0, 0, 0)
682
682
683 if changed:
683 if changed:
684 profilecount = len(profiles - aprofiles)
684 profilecount = len(profiles - aprofiles)
685 includecount = len(includes - aincludes)
685 includecount = len(includes - aincludes)
686 excludecount = len(excludes - aexcludes)
686 excludecount = len(excludes - aexcludes)
687
687
688 fcounts = map(
688 fcounts = map(
689 len,
689 len,
690 _updateconfigandrefreshwdir(
690 _updateconfigandrefreshwdir(
691 repo, includes, excludes, profiles, force=force
691 repo, includes, excludes, profiles, force=force
692 ),
692 ),
693 )
693 )
694
694
695 printchanges(
695 printchanges(
696 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
696 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
697 )
697 )
698
698
699
699
700 def updateconfig(
700 def updateconfig(
701 repo,
701 repo,
702 pats,
702 pats,
703 opts,
703 opts,
704 include=False,
704 include=False,
705 exclude=False,
705 exclude=False,
706 reset=False,
706 reset=False,
707 delete=False,
707 delete=False,
708 enableprofile=False,
708 enableprofile=False,
709 disableprofile=False,
709 disableprofile=False,
710 force=False,
710 force=False,
711 usereporootpaths=False,
711 usereporootpaths=False,
712 ):
712 ):
713 """Perform a sparse config update.
713 """Perform a sparse config update.
714
714
715 Only one of the actions may be performed.
715 Only one of the actions may be performed.
716
716
717 The new config is written out and a working directory refresh is performed.
717 The new config is written out and a working directory refresh is performed.
718 """
718 """
719 with repo.wlock(), repo.dirstate.parentchange():
719 with repo.wlock(), repo.dirstate.parentchange():
720 raw = repo.vfs.tryread(b'sparse')
720 raw = repo.vfs.tryread(b'sparse')
721 oldinclude, oldexclude, oldprofiles = parseconfig(
721 oldinclude, oldexclude, oldprofiles = parseconfig(
722 repo.ui, raw, b'sparse'
722 repo.ui, raw, b'sparse'
723 )
723 )
724
724
725 if reset:
725 if reset:
726 newinclude = set()
726 newinclude = set()
727 newexclude = set()
727 newexclude = set()
728 newprofiles = set()
728 newprofiles = set()
729 else:
729 else:
730 newinclude = set(oldinclude)
730 newinclude = set(oldinclude)
731 newexclude = set(oldexclude)
731 newexclude = set(oldexclude)
732 newprofiles = set(oldprofiles)
732 newprofiles = set(oldprofiles)
733
733
734 if any(os.path.isabs(pat) for pat in pats):
734 if any(os.path.isabs(pat) for pat in pats):
735 raise error.Abort(_(b'paths cannot be absolute'))
735 raise error.Abort(_(b'paths cannot be absolute'))
736
736
737 if not usereporootpaths:
737 if not usereporootpaths:
738 # let's treat paths as relative to cwd
738 # let's treat paths as relative to cwd
739 root, cwd = repo.root, repo.getcwd()
739 root, cwd = repo.root, repo.getcwd()
740 abspats = []
740 abspats = []
741 for kindpat in pats:
741 for kindpat in pats:
742 kind, pat = matchmod._patsplit(kindpat, None)
742 kind, pat = matchmod._patsplit(kindpat, None)
743 if kind in matchmod.cwdrelativepatternkinds or kind is None:
743 if kind in matchmod.cwdrelativepatternkinds or kind is None:
744 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
744 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
745 root, cwd, pat
745 root, cwd, pat
746 )
746 )
747 abspats.append(ap)
747 abspats.append(ap)
748 else:
748 else:
749 abspats.append(kindpat)
749 abspats.append(kindpat)
750 pats = abspats
750 pats = abspats
751
751
752 if include:
752 if include:
753 newinclude.update(pats)
753 newinclude.update(pats)
754 elif exclude:
754 elif exclude:
755 newexclude.update(pats)
755 newexclude.update(pats)
756 elif enableprofile:
756 elif enableprofile:
757 newprofiles.update(pats)
757 newprofiles.update(pats)
758 elif disableprofile:
758 elif disableprofile:
759 newprofiles.difference_update(pats)
759 newprofiles.difference_update(pats)
760 elif delete:
760 elif delete:
761 newinclude.difference_update(pats)
761 newinclude.difference_update(pats)
762 newexclude.difference_update(pats)
762 newexclude.difference_update(pats)
763
763
764 profilecount = len(newprofiles - oldprofiles) - len(
764 profilecount = len(newprofiles - oldprofiles) - len(
765 oldprofiles - newprofiles
765 oldprofiles - newprofiles
766 )
766 )
767 includecount = len(newinclude - oldinclude) - len(
767 includecount = len(newinclude - oldinclude) - len(
768 oldinclude - newinclude
768 oldinclude - newinclude
769 )
769 )
770 excludecount = len(newexclude - oldexclude) - len(
770 excludecount = len(newexclude - oldexclude) - len(
771 oldexclude - newexclude
771 oldexclude - newexclude
772 )
772 )
773
773
774 fcounts = map(
774 fcounts = map(
775 len,
775 len,
776 _updateconfigandrefreshwdir(
776 _updateconfigandrefreshwdir(
777 repo,
777 repo,
778 newinclude,
778 newinclude,
779 newexclude,
779 newexclude,
780 newprofiles,
780 newprofiles,
781 force=force,
781 force=force,
782 removing=reset,
782 removing=reset,
783 ),
783 ),
784 )
784 )
785
785
786 printchanges(
786 printchanges(
787 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
787 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
788 )
788 )
789
789
790
790
791 def printchanges(
791 def printchanges(
792 ui,
792 ui,
793 opts,
793 opts,
794 profilecount=0,
794 profilecount=0,
795 includecount=0,
795 includecount=0,
796 excludecount=0,
796 excludecount=0,
797 added=0,
797 added=0,
798 dropped=0,
798 dropped=0,
799 conflicting=0,
799 conflicting=0,
800 ):
800 ):
801 """Print output summarizing sparse config changes."""
801 """Print output summarizing sparse config changes."""
802 with ui.formatter(b'sparse', opts) as fm:
802 with ui.formatter(b'sparse', opts) as fm:
803 fm.startitem()
803 fm.startitem()
804 fm.condwrite(
804 fm.condwrite(
805 ui.verbose,
805 ui.verbose,
806 b'profiles_added',
806 b'profiles_added',
807 _(b'Profiles changed: %d\n'),
807 _(b'Profiles changed: %d\n'),
808 profilecount,
808 profilecount,
809 )
809 )
810 fm.condwrite(
810 fm.condwrite(
811 ui.verbose,
811 ui.verbose,
812 b'include_rules_added',
812 b'include_rules_added',
813 _(b'Include rules changed: %d\n'),
813 _(b'Include rules changed: %d\n'),
814 includecount,
814 includecount,
815 )
815 )
816 fm.condwrite(
816 fm.condwrite(
817 ui.verbose,
817 ui.verbose,
818 b'exclude_rules_added',
818 b'exclude_rules_added',
819 _(b'Exclude rules changed: %d\n'),
819 _(b'Exclude rules changed: %d\n'),
820 excludecount,
820 excludecount,
821 )
821 )
822
822
823 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
823 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
824 # files are added or removed outside of the templating formatter
824 # files are added or removed outside of the templating formatter
825 # framework. No point in repeating ourselves in that case.
825 # framework. No point in repeating ourselves in that case.
826 if not fm.isplain():
826 if not fm.isplain():
827 fm.condwrite(
827 fm.condwrite(
828 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
828 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
829 )
829 )
830 fm.condwrite(
830 fm.condwrite(
831 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
831 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
832 )
832 )
833 fm.condwrite(
833 fm.condwrite(
834 ui.verbose,
834 ui.verbose,
835 b'files_conflicting',
835 b'files_conflicting',
836 _(b'Files conflicting: %d\n'),
836 _(b'Files conflicting: %d\n'),
837 conflicting,
837 conflicting,
838 )
838 )
General Comments 0
You need to be logged in to leave comments. Login now