##// END OF EJS Templates
sparse: use self instead of repo.dirstate...
Gregory Szorc -
r33372:4481f1fd default
parent child Browse files
Show More
@@ -1,446 +1,445 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 import (
79 79 cmdutil,
80 80 commands,
81 81 dirstate,
82 82 error,
83 83 extensions,
84 84 hg,
85 85 localrepo,
86 86 match as matchmod,
87 87 registrar,
88 88 sparse,
89 89 util,
90 90 )
91 91
92 92 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
93 93 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
94 94 # be specifying the version(s) of Mercurial they are tested with, or
95 95 # leave the attribute unspecified.
96 96 testedwith = 'ships-with-hg-core'
97 97
98 98 cmdtable = {}
99 99 command = registrar.command(cmdtable)
100 100
101 101 def extsetup(ui):
102 102 sparse.enabled = True
103 103
104 104 _setupclone(ui)
105 105 _setuplog(ui)
106 106 _setupadd(ui)
107 107 _setupdirstate(ui)
108 108
109 109 def reposetup(ui, repo):
110 110 if not util.safehasattr(repo, 'dirstate'):
111 111 return
112 112
113 113 if 'dirstate' in repo._filecache:
114 114 repo.dirstate.repo = repo
115 115
116 116 def replacefilecache(cls, propname, replacement):
117 117 """Replace a filecache property with a new class. This allows changing the
118 118 cache invalidation condition."""
119 119 origcls = cls
120 120 assert callable(replacement)
121 121 while cls is not object:
122 122 if propname in cls.__dict__:
123 123 orig = cls.__dict__[propname]
124 124 setattr(cls, propname, replacement(orig))
125 125 break
126 126 cls = cls.__bases__[0]
127 127
128 128 if cls is object:
129 129 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
130 130 propname))
131 131
132 132 def _setuplog(ui):
133 133 entry = commands.table['^log|history']
134 134 entry[1].append(('', 'sparse', None,
135 135 "limit to changesets affecting the sparse checkout"))
136 136
137 137 def _logrevs(orig, repo, opts):
138 138 revs = orig(repo, opts)
139 139 if opts.get('sparse'):
140 140 sparsematch = sparse.matcher(repo)
141 141 def ctxmatch(rev):
142 142 ctx = repo[rev]
143 143 return any(f for f in ctx.files() if sparsematch(f))
144 144 revs = revs.filter(ctxmatch)
145 145 return revs
146 146 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
147 147
148 148 def _clonesparsecmd(orig, ui, repo, *args, **opts):
149 149 include_pat = opts.get('include')
150 150 exclude_pat = opts.get('exclude')
151 151 enableprofile_pat = opts.get('enable_profile')
152 152 include = exclude = enableprofile = False
153 153 if include_pat:
154 154 pat = include_pat
155 155 include = True
156 156 if exclude_pat:
157 157 pat = exclude_pat
158 158 exclude = True
159 159 if enableprofile_pat:
160 160 pat = enableprofile_pat
161 161 enableprofile = True
162 162 if sum([include, exclude, enableprofile]) > 1:
163 163 raise error.Abort(_("too many flags specified."))
164 164 if include or exclude or enableprofile:
165 165 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
166 166 _config(self.ui, self.unfiltered(), pat, {}, include=include,
167 167 exclude=exclude, enableprofile=enableprofile)
168 168 return orig(self, node, overwrite, *args, **kwargs)
169 169 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
170 170 return orig(ui, repo, *args, **opts)
171 171
172 172 def _setupclone(ui):
173 173 entry = commands.table['^clone']
174 174 entry[1].append(('', 'enable-profile', [],
175 175 'enable a sparse profile'))
176 176 entry[1].append(('', 'include', [],
177 177 'include sparse pattern'))
178 178 entry[1].append(('', 'exclude', [],
179 179 'exclude sparse pattern'))
180 180 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
181 181
182 182 def _setupadd(ui):
183 183 entry = commands.table['^add']
184 184 entry[1].append(('s', 'sparse', None,
185 185 'also include directories of added files in sparse config'))
186 186
187 187 def _add(orig, ui, repo, *pats, **opts):
188 188 if opts.get('sparse'):
189 189 dirs = set()
190 190 for pat in pats:
191 191 dirname, basename = util.split(pat)
192 192 dirs.add(dirname)
193 193 _config(ui, repo, list(dirs), opts, include=True)
194 194 return orig(ui, repo, *pats, **opts)
195 195
196 196 extensions.wrapcommand(commands.table, 'add', _add)
197 197
198 198 def _setupdirstate(ui):
199 199 """Modify the dirstate to prevent stat'ing excluded files,
200 200 and to prevent modifications to files outside the checkout.
201 201 """
202 202
203 203 def _dirstate(orig, repo):
204 204 dirstate = orig(repo)
205 205 dirstate.repo = repo
206 206 return dirstate
207 207 extensions.wrapfunction(
208 208 localrepo.localrepository.dirstate, 'func', _dirstate)
209 209
210 210 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
211 211 # property, which means normal function wrapping doesn't work.
212 212 class ignorewrapper(object):
213 213 def __init__(self, orig):
214 214 self.orig = orig
215 215 self.origignore = None
216 216 self.func = None
217 217 self.sparsematch = None
218 218
219 219 def __get__(self, obj, type=None):
220 220 repo = obj.repo
221 221 origignore = self.orig.__get__(obj)
222 222
223 223 sparsematch = sparse.matcher(repo)
224 224 if sparsematch.always():
225 225 return origignore
226 226
227 227 if self.sparsematch != sparsematch or self.origignore != origignore:
228 228 self.func = matchmod.unionmatcher([
229 229 origignore, matchmod.negatematcher(sparsematch)])
230 230 self.sparsematch = sparsematch
231 231 self.origignore = origignore
232 232 return self.func
233 233
234 234 def __set__(self, obj, value):
235 235 return self.orig.__set__(obj, value)
236 236
237 237 def __delete__(self, obj):
238 238 return self.orig.__delete__(obj)
239 239
240 240 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
241 241
242 242 # dirstate.rebuild should not add non-matching files
243 243 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
244 244 matcher = sparse.matcher(self.repo)
245 245 if not matcher.always():
246 246 allfiles = allfiles.matches(matcher)
247 247 if changedfiles:
248 248 changedfiles = [f for f in changedfiles if matcher(f)]
249 249
250 250 if changedfiles is not None:
251 251 # In _rebuild, these files will be deleted from the dirstate
252 252 # when they are not found to be in allfiles
253 253 dirstatefilestoremove = set(f for f in self if not matcher(f))
254 254 changedfiles = dirstatefilestoremove.union(changedfiles)
255 255
256 256 return orig(self, parent, allfiles, changedfiles)
257 257 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
258 258
259 259 # Prevent adding files that are outside the sparse checkout
260 260 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
261 261 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
262 262 '`hg add -s <file>` to include file directory while adding')
263 263 for func in editfuncs:
264 264 def _wrapper(orig, self, *args):
265 265 repo = self.repo
266 266 sparsematch = sparse.matcher(repo)
267 267 if not sparsematch.always():
268 dirstate = repo.dirstate
269 268 for f in args:
270 269 if (f is not None and not sparsematch(f) and
271 f not in dirstate):
270 f not in self):
272 271 raise error.Abort(_("cannot add '%s' - it is outside "
273 272 "the sparse checkout") % f,
274 273 hint=hint)
275 274 return orig(self, *args)
276 275 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
277 276
278 277 @command('^debugsparse', [
279 278 ('I', 'include', False, _('include files in the sparse checkout')),
280 279 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
281 280 ('d', 'delete', False, _('delete an include/exclude rule')),
282 281 ('f', 'force', False, _('allow changing rules even with pending changes')),
283 282 ('', 'enable-profile', False, _('enables the specified profile')),
284 283 ('', 'disable-profile', False, _('disables the specified profile')),
285 284 ('', 'import-rules', False, _('imports rules from a file')),
286 285 ('', 'clear-rules', False, _('clears local include/exclude rules')),
287 286 ('', 'refresh', False, _('updates the working after sparseness changes')),
288 287 ('', 'reset', False, _('makes the repo full again')),
289 288 ] + commands.templateopts,
290 289 _('[--OPTION] PATTERN...'))
291 290 def debugsparse(ui, repo, *pats, **opts):
292 291 """make the current checkout sparse, or edit the existing checkout
293 292
294 293 The sparse command is used to make the current checkout sparse.
295 294 This means files that don't meet the sparse condition will not be
296 295 written to disk, or show up in any working copy operations. It does
297 296 not affect files in history in any way.
298 297
299 298 Passing no arguments prints the currently applied sparse rules.
300 299
301 300 --include and --exclude are used to add and remove files from the sparse
302 301 checkout. The effects of adding an include or exclude rule are applied
303 302 immediately. If applying the new rule would cause a file with pending
304 303 changes to be added or removed, the command will fail. Pass --force to
305 304 force a rule change even with pending changes (the changes on disk will
306 305 be preserved).
307 306
308 307 --delete removes an existing include/exclude rule. The effects are
309 308 immediate.
310 309
311 310 --refresh refreshes the files on disk based on the sparse rules. This is
312 311 only necessary if .hg/sparse was changed by hand.
313 312
314 313 --enable-profile and --disable-profile accept a path to a .hgsparse file.
315 314 This allows defining sparse checkouts and tracking them inside the
316 315 repository. This is useful for defining commonly used sparse checkouts for
317 316 many people to use. As the profile definition changes over time, the sparse
318 317 checkout will automatically be updated appropriately, depending on which
319 318 changeset is checked out. Changes to .hgsparse are not applied until they
320 319 have been committed.
321 320
322 321 --import-rules accepts a path to a file containing rules in the .hgsparse
323 322 format, allowing you to add --include, --exclude and --enable-profile rules
324 323 in bulk. Like the --include, --exclude and --enable-profile switches, the
325 324 changes are applied immediately.
326 325
327 326 --clear-rules removes all local include and exclude rules, while leaving
328 327 any enabled profiles in place.
329 328
330 329 Returns 0 if editing the sparse checkout succeeds.
331 330 """
332 331 include = opts.get('include')
333 332 exclude = opts.get('exclude')
334 333 force = opts.get('force')
335 334 enableprofile = opts.get('enable_profile')
336 335 disableprofile = opts.get('disable_profile')
337 336 importrules = opts.get('import_rules')
338 337 clearrules = opts.get('clear_rules')
339 338 delete = opts.get('delete')
340 339 refresh = opts.get('refresh')
341 340 reset = opts.get('reset')
342 341 count = sum([include, exclude, enableprofile, disableprofile, delete,
343 342 importrules, refresh, clearrules, reset])
344 343 if count > 1:
345 344 raise error.Abort(_("too many flags specified"))
346 345
347 346 if count == 0:
348 347 if repo.vfs.exists('sparse'):
349 348 ui.status(repo.vfs.read("sparse") + "\n")
350 349 temporaryincludes = sparse.readtemporaryincludes(repo)
351 350 if temporaryincludes:
352 351 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
353 352 ui.status(("\n".join(temporaryincludes) + "\n"))
354 353 else:
355 354 ui.status(_('repo is not sparse\n'))
356 355 return
357 356
358 357 if include or exclude or delete or reset or enableprofile or disableprofile:
359 358 _config(ui, repo, pats, opts, include=include, exclude=exclude,
360 359 reset=reset, delete=delete, enableprofile=enableprofile,
361 360 disableprofile=disableprofile, force=force)
362 361
363 362 if importrules:
364 363 sparse.importfromfiles(repo, opts, pats, force=force)
365 364
366 365 if clearrules:
367 366 sparse.clearrules(repo, force=force)
368 367
369 368 if refresh:
370 369 try:
371 370 wlock = repo.wlock()
372 371 fcounts = map(
373 372 len,
374 373 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
375 374 force=force))
376 375 sparse.printchanges(ui, opts, added=fcounts[0], dropped=fcounts[1],
377 376 conflicting=fcounts[2])
378 377 finally:
379 378 wlock.release()
380 379
381 380 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
382 381 delete=False, enableprofile=False, disableprofile=False,
383 382 force=False):
384 383 """
385 384 Perform a sparse config update. Only one of the kwargs may be specified.
386 385 """
387 386 wlock = repo.wlock()
388 387 try:
389 388 oldsparsematch = sparse.matcher(repo)
390 389
391 390 raw = repo.vfs.tryread('sparse')
392 391 if raw:
393 392 oldinclude, oldexclude, oldprofiles = map(
394 393 set, sparse.parseconfig(ui, raw))
395 394 else:
396 395 oldinclude = set()
397 396 oldexclude = set()
398 397 oldprofiles = set()
399 398
400 399 try:
401 400 if reset:
402 401 newinclude = set()
403 402 newexclude = set()
404 403 newprofiles = set()
405 404 else:
406 405 newinclude = set(oldinclude)
407 406 newexclude = set(oldexclude)
408 407 newprofiles = set(oldprofiles)
409 408
410 409 oldstatus = repo.status()
411 410
412 411 if any(pat.startswith('/') for pat in pats):
413 412 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
414 413 % ([pat for pat in pats if pat.startswith('/')]))
415 414 elif include:
416 415 newinclude.update(pats)
417 416 elif exclude:
418 417 newexclude.update(pats)
419 418 elif enableprofile:
420 419 newprofiles.update(pats)
421 420 elif disableprofile:
422 421 newprofiles.difference_update(pats)
423 422 elif delete:
424 423 newinclude.difference_update(pats)
425 424 newexclude.difference_update(pats)
426 425
427 426 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
428 427
429 428 fcounts = map(
430 429 len,
431 430 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
432 431 force=force))
433 432
434 433 profilecount = (len(newprofiles - oldprofiles) -
435 434 len(oldprofiles - newprofiles))
436 435 includecount = (len(newinclude - oldinclude) -
437 436 len(oldinclude - newinclude))
438 437 excludecount = (len(newexclude - oldexclude) -
439 438 len(oldexclude - newexclude))
440 439 sparse.printchanges(ui, opts, profilecount, includecount,
441 440 excludecount, *fcounts)
442 441 except Exception:
443 442 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
444 443 raise
445 444 finally:
446 445 wlock.release()
General Comments 0
You need to be logged in to leave comments. Login now