##// END OF EJS Templates
sparse: move code for clearing rules to core...
Gregory Szorc -
r33354:4695f182 default
parent child Browse files
Show More
@@ -1,542 +1,531 b''
1 1 # sparse.py - allow sparse checkouts of the working directory
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 """allow sparse checkouts of the working directory (EXPERIMENTAL)
9 9
10 10 (This extension is not yet protected by backwards compatibility
11 11 guarantees. Any aspect may break in future releases until this
12 12 notice is removed.)
13 13
14 14 This extension allows the working directory to only consist of a
15 15 subset of files for the revision. This allows specific files or
16 16 directories to be explicitly included or excluded. Many repository
17 17 operations have performance proportional to the number of files in
18 18 the working directory. So only realizing a subset of files in the
19 19 working directory can improve performance.
20 20
21 21 Sparse Config Files
22 22 -------------------
23 23
24 24 The set of files that are part of a sparse checkout are defined by
25 25 a sparse config file. The file defines 3 things: includes (files to
26 26 include in the sparse checkout), excludes (files to exclude from the
27 27 sparse checkout), and profiles (links to other config files).
28 28
29 29 The file format is newline delimited. Empty lines and lines beginning
30 30 with ``#`` are ignored.
31 31
32 32 Lines beginning with ``%include `` denote another sparse config file
33 33 to include. e.g. ``%include tests.sparse``. The filename is relative
34 34 to the repository root.
35 35
36 36 The special lines ``[include]`` and ``[exclude]`` denote the section
37 37 for includes and excludes that follow, respectively. It is illegal to
38 38 have ``[include]`` after ``[exclude]``. If no sections are defined,
39 39 entries are assumed to be in the ``[include]`` section.
40 40
41 41 Non-special lines resemble file patterns to be added to either includes
42 42 or excludes. The syntax of these lines is documented by :hg:`help patterns`.
43 43 Patterns are interpreted as ``glob:`` by default and match against the
44 44 root of the repository.
45 45
46 46 Exclusion patterns take precedence over inclusion patterns. So even
47 47 if a file is explicitly included, an ``[exclude]`` entry can remove it.
48 48
49 49 For example, say you have a repository with 3 directories, ``frontend/``,
50 50 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
51 51 to different projects and it is uncommon for someone working on one
52 52 to need the files for the other. But ``tools/`` contains files shared
53 53 between both projects. Your sparse config files may resemble::
54 54
55 55 # frontend.sparse
56 56 frontend/**
57 57 tools/**
58 58
59 59 # backend.sparse
60 60 backend/**
61 61 tools/**
62 62
63 63 Say the backend grows in size. Or there's a directory with thousands
64 64 of files you wish to exclude. You can modify the profile to exclude
65 65 certain files::
66 66
67 67 [include]
68 68 backend/**
69 69 tools/**
70 70
71 71 [exclude]
72 72 tools/tests/**
73 73 """
74 74
75 75 from __future__ import absolute_import
76 76
77 77 from mercurial.i18n import _
78 78 from mercurial.node import nullid
79 79 from mercurial import (
80 80 cmdutil,
81 81 commands,
82 82 dirstate,
83 83 error,
84 84 extensions,
85 85 hg,
86 86 localrepo,
87 87 match as matchmod,
88 88 registrar,
89 89 sparse,
90 90 util,
91 91 )
92 92
93 93 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
94 94 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
95 95 # be specifying the version(s) of Mercurial they are tested with, or
96 96 # leave the attribute unspecified.
97 97 testedwith = 'ships-with-hg-core'
98 98
99 99 cmdtable = {}
100 100 command = registrar.command(cmdtable)
101 101
102 102 def extsetup(ui):
103 103 sparse.enabled = True
104 104
105 105 _setupclone(ui)
106 106 _setuplog(ui)
107 107 _setupadd(ui)
108 108 _setupdirstate(ui)
109 109
110 110 def reposetup(ui, repo):
111 111 if not util.safehasattr(repo, 'dirstate'):
112 112 return
113 113
114 114 if 'dirstate' in repo._filecache:
115 115 repo.dirstate.repo = repo
116 116
117 117 def replacefilecache(cls, propname, replacement):
118 118 """Replace a filecache property with a new class. This allows changing the
119 119 cache invalidation condition."""
120 120 origcls = cls
121 121 assert callable(replacement)
122 122 while cls is not object:
123 123 if propname in cls.__dict__:
124 124 orig = cls.__dict__[propname]
125 125 setattr(cls, propname, replacement(orig))
126 126 break
127 127 cls = cls.__bases__[0]
128 128
129 129 if cls is object:
130 130 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
131 131 propname))
132 132
133 133 def _setuplog(ui):
134 134 entry = commands.table['^log|history']
135 135 entry[1].append(('', 'sparse', None,
136 136 "limit to changesets affecting the sparse checkout"))
137 137
138 138 def _logrevs(orig, repo, opts):
139 139 revs = orig(repo, opts)
140 140 if opts.get('sparse'):
141 141 sparsematch = sparse.matcher(repo)
142 142 def ctxmatch(rev):
143 143 ctx = repo[rev]
144 144 return any(f for f in ctx.files() if sparsematch(f))
145 145 revs = revs.filter(ctxmatch)
146 146 return revs
147 147 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
148 148
149 149 def _clonesparsecmd(orig, ui, repo, *args, **opts):
150 150 include_pat = opts.get('include')
151 151 exclude_pat = opts.get('exclude')
152 152 enableprofile_pat = opts.get('enable_profile')
153 153 include = exclude = enableprofile = False
154 154 if include_pat:
155 155 pat = include_pat
156 156 include = True
157 157 if exclude_pat:
158 158 pat = exclude_pat
159 159 exclude = True
160 160 if enableprofile_pat:
161 161 pat = enableprofile_pat
162 162 enableprofile = True
163 163 if sum([include, exclude, enableprofile]) > 1:
164 164 raise error.Abort(_("too many flags specified."))
165 165 if include or exclude or enableprofile:
166 166 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
167 167 _config(self.ui, self.unfiltered(), pat, {}, include=include,
168 168 exclude=exclude, enableprofile=enableprofile)
169 169 return orig(self, node, overwrite, *args, **kwargs)
170 170 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
171 171 return orig(ui, repo, *args, **opts)
172 172
173 173 def _setupclone(ui):
174 174 entry = commands.table['^clone']
175 175 entry[1].append(('', 'enable-profile', [],
176 176 'enable a sparse profile'))
177 177 entry[1].append(('', 'include', [],
178 178 'include sparse pattern'))
179 179 entry[1].append(('', 'exclude', [],
180 180 'exclude sparse pattern'))
181 181 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
182 182
183 183 def _setupadd(ui):
184 184 entry = commands.table['^add']
185 185 entry[1].append(('s', 'sparse', None,
186 186 'also include directories of added files in sparse config'))
187 187
188 188 def _add(orig, ui, repo, *pats, **opts):
189 189 if opts.get('sparse'):
190 190 dirs = set()
191 191 for pat in pats:
192 192 dirname, basename = util.split(pat)
193 193 dirs.add(dirname)
194 194 _config(ui, repo, list(dirs), opts, include=True)
195 195 return orig(ui, repo, *pats, **opts)
196 196
197 197 extensions.wrapcommand(commands.table, 'add', _add)
198 198
199 199 def _setupdirstate(ui):
200 200 """Modify the dirstate to prevent stat'ing excluded files,
201 201 and to prevent modifications to files outside the checkout.
202 202 """
203 203
204 204 def _dirstate(orig, repo):
205 205 dirstate = orig(repo)
206 206 dirstate.repo = repo
207 207 return dirstate
208 208 extensions.wrapfunction(
209 209 localrepo.localrepository.dirstate, 'func', _dirstate)
210 210
211 211 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
212 212 # property, which means normal function wrapping doesn't work.
213 213 class ignorewrapper(object):
214 214 def __init__(self, orig):
215 215 self.orig = orig
216 216 self.origignore = None
217 217 self.func = None
218 218 self.sparsematch = None
219 219
220 220 def __get__(self, obj, type=None):
221 221 repo = obj.repo
222 222 origignore = self.orig.__get__(obj)
223 223
224 224 sparsematch = sparse.matcher(repo)
225 225 if sparsematch.always():
226 226 return origignore
227 227
228 228 if self.sparsematch != sparsematch or self.origignore != origignore:
229 229 self.func = matchmod.unionmatcher([
230 230 origignore, matchmod.negatematcher(sparsematch)])
231 231 self.sparsematch = sparsematch
232 232 self.origignore = origignore
233 233 return self.func
234 234
235 235 def __set__(self, obj, value):
236 236 return self.orig.__set__(obj, value)
237 237
238 238 def __delete__(self, obj):
239 239 return self.orig.__delete__(obj)
240 240
241 241 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
242 242
243 243 # dirstate.rebuild should not add non-matching files
244 244 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
245 245 matcher = sparse.matcher(self.repo)
246 246 if not matcher.always():
247 247 allfiles = allfiles.matches(matcher)
248 248 if changedfiles:
249 249 changedfiles = [f for f in changedfiles if matcher(f)]
250 250
251 251 if changedfiles is not None:
252 252 # In _rebuild, these files will be deleted from the dirstate
253 253 # when they are not found to be in allfiles
254 254 dirstatefilestoremove = set(f for f in self if not matcher(f))
255 255 changedfiles = dirstatefilestoremove.union(changedfiles)
256 256
257 257 return orig(self, parent, allfiles, changedfiles)
258 258 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
259 259
260 260 # Prevent adding files that are outside the sparse checkout
261 261 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
262 262 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
263 263 '`hg add -s <file>` to include file directory while adding')
264 264 for func in editfuncs:
265 265 def _wrapper(orig, self, *args):
266 266 repo = self.repo
267 267 sparsematch = sparse.matcher(repo)
268 268 if not sparsematch.always():
269 269 dirstate = repo.dirstate
270 270 for f in args:
271 271 if (f is not None and not sparsematch(f) and
272 272 f not in dirstate):
273 273 raise error.Abort(_("cannot add '%s' - it is outside "
274 274 "the sparse checkout") % f,
275 275 hint=hint)
276 276 return orig(self, *args)
277 277 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
278 278
279 279 @command('^debugsparse', [
280 280 ('I', 'include', False, _('include files in the sparse checkout')),
281 281 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
282 282 ('d', 'delete', False, _('delete an include/exclude rule')),
283 283 ('f', 'force', False, _('allow changing rules even with pending changes')),
284 284 ('', 'enable-profile', False, _('enables the specified profile')),
285 285 ('', 'disable-profile', False, _('disables the specified profile')),
286 286 ('', 'import-rules', False, _('imports rules from a file')),
287 287 ('', 'clear-rules', False, _('clears local include/exclude rules')),
288 288 ('', 'refresh', False, _('updates the working after sparseness changes')),
289 289 ('', 'reset', False, _('makes the repo full again')),
290 290 ] + commands.templateopts,
291 291 _('[--OPTION] PATTERN...'))
292 292 def debugsparse(ui, repo, *pats, **opts):
293 293 """make the current checkout sparse, or edit the existing checkout
294 294
295 295 The sparse command is used to make the current checkout sparse.
296 296 This means files that don't meet the sparse condition will not be
297 297 written to disk, or show up in any working copy operations. It does
298 298 not affect files in history in any way.
299 299
300 300 Passing no arguments prints the currently applied sparse rules.
301 301
302 302 --include and --exclude are used to add and remove files from the sparse
303 303 checkout. The effects of adding an include or exclude rule are applied
304 304 immediately. If applying the new rule would cause a file with pending
305 305 changes to be added or removed, the command will fail. Pass --force to
306 306 force a rule change even with pending changes (the changes on disk will
307 307 be preserved).
308 308
309 309 --delete removes an existing include/exclude rule. The effects are
310 310 immediate.
311 311
312 312 --refresh refreshes the files on disk based on the sparse rules. This is
313 313 only necessary if .hg/sparse was changed by hand.
314 314
315 315 --enable-profile and --disable-profile accept a path to a .hgsparse file.
316 316 This allows defining sparse checkouts and tracking them inside the
317 317 repository. This is useful for defining commonly used sparse checkouts for
318 318 many people to use. As the profile definition changes over time, the sparse
319 319 checkout will automatically be updated appropriately, depending on which
320 320 changeset is checked out. Changes to .hgsparse are not applied until they
321 321 have been committed.
322 322
323 323 --import-rules accepts a path to a file containing rules in the .hgsparse
324 324 format, allowing you to add --include, --exclude and --enable-profile rules
325 325 in bulk. Like the --include, --exclude and --enable-profile switches, the
326 326 changes are applied immediately.
327 327
328 328 --clear-rules removes all local include and exclude rules, while leaving
329 329 any enabled profiles in place.
330 330
331 331 Returns 0 if editing the sparse checkout succeeds.
332 332 """
333 333 include = opts.get('include')
334 334 exclude = opts.get('exclude')
335 335 force = opts.get('force')
336 336 enableprofile = opts.get('enable_profile')
337 337 disableprofile = opts.get('disable_profile')
338 338 importrules = opts.get('import_rules')
339 339 clearrules = opts.get('clear_rules')
340 340 delete = opts.get('delete')
341 341 refresh = opts.get('refresh')
342 342 reset = opts.get('reset')
343 343 count = sum([include, exclude, enableprofile, disableprofile, delete,
344 344 importrules, refresh, clearrules, reset])
345 345 if count > 1:
346 346 raise error.Abort(_("too many flags specified"))
347 347
348 348 if count == 0:
349 349 if repo.vfs.exists('sparse'):
350 350 ui.status(repo.vfs.read("sparse") + "\n")
351 351 temporaryincludes = sparse.readtemporaryincludes(repo)
352 352 if temporaryincludes:
353 353 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
354 354 ui.status(("\n".join(temporaryincludes) + "\n"))
355 355 else:
356 356 ui.status(_('repo is not sparse\n'))
357 357 return
358 358
359 359 if include or exclude or delete or reset or enableprofile or disableprofile:
360 360 _config(ui, repo, pats, opts, include=include, exclude=exclude,
361 361 reset=reset, delete=delete, enableprofile=enableprofile,
362 362 disableprofile=disableprofile, force=force)
363 363
364 364 if importrules:
365 365 _import(ui, repo, pats, opts, force=force)
366 366
367 367 if clearrules:
368 _clear(ui, repo, pats, force=force)
368 sparse.clearrules(repo, force=force)
369 369
370 370 if refresh:
371 371 try:
372 372 wlock = repo.wlock()
373 373 fcounts = map(
374 374 len,
375 375 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
376 376 force=force))
377 377 _verbose_output(ui, opts, 0, 0, 0, *fcounts)
378 378 finally:
379 379 wlock.release()
380 380
381 381 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
382 382 delete=False, enableprofile=False, disableprofile=False,
383 383 force=False):
384 384 """
385 385 Perform a sparse config update. Only one of the kwargs may be specified.
386 386 """
387 387 wlock = repo.wlock()
388 388 try:
389 389 oldsparsematch = sparse.matcher(repo)
390 390
391 391 raw = repo.vfs.tryread('sparse')
392 392 if raw:
393 393 oldinclude, oldexclude, oldprofiles = map(
394 394 set, sparse.parseconfig(ui, raw))
395 395 else:
396 396 oldinclude = set()
397 397 oldexclude = set()
398 398 oldprofiles = set()
399 399
400 400 try:
401 401 if reset:
402 402 newinclude = set()
403 403 newexclude = set()
404 404 newprofiles = set()
405 405 else:
406 406 newinclude = set(oldinclude)
407 407 newexclude = set(oldexclude)
408 408 newprofiles = set(oldprofiles)
409 409
410 410 oldstatus = repo.status()
411 411
412 412 if any(pat.startswith('/') for pat in pats):
413 413 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
414 414 % ([pat for pat in pats if pat.startswith('/')]))
415 415 elif include:
416 416 newinclude.update(pats)
417 417 elif exclude:
418 418 newexclude.update(pats)
419 419 elif enableprofile:
420 420 newprofiles.update(pats)
421 421 elif disableprofile:
422 422 newprofiles.difference_update(pats)
423 423 elif delete:
424 424 newinclude.difference_update(pats)
425 425 newexclude.difference_update(pats)
426 426
427 427 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
428 428
429 429 fcounts = map(
430 430 len,
431 431 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
432 432 force=force))
433 433
434 434 profilecount = (len(newprofiles - oldprofiles) -
435 435 len(oldprofiles - newprofiles))
436 436 includecount = (len(newinclude - oldinclude) -
437 437 len(oldinclude - newinclude))
438 438 excludecount = (len(newexclude - oldexclude) -
439 439 len(oldexclude - newexclude))
440 440 _verbose_output(
441 441 ui, opts, profilecount, includecount, excludecount, *fcounts)
442 442 except Exception:
443 443 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
444 444 raise
445 445 finally:
446 446 wlock.release()
447 447
448 448 def _import(ui, repo, files, opts, force=False):
449 449 with repo.wlock():
450 450 # load union of current active profile
451 451 revs = [repo.changelog.rev(node) for node in
452 452 repo.dirstate.parents() if node != nullid]
453 453
454 454 # read current configuration
455 455 raw = repo.vfs.tryread('sparse')
456 456 oincludes, oexcludes, oprofiles = sparse.parseconfig(ui, raw)
457 457 includes, excludes, profiles = map(
458 458 set, (oincludes, oexcludes, oprofiles))
459 459
460 460 # all active rules
461 461 aincludes, aexcludes, aprofiles = set(), set(), set()
462 462 for rev in revs:
463 463 rincludes, rexcludes, rprofiles = sparse.patternsforrev(repo, rev)
464 464 aincludes.update(rincludes)
465 465 aexcludes.update(rexcludes)
466 466 aprofiles.update(rprofiles)
467 467
468 468 # import rules on top; only take in rules that are not yet
469 469 # part of the active rules.
470 470 changed = False
471 471 for file in files:
472 472 with util.posixfile(util.expandpath(file)) as importfile:
473 473 iincludes, iexcludes, iprofiles = sparse.parseconfig(
474 474 ui, importfile.read())
475 475 oldsize = len(includes) + len(excludes) + len(profiles)
476 476 includes.update(iincludes - aincludes)
477 477 excludes.update(iexcludes - aexcludes)
478 478 profiles.update(set(iprofiles) - aprofiles)
479 479 if len(includes) + len(excludes) + len(profiles) > oldsize:
480 480 changed = True
481 481
482 482 profilecount = includecount = excludecount = 0
483 483 fcounts = (0, 0, 0)
484 484
485 485 if changed:
486 486 profilecount = len(profiles - aprofiles)
487 487 includecount = len(includes - aincludes)
488 488 excludecount = len(excludes - aexcludes)
489 489
490 490 oldstatus = repo.status()
491 491 oldsparsematch = sparse.matcher(repo)
492 492 sparse.writeconfig(repo, includes, excludes, profiles)
493 493
494 494 try:
495 495 fcounts = map(
496 496 len,
497 497 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
498 498 force=force))
499 499 except Exception:
500 500 sparse.writeconfig(repo, oincludes, oexcludes, oprofiles)
501 501 raise
502 502
503 503 _verbose_output(ui, opts, profilecount, includecount, excludecount,
504 504 *fcounts)
505 505
506 def _clear(ui, repo, files, force=False):
507 with repo.wlock():
508 raw = repo.vfs.tryread('sparse')
509 includes, excludes, profiles = sparse.parseconfig(ui, raw)
510
511 if includes or excludes:
512 oldstatus = repo.status()
513 oldsparsematch = sparse.matcher(repo)
514 sparse.writeconfig(repo, set(), set(), profiles)
515 sparse.refreshwdir(repo, oldstatus, oldsparsematch, force)
516
517 506 def _verbose_output(ui, opts, profilecount, includecount, excludecount, added,
518 507 dropped, lookup):
519 508 """Produce --verbose and templatable output
520 509
521 510 This specifically enables -Tjson, providing machine-readable stats on how
522 511 the sparse profile changed.
523 512
524 513 """
525 514 with ui.formatter('sparse', opts) as fm:
526 515 fm.startitem()
527 516 fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n',
528 517 profilecount)
529 518 fm.condwrite(ui.verbose, 'include_rules_added',
530 519 'Include rule # change: %d\n', includecount)
531 520 fm.condwrite(ui.verbose, 'exclude_rules_added',
532 521 'Exclude rule # change: %d\n', excludecount)
533 522 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
534 523 # files are added or removed outside of the templating formatter
535 524 # framework. No point in repeating ourselves in that case.
536 525 if not fm.isplain():
537 526 fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n',
538 527 added)
539 528 fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n',
540 529 dropped)
541 530 fm.condwrite(ui.verbose, 'files_conflicting',
542 531 'Files conflicting: %d\n', lookup)
@@ -1,496 +1,514 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 )
22 22
23 23 # Whether sparse features are enabled. This variable is intended to be
24 24 # temporary to facilitate porting sparse to core. It should eventually be
25 25 # a per-repo option, possibly a repo requirement.
26 26 enabled = False
27 27
28 28 def parseconfig(ui, raw):
29 29 """Parse sparse config file content.
30 30
31 31 Returns a tuple of includes, excludes, and profiles.
32 32 """
33 33 includes = set()
34 34 excludes = set()
35 35 current = includes
36 36 profiles = []
37 37 for line in raw.split('\n'):
38 38 line = line.strip()
39 39 if not line or line.startswith('#'):
40 40 # empty or comment line, skip
41 41 continue
42 42 elif line.startswith('%include '):
43 43 line = line[9:].strip()
44 44 if line:
45 45 profiles.append(line)
46 46 elif line == '[include]':
47 47 if current != includes:
48 48 # TODO pass filename into this API so we can report it.
49 49 raise error.Abort(_('sparse config cannot have includes ' +
50 50 'after excludes'))
51 51 continue
52 52 elif line == '[exclude]':
53 53 current = excludes
54 54 elif line:
55 55 if line.strip().startswith('/'):
56 56 ui.warn(_('warning: sparse profile cannot use' +
57 57 ' paths starting with /, ignoring %s\n') % line)
58 58 continue
59 59 current.add(line)
60 60
61 61 return includes, excludes, profiles
62 62
63 63 # Exists as separate function to facilitate monkeypatching.
64 64 def readprofile(repo, profile, changeid):
65 65 """Resolve the raw content of a sparse profile file."""
66 66 # TODO add some kind of cache here because this incurs a manifest
67 67 # resolve and can be slow.
68 68 return repo.filectx(profile, changeid=changeid).data()
69 69
70 70 def patternsforrev(repo, rev):
71 71 """Obtain sparse checkout patterns for the given rev.
72 72
73 73 Returns a tuple of iterables representing includes, excludes, and
74 74 patterns.
75 75 """
76 76 # Feature isn't enabled. No-op.
77 77 if not enabled:
78 78 return set(), set(), []
79 79
80 80 raw = repo.vfs.tryread('sparse')
81 81 if not raw:
82 82 return set(), set(), []
83 83
84 84 if rev is None:
85 85 raise error.Abort(_('cannot parse sparse patterns from working '
86 86 'directory'))
87 87
88 88 includes, excludes, profiles = parseconfig(repo.ui, raw)
89 89 ctx = repo[rev]
90 90
91 91 if profiles:
92 92 visited = set()
93 93 while profiles:
94 94 profile = profiles.pop()
95 95 if profile in visited:
96 96 continue
97 97
98 98 visited.add(profile)
99 99
100 100 try:
101 101 raw = readprofile(repo, profile, rev)
102 102 except error.ManifestLookupError:
103 103 msg = (
104 104 "warning: sparse profile '%s' not found "
105 105 "in rev %s - ignoring it\n" % (profile, ctx))
106 106 # experimental config: sparse.missingwarning
107 107 if repo.ui.configbool(
108 108 'sparse', 'missingwarning', True):
109 109 repo.ui.warn(msg)
110 110 else:
111 111 repo.ui.debug(msg)
112 112 continue
113 113
114 114 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
115 115 includes.update(pincludes)
116 116 excludes.update(pexcludes)
117 117 for subprofile in subprofs:
118 118 profiles.append(subprofile)
119 119
120 120 profiles = visited
121 121
122 122 if includes:
123 123 includes.add('.hg*')
124 124
125 125 return includes, excludes, profiles
126 126
127 127 def activeprofiles(repo):
128 128 revs = [repo.changelog.rev(node) for node in
129 129 repo.dirstate.parents() if node != nullid]
130 130
131 131 profiles = set()
132 132 for rev in revs:
133 133 profiles.update(patternsforrev(repo, rev)[2])
134 134
135 135 return profiles
136 136
137 137 def configsignature(repo, includetemp=True):
138 138 """Obtain the signature string for the current sparse configuration.
139 139
140 140 This is used to construct a cache key for matchers.
141 141 """
142 142 cache = repo._sparsesignaturecache
143 143
144 144 signature = cache.get('signature')
145 145
146 146 if includetemp:
147 147 tempsignature = cache.get('tempsignature')
148 148 else:
149 149 tempsignature = '0'
150 150
151 151 if signature is None or (includetemp and tempsignature is None):
152 152 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
153 153 cache['signature'] = signature
154 154
155 155 if includetemp:
156 156 raw = repo.vfs.tryread('tempsparse')
157 157 tempsignature = hashlib.sha1(raw).hexdigest()
158 158 cache['tempsignature'] = tempsignature
159 159
160 160 return '%s %s' % (signature, tempsignature)
161 161
162 162 def writeconfig(repo, includes, excludes, profiles):
163 163 """Write the sparse config file given a sparse configuration."""
164 164 with repo.vfs('sparse', 'wb') as fh:
165 165 for p in sorted(profiles):
166 166 fh.write('%%include %s\n' % p)
167 167
168 168 if includes:
169 169 fh.write('[include]\n')
170 170 for i in sorted(includes):
171 171 fh.write(i)
172 172 fh.write('\n')
173 173
174 174 if excludes:
175 175 fh.write('[exclude]\n')
176 176 for e in sorted(excludes):
177 177 fh.write(e)
178 178 fh.write('\n')
179 179
180 180 repo._sparsesignaturecache.clear()
181 181
182 182 def readtemporaryincludes(repo):
183 183 raw = repo.vfs.tryread('tempsparse')
184 184 if not raw:
185 185 return set()
186 186
187 187 return set(raw.split('\n'))
188 188
189 189 def writetemporaryincludes(repo, includes):
190 190 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
191 191 repo._sparsesignaturecache.clear()
192 192
193 193 def addtemporaryincludes(repo, additional):
194 194 includes = readtemporaryincludes(repo)
195 195 for i in additional:
196 196 includes.add(i)
197 197 writetemporaryincludes(repo, includes)
198 198
199 199 def prunetemporaryincludes(repo):
200 200 if not enabled or not repo.vfs.exists('tempsparse'):
201 201 return
202 202
203 203 origstatus = repo.status()
204 204 modified, added, removed, deleted, a, b, c = origstatus
205 205 if modified or added or removed or deleted:
206 206 # Still have pending changes. Don't bother trying to prune.
207 207 return
208 208
209 209 sparsematch = matcher(repo, includetemp=False)
210 210 dirstate = repo.dirstate
211 211 actions = []
212 212 dropped = []
213 213 tempincludes = readtemporaryincludes(repo)
214 214 for file in tempincludes:
215 215 if file in dirstate and not sparsematch(file):
216 216 message = _('dropping temporarily included sparse files')
217 217 actions.append((file, None, message))
218 218 dropped.append(file)
219 219
220 220 typeactions = collections.defaultdict(list)
221 221 typeactions['r'] = actions
222 222 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
223 223
224 224 # Fix dirstate
225 225 for file in dropped:
226 226 dirstate.drop(file)
227 227
228 228 repo.vfs.unlink('tempsparse')
229 229 repo._sparsesignaturecache.clear()
230 230 msg = _('cleaned up %d temporarily added file(s) from the '
231 231 'sparse checkout\n')
232 232 repo.ui.status(msg % len(tempincludes))
233 233
234 234 def matcher(repo, revs=None, includetemp=True):
235 235 """Obtain a matcher for sparse working directories for the given revs.
236 236
237 237 If multiple revisions are specified, the matcher is the union of all
238 238 revs.
239 239
240 240 ``includetemp`` indicates whether to use the temporary sparse profile.
241 241 """
242 242 # If sparse isn't enabled, sparse matcher matches everything.
243 243 if not enabled:
244 244 return matchmod.always(repo.root, '')
245 245
246 246 if not revs or revs == [None]:
247 247 revs = [repo.changelog.rev(node)
248 248 for node in repo.dirstate.parents() if node != nullid]
249 249
250 250 signature = configsignature(repo, includetemp=includetemp)
251 251
252 252 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
253 253
254 254 result = repo._sparsematchercache.get(key)
255 255 if result:
256 256 return result
257 257
258 258 matchers = []
259 259 for rev in revs:
260 260 try:
261 261 includes, excludes, profiles = patternsforrev(repo, rev)
262 262
263 263 if includes or excludes:
264 264 # Explicitly include subdirectories of includes so
265 265 # status will walk them down to the actual include.
266 266 subdirs = set()
267 267 for include in includes:
268 268 # TODO consider using posix path functions here so Windows
269 269 # \ directory separators don't come into play.
270 270 dirname = os.path.dirname(include)
271 271 # basename is used to avoid issues with absolute
272 272 # paths (which on Windows can include the drive).
273 273 while os.path.basename(dirname):
274 274 subdirs.add(dirname)
275 275 dirname = os.path.dirname(dirname)
276 276
277 277 matcher = matchmod.match(repo.root, '', [],
278 278 include=includes, exclude=excludes,
279 279 default='relpath')
280 280 if subdirs:
281 281 matcher = matchmod.forceincludematcher(matcher, subdirs)
282 282 matchers.append(matcher)
283 283 except IOError:
284 284 pass
285 285
286 286 if not matchers:
287 287 result = matchmod.always(repo.root, '')
288 288 elif len(matchers) == 1:
289 289 result = matchers[0]
290 290 else:
291 291 result = matchmod.unionmatcher(matchers)
292 292
293 293 if includetemp:
294 294 tempincludes = readtemporaryincludes(repo)
295 295 result = matchmod.forceincludematcher(result, tempincludes)
296 296
297 297 repo._sparsematchercache[key] = result
298 298
299 299 return result
300 300
301 301 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
302 302 """Filter updates to only lay out files that match the sparse rules."""
303 303 if not enabled:
304 304 return actions
305 305
306 306 oldrevs = [pctx.rev() for pctx in wctx.parents()]
307 307 oldsparsematch = matcher(repo, oldrevs)
308 308
309 309 if oldsparsematch.always():
310 310 return actions
311 311
312 312 files = set()
313 313 prunedactions = {}
314 314
315 315 if branchmerge:
316 316 # If we're merging, use the wctx filter, since we're merging into
317 317 # the wctx.
318 318 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
319 319 else:
320 320 # If we're updating, use the target context's filter, since we're
321 321 # moving to the target context.
322 322 sparsematch = matcher(repo, [mctx.rev()])
323 323
324 324 temporaryfiles = []
325 325 for file, action in actions.iteritems():
326 326 type, args, msg = action
327 327 files.add(file)
328 328 if sparsematch(file):
329 329 prunedactions[file] = action
330 330 elif type == 'm':
331 331 temporaryfiles.append(file)
332 332 prunedactions[file] = action
333 333 elif branchmerge:
334 334 if type != 'k':
335 335 temporaryfiles.append(file)
336 336 prunedactions[file] = action
337 337 elif type == 'f':
338 338 prunedactions[file] = action
339 339 elif file in wctx:
340 340 prunedactions[file] = ('r', args, msg)
341 341
342 342 if len(temporaryfiles) > 0:
343 343 repo.ui.status(_('temporarily included %d file(s) in the sparse '
344 344 'checkout for merging\n') % len(temporaryfiles))
345 345 addtemporaryincludes(repo, temporaryfiles)
346 346
347 347 # Add the new files to the working copy so they can be merged, etc
348 348 actions = []
349 349 message = 'temporarily adding to sparse checkout'
350 350 wctxmanifest = repo[None].manifest()
351 351 for file in temporaryfiles:
352 352 if file in wctxmanifest:
353 353 fctx = repo[None][file]
354 354 actions.append((file, (fctx.flags(), False), message))
355 355
356 356 typeactions = collections.defaultdict(list)
357 357 typeactions['g'] = actions
358 358 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
359 359 False)
360 360
361 361 dirstate = repo.dirstate
362 362 for file, flags, msg in actions:
363 363 dirstate.normal(file)
364 364
365 365 profiles = activeprofiles(repo)
366 366 changedprofiles = profiles & files
367 367 # If an active profile changed during the update, refresh the checkout.
368 368 # Don't do this during a branch merge, since all incoming changes should
369 369 # have been handled by the temporary includes above.
370 370 if changedprofiles and not branchmerge:
371 371 mf = mctx.manifest()
372 372 for file in mf:
373 373 old = oldsparsematch(file)
374 374 new = sparsematch(file)
375 375 if not old and new:
376 376 flags = mf.flags(file)
377 377 prunedactions[file] = ('g', (flags, False), '')
378 378 elif old and not new:
379 379 prunedactions[file] = ('r', [], '')
380 380
381 381 return prunedactions
382 382
383 383 def refreshwdir(repo, origstatus, origsparsematch, force=False):
384 384 """Refreshes working directory by taking sparse config into account.
385 385
386 386 The old status and sparse matcher is compared against the current sparse
387 387 matcher.
388 388
389 389 Will abort if a file with pending changes is being excluded or included
390 390 unless ``force`` is True.
391 391 """
392 392 modified, added, removed, deleted, unknown, ignored, clean = origstatus
393 393
394 394 # Verify there are no pending changes
395 395 pending = set()
396 396 pending.update(modified)
397 397 pending.update(added)
398 398 pending.update(removed)
399 399 sparsematch = matcher(repo)
400 400 abort = False
401 401
402 402 for f in pending:
403 403 if not sparsematch(f):
404 404 repo.ui.warn(_("pending changes to '%s'\n") % f)
405 405 abort = not force
406 406
407 407 if abort:
408 408 raise error.Abort(_('could not update sparseness due to pending '
409 409 'changes'))
410 410
411 411 # Calculate actions
412 412 dirstate = repo.dirstate
413 413 ctx = repo['.']
414 414 added = []
415 415 lookup = []
416 416 dropped = []
417 417 mf = ctx.manifest()
418 418 files = set(mf)
419 419
420 420 actions = {}
421 421
422 422 for file in files:
423 423 old = origsparsematch(file)
424 424 new = sparsematch(file)
425 425 # Add files that are newly included, or that don't exist in
426 426 # the dirstate yet.
427 427 if (new and not old) or (old and new and not file in dirstate):
428 428 fl = mf.flags(file)
429 429 if repo.wvfs.exists(file):
430 430 actions[file] = ('e', (fl,), '')
431 431 lookup.append(file)
432 432 else:
433 433 actions[file] = ('g', (fl, False), '')
434 434 added.append(file)
435 435 # Drop files that are newly excluded, or that still exist in
436 436 # the dirstate.
437 437 elif (old and not new) or (not old and not new and file in dirstate):
438 438 dropped.append(file)
439 439 if file not in pending:
440 440 actions[file] = ('r', [], '')
441 441
442 442 # Verify there are no pending changes in newly included files
443 443 abort = False
444 444 for file in lookup:
445 445 repo.ui.warn(_("pending changes to '%s'\n") % file)
446 446 abort = not force
447 447 if abort:
448 448 raise error.Abort(_('cannot change sparseness due to pending '
449 449 'changes (delete the files or use '
450 450 '--force to bring them back dirty)'))
451 451
452 452 # Check for files that were only in the dirstate.
453 453 for file, state in dirstate.iteritems():
454 454 if not file in files:
455 455 old = origsparsematch(file)
456 456 new = sparsematch(file)
457 457 if old and not new:
458 458 dropped.append(file)
459 459
460 460 # Apply changes to disk
461 461 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
462 462 for f, (m, args, msg) in actions.iteritems():
463 463 if m not in typeactions:
464 464 typeactions[m] = []
465 465 typeactions[m].append((f, args, msg))
466 466
467 467 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
468 468
469 469 # Fix dirstate
470 470 for file in added:
471 471 dirstate.normal(file)
472 472
473 473 for file in dropped:
474 474 dirstate.drop(file)
475 475
476 476 for file in lookup:
477 477 # File exists on disk, and we're bringing it back in an unknown state.
478 478 dirstate.normallookup(file)
479 479
480 480 return added, dropped, lookup
481 481
482 482 def aftercommit(repo, node):
483 483 """Perform actions after a working directory commit."""
484 484 # This function is called unconditionally, even if sparse isn't
485 485 # enabled.
486 486 ctx = repo[node]
487 487
488 488 profiles = patternsforrev(repo, ctx.rev())[2]
489 489
490 490 # profiles will only have data if sparse is enabled.
491 491 if set(profiles) & set(ctx.files()):
492 492 origstatus = repo.status()
493 493 origsparsematch = matcher(repo)
494 494 refreshwdir(repo, origstatus, origsparsematch, force=True)
495 495
496 496 prunetemporaryincludes(repo)
497
498 def clearrules(repo, force=False):
499 """Clears include/exclude rules from the sparse config.
500
501 The remaining sparse config only has profiles, if defined. The working
502 directory is refreshed, as needed.
503 """
504 with repo.wlock():
505 raw = repo.vfs.tryread('sparse')
506 includes, excludes, profiles = parseconfig(repo.ui, raw)
507
508 if not includes and not excludes:
509 return
510
511 oldstatus = repo.status()
512 oldmatch = matcher(repo)
513 writeconfig(repo, set(), set(), profiles)
514 refreshwdir(repo, oldstatus, oldmatch, force=force)
General Comments 0
You need to be logged in to leave comments. Login now