##// END OF EJS Templates
sparse: don't enable on clone if it was a narrow clone...
Pulkit Goyal -
r41183:8eaf693b default
parent child Browse files
Show More
@@ -1,344 +1,347
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]``.
39 39
40 40 Non-special lines resemble file patterns to be added to either includes
41 41 or excludes. The syntax of these lines is documented by :hg:`help patterns`.
42 42 Patterns are interpreted as ``glob:`` by default and match against the
43 43 root of the repository.
44 44
45 45 Exclusion patterns take precedence over inclusion patterns. So even
46 46 if a file is explicitly included, an ``[exclude]`` entry can remove it.
47 47
48 48 For example, say you have a repository with 3 directories, ``frontend/``,
49 49 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
50 50 to different projects and it is uncommon for someone working on one
51 51 to need the files for the other. But ``tools/`` contains files shared
52 52 between both projects. Your sparse config files may resemble::
53 53
54 54 # frontend.sparse
55 55 frontend/**
56 56 tools/**
57 57
58 58 # backend.sparse
59 59 backend/**
60 60 tools/**
61 61
62 62 Say the backend grows in size. Or there's a directory with thousands
63 63 of files you wish to exclude. You can modify the profile to exclude
64 64 certain files::
65 65
66 66 [include]
67 67 backend/**
68 68 tools/**
69 69
70 70 [exclude]
71 71 tools/tests/**
72 72 """
73 73
74 74 from __future__ import absolute_import
75 75
76 76 from mercurial.i18n import _
77 77 from mercurial import (
78 78 commands,
79 79 dirstate,
80 80 error,
81 81 extensions,
82 82 hg,
83 83 logcmdutil,
84 84 match as matchmod,
85 85 pycompat,
86 86 registrar,
87 87 sparse,
88 88 util,
89 89 )
90 90
91 91 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
92 92 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
93 93 # be specifying the version(s) of Mercurial they are tested with, or
94 94 # leave the attribute unspecified.
95 95 testedwith = 'ships-with-hg-core'
96 96
97 97 cmdtable = {}
98 98 command = registrar.command(cmdtable)
99 99
100 100 def extsetup(ui):
101 101 sparse.enabled = True
102 102
103 103 _setupclone(ui)
104 104 _setuplog(ui)
105 105 _setupadd(ui)
106 106 _setupdirstate(ui)
107 107
108 108 def replacefilecache(cls, propname, replacement):
109 109 """Replace a filecache property with a new class. This allows changing the
110 110 cache invalidation condition."""
111 111 origcls = cls
112 112 assert callable(replacement)
113 113 while cls is not object:
114 114 if propname in cls.__dict__:
115 115 orig = cls.__dict__[propname]
116 116 setattr(cls, propname, replacement(orig))
117 117 break
118 118 cls = cls.__bases__[0]
119 119
120 120 if cls is object:
121 121 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
122 122 propname))
123 123
124 124 def _setuplog(ui):
125 125 entry = commands.table['log|history']
126 126 entry[1].append(('', 'sparse', None,
127 127 "limit to changesets affecting the sparse checkout"))
128 128
129 129 def _initialrevs(orig, repo, opts):
130 130 revs = orig(repo, opts)
131 131 if opts.get('sparse'):
132 132 sparsematch = sparse.matcher(repo)
133 133 def ctxmatch(rev):
134 134 ctx = repo[rev]
135 135 return any(f for f in ctx.files() if sparsematch(f))
136 136 revs = revs.filter(ctxmatch)
137 137 return revs
138 138 extensions.wrapfunction(logcmdutil, '_initialrevs', _initialrevs)
139 139
140 140 def _clonesparsecmd(orig, ui, repo, *args, **opts):
141 141 include_pat = opts.get(r'include')
142 142 exclude_pat = opts.get(r'exclude')
143 143 enableprofile_pat = opts.get(r'enable_profile')
144 narrow_pat = opts.get(r'narrow')
144 145 include = exclude = enableprofile = False
145 146 if include_pat:
146 147 pat = include_pat
147 148 include = True
148 149 if exclude_pat:
149 150 pat = exclude_pat
150 151 exclude = True
151 152 if enableprofile_pat:
152 153 pat = enableprofile_pat
153 154 enableprofile = True
154 155 if sum([include, exclude, enableprofile]) > 1:
155 156 raise error.Abort(_("too many flags specified."))
156 if include or exclude or enableprofile:
157 # if --narrow is passed, it means they are includes and excludes for narrow
158 # clone
159 if not narrow_pat and (include or exclude or enableprofile):
157 160 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
158 161 sparse.updateconfig(self.unfiltered(), pat, {}, include=include,
159 162 exclude=exclude, enableprofile=enableprofile,
160 163 usereporootpaths=True)
161 164 return orig(self, node, overwrite, *args, **kwargs)
162 165 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
163 166 return orig(ui, repo, *args, **opts)
164 167
165 168 def _setupclone(ui):
166 169 entry = commands.table['clone']
167 170 entry[1].append(('', 'enable-profile', [],
168 171 'enable a sparse profile'))
169 172 entry[1].append(('', 'include', [],
170 173 'include sparse pattern'))
171 174 entry[1].append(('', 'exclude', [],
172 175 'exclude sparse pattern'))
173 176 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
174 177
175 178 def _setupadd(ui):
176 179 entry = commands.table['add']
177 180 entry[1].append(('s', 'sparse', None,
178 181 'also include directories of added files in sparse config'))
179 182
180 183 def _add(orig, ui, repo, *pats, **opts):
181 184 if opts.get(r'sparse'):
182 185 dirs = set()
183 186 for pat in pats:
184 187 dirname, basename = util.split(pat)
185 188 dirs.add(dirname)
186 189 sparse.updateconfig(repo, list(dirs), opts, include=True)
187 190 return orig(ui, repo, *pats, **opts)
188 191
189 192 extensions.wrapcommand(commands.table, 'add', _add)
190 193
191 194 def _setupdirstate(ui):
192 195 """Modify the dirstate to prevent stat'ing excluded files,
193 196 and to prevent modifications to files outside the checkout.
194 197 """
195 198
196 199 def walk(orig, self, match, subrepos, unknown, ignored, full=True):
197 200 # hack to not exclude explicitly-specified paths so that they can
198 201 # be warned later on e.g. dirstate.add()
199 202 em = matchmod.exact(match._root, match._cwd, match.files())
200 203 sm = matchmod.unionmatcher([self._sparsematcher, em])
201 204 match = matchmod.intersectmatchers(match, sm)
202 205 return orig(self, match, subrepos, unknown, ignored, full)
203 206
204 207 extensions.wrapfunction(dirstate.dirstate, 'walk', walk)
205 208
206 209 # dirstate.rebuild should not add non-matching files
207 210 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
208 211 matcher = self._sparsematcher
209 212 if not matcher.always():
210 213 allfiles = allfiles.matches(matcher)
211 214 if changedfiles:
212 215 changedfiles = [f for f in changedfiles if matcher(f)]
213 216
214 217 if changedfiles is not None:
215 218 # In _rebuild, these files will be deleted from the dirstate
216 219 # when they are not found to be in allfiles
217 220 dirstatefilestoremove = set(f for f in self if not matcher(f))
218 221 changedfiles = dirstatefilestoremove.union(changedfiles)
219 222
220 223 return orig(self, parent, allfiles, changedfiles)
221 224 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
222 225
223 226 # Prevent adding files that are outside the sparse checkout
224 227 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
225 228 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
226 229 '`hg add -s <file>` to include file directory while adding')
227 230 for func in editfuncs:
228 231 def _wrapper(orig, self, *args):
229 232 sparsematch = self._sparsematcher
230 233 if not sparsematch.always():
231 234 for f in args:
232 235 if (f is not None and not sparsematch(f) and
233 236 f not in self):
234 237 raise error.Abort(_("cannot add '%s' - it is outside "
235 238 "the sparse checkout") % f,
236 239 hint=hint)
237 240 return orig(self, *args)
238 241 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
239 242
240 243 @command('debugsparse', [
241 244 ('I', 'include', False, _('include files in the sparse checkout')),
242 245 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
243 246 ('d', 'delete', False, _('delete an include/exclude rule')),
244 247 ('f', 'force', False, _('allow changing rules even with pending changes')),
245 248 ('', 'enable-profile', False, _('enables the specified profile')),
246 249 ('', 'disable-profile', False, _('disables the specified profile')),
247 250 ('', 'import-rules', False, _('imports rules from a file')),
248 251 ('', 'clear-rules', False, _('clears local include/exclude rules')),
249 252 ('', 'refresh', False, _('updates the working after sparseness changes')),
250 253 ('', 'reset', False, _('makes the repo full again')),
251 254 ] + commands.templateopts,
252 255 _('[--OPTION] PATTERN...'),
253 256 helpbasic=True)
254 257 def debugsparse(ui, repo, *pats, **opts):
255 258 """make the current checkout sparse, or edit the existing checkout
256 259
257 260 The sparse command is used to make the current checkout sparse.
258 261 This means files that don't meet the sparse condition will not be
259 262 written to disk, or show up in any working copy operations. It does
260 263 not affect files in history in any way.
261 264
262 265 Passing no arguments prints the currently applied sparse rules.
263 266
264 267 --include and --exclude are used to add and remove files from the sparse
265 268 checkout. The effects of adding an include or exclude rule are applied
266 269 immediately. If applying the new rule would cause a file with pending
267 270 changes to be added or removed, the command will fail. Pass --force to
268 271 force a rule change even with pending changes (the changes on disk will
269 272 be preserved).
270 273
271 274 --delete removes an existing include/exclude rule. The effects are
272 275 immediate.
273 276
274 277 --refresh refreshes the files on disk based on the sparse rules. This is
275 278 only necessary if .hg/sparse was changed by hand.
276 279
277 280 --enable-profile and --disable-profile accept a path to a .hgsparse file.
278 281 This allows defining sparse checkouts and tracking them inside the
279 282 repository. This is useful for defining commonly used sparse checkouts for
280 283 many people to use. As the profile definition changes over time, the sparse
281 284 checkout will automatically be updated appropriately, depending on which
282 285 changeset is checked out. Changes to .hgsparse are not applied until they
283 286 have been committed.
284 287
285 288 --import-rules accepts a path to a file containing rules in the .hgsparse
286 289 format, allowing you to add --include, --exclude and --enable-profile rules
287 290 in bulk. Like the --include, --exclude and --enable-profile switches, the
288 291 changes are applied immediately.
289 292
290 293 --clear-rules removes all local include and exclude rules, while leaving
291 294 any enabled profiles in place.
292 295
293 296 Returns 0 if editing the sparse checkout succeeds.
294 297 """
295 298 opts = pycompat.byteskwargs(opts)
296 299 include = opts.get('include')
297 300 exclude = opts.get('exclude')
298 301 force = opts.get('force')
299 302 enableprofile = opts.get('enable_profile')
300 303 disableprofile = opts.get('disable_profile')
301 304 importrules = opts.get('import_rules')
302 305 clearrules = opts.get('clear_rules')
303 306 delete = opts.get('delete')
304 307 refresh = opts.get('refresh')
305 308 reset = opts.get('reset')
306 309 count = sum([include, exclude, enableprofile, disableprofile, delete,
307 310 importrules, refresh, clearrules, reset])
308 311 if count > 1:
309 312 raise error.Abort(_("too many flags specified"))
310 313
311 314 if count == 0:
312 315 if repo.vfs.exists('sparse'):
313 316 ui.status(repo.vfs.read("sparse") + "\n")
314 317 temporaryincludes = sparse.readtemporaryincludes(repo)
315 318 if temporaryincludes:
316 319 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
317 320 ui.status(("\n".join(temporaryincludes) + "\n"))
318 321 else:
319 322 ui.status(_('repo is not sparse\n'))
320 323 return
321 324
322 325 if include or exclude or delete or reset or enableprofile or disableprofile:
323 326 sparse.updateconfig(repo, pats, opts, include=include, exclude=exclude,
324 327 reset=reset, delete=delete,
325 328 enableprofile=enableprofile,
326 329 disableprofile=disableprofile, force=force)
327 330
328 331 if importrules:
329 332 sparse.importfromfiles(repo, opts, pats, force=force)
330 333
331 334 if clearrules:
332 335 sparse.clearrules(repo, force=force)
333 336
334 337 if refresh:
335 338 try:
336 339 wlock = repo.wlock()
337 340 fcounts = map(
338 341 len,
339 342 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
340 343 force=force))
341 344 sparse.printchanges(ui, opts, added=fcounts[0], dropped=fcounts[1],
342 345 conflicting=fcounts[2])
343 346 finally:
344 347 wlock.release()
@@ -1,69 +1,67
1 1 Testing interaction of sparse and narrow when both are enabled on the client
2 2 side and we do a non-ellipsis clone
3 3
4 4 #testcases tree flat
5 5 $ . "$TESTDIR/narrow-library.sh"
6 6 $ cat << EOF >> $HGRCPATH
7 7 > [extensions]
8 8 > sparse =
9 9 > EOF
10 10
11 11 #if tree
12 12 $ cat << EOF >> $HGRCPATH
13 13 > [experimental]
14 14 > treemanifest = 1
15 15 > EOF
16 16 #endif
17 17
18 18 $ hg init master
19 19 $ cd master
20 20
21 21 $ mkdir inside
22 22 $ echo 'inside' > inside/f
23 23 $ hg add inside/f
24 24 $ hg commit -m 'add inside'
25 25
26 26 $ mkdir widest
27 27 $ echo 'widest' > widest/f
28 28 $ hg add widest/f
29 29 $ hg commit -m 'add widest'
30 30
31 31 $ mkdir outside
32 32 $ echo 'outside' > outside/f
33 33 $ hg add outside/f
34 34 $ hg commit -m 'add outside'
35 35
36 36 $ cd ..
37 37
38 38 narrow clone the inside file
39 39
40 40 $ hg clone --narrow ssh://user@dummy/master narrow --include inside/f
41 41 requesting all changes
42 42 adding changesets
43 43 adding manifests
44 44 adding file changes
45 45 added 3 changesets with 1 changes to 1 files
46 46 new changesets *:* (glob)
47 47 updating to branch default
48 48 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
49 49 $ cd narrow
50 50 $ hg tracked
51 51 I path:inside/f
52 52 $ hg files
53 53 inside/f
54 54
55 XXX: we should not have sparse enabled
56 $ cat .hg/sparse
57 [include]
58 inside/f
55 XXX: we should have a flag in `hg debugsparse` to list the sparse profile
56 $ test -f .hg/sparse
57 [1]
59 58
60 59 $ cat .hg/requires
61 60 dotencode
62 exp-sparse
63 61 fncache
64 62 generaldelta
65 63 narrowhg-experimental
66 64 revlogv1
67 65 sparserevlog
68 66 store
69 67 treemanifest (tree !)
General Comments 0
You need to be logged in to leave comments. Login now