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