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