##// END OF EJS Templates
sparse: fix debugrebuilddirsate when narrow extension is enabled...
Pulkit Goyal -
r41185:b05eb98a default
parent child Browse files
Show More
@@ -1,347 +1,347 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]``.
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 144 narrow_pat = opts.get(r'narrow')
145 145 include = exclude = enableprofile = False
146 146 if include_pat:
147 147 pat = include_pat
148 148 include = True
149 149 if exclude_pat:
150 150 pat = exclude_pat
151 151 exclude = True
152 152 if enableprofile_pat:
153 153 pat = enableprofile_pat
154 154 enableprofile = True
155 155 if sum([include, exclude, enableprofile]) > 1:
156 156 raise error.Abort(_("too many flags specified."))
157 157 # if --narrow is passed, it means they are includes and excludes for narrow
158 158 # clone
159 159 if not narrow_pat and (include or exclude or enableprofile):
160 160 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
161 161 sparse.updateconfig(self.unfiltered(), pat, {}, include=include,
162 162 exclude=exclude, enableprofile=enableprofile,
163 163 usereporootpaths=True)
164 164 return orig(self, node, overwrite, *args, **kwargs)
165 165 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
166 166 return orig(ui, repo, *args, **opts)
167 167
168 168 def _setupclone(ui):
169 169 entry = commands.table['clone']
170 170 entry[1].append(('', 'enable-profile', [],
171 171 'enable a sparse profile'))
172 172 entry[1].append(('', 'include', [],
173 173 'include sparse pattern'))
174 174 entry[1].append(('', 'exclude', [],
175 175 'exclude sparse pattern'))
176 176 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
177 177
178 178 def _setupadd(ui):
179 179 entry = commands.table['add']
180 180 entry[1].append(('s', 'sparse', None,
181 181 'also include directories of added files in sparse config'))
182 182
183 183 def _add(orig, ui, repo, *pats, **opts):
184 184 if opts.get(r'sparse'):
185 185 dirs = set()
186 186 for pat in pats:
187 187 dirname, basename = util.split(pat)
188 188 dirs.add(dirname)
189 189 sparse.updateconfig(repo, list(dirs), opts, include=True)
190 190 return orig(ui, repo, *pats, **opts)
191 191
192 192 extensions.wrapcommand(commands.table, 'add', _add)
193 193
194 194 def _setupdirstate(ui):
195 195 """Modify the dirstate to prevent stat'ing excluded files,
196 196 and to prevent modifications to files outside the checkout.
197 197 """
198 198
199 199 def walk(orig, self, match, subrepos, unknown, ignored, full=True):
200 200 # hack to not exclude explicitly-specified paths so that they can
201 201 # be warned later on e.g. dirstate.add()
202 202 em = matchmod.exact(match._root, match._cwd, match.files())
203 203 sm = matchmod.unionmatcher([self._sparsematcher, em])
204 204 match = matchmod.intersectmatchers(match, sm)
205 205 return orig(self, match, subrepos, unknown, ignored, full)
206 206
207 207 extensions.wrapfunction(dirstate.dirstate, 'walk', walk)
208 208
209 209 # dirstate.rebuild should not add non-matching files
210 210 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
211 211 matcher = self._sparsematcher
212 212 if not matcher.always():
213 allfiles = allfiles.matches(matcher)
213 allfiles = [f for f in allfiles if matcher(f)]
214 214 if changedfiles:
215 215 changedfiles = [f for f in changedfiles if matcher(f)]
216 216
217 217 if changedfiles is not None:
218 218 # In _rebuild, these files will be deleted from the dirstate
219 219 # when they are not found to be in allfiles
220 220 dirstatefilestoremove = set(f for f in self if not matcher(f))
221 221 changedfiles = dirstatefilestoremove.union(changedfiles)
222 222
223 223 return orig(self, parent, allfiles, changedfiles)
224 224 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
225 225
226 226 # Prevent adding files that are outside the sparse checkout
227 227 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
228 228 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
229 229 '`hg add -s <file>` to include file directory while adding')
230 230 for func in editfuncs:
231 231 def _wrapper(orig, self, *args):
232 232 sparsematch = self._sparsematcher
233 233 if not sparsematch.always():
234 234 for f in args:
235 235 if (f is not None and not sparsematch(f) and
236 236 f not in self):
237 237 raise error.Abort(_("cannot add '%s' - it is outside "
238 238 "the sparse checkout") % f,
239 239 hint=hint)
240 240 return orig(self, *args)
241 241 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
242 242
243 243 @command('debugsparse', [
244 244 ('I', 'include', False, _('include files in the sparse checkout')),
245 245 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
246 246 ('d', 'delete', False, _('delete an include/exclude rule')),
247 247 ('f', 'force', False, _('allow changing rules even with pending changes')),
248 248 ('', 'enable-profile', False, _('enables the specified profile')),
249 249 ('', 'disable-profile', False, _('disables the specified profile')),
250 250 ('', 'import-rules', False, _('imports rules from a file')),
251 251 ('', 'clear-rules', False, _('clears local include/exclude rules')),
252 252 ('', 'refresh', False, _('updates the working after sparseness changes')),
253 253 ('', 'reset', False, _('makes the repo full again')),
254 254 ] + commands.templateopts,
255 255 _('[--OPTION] PATTERN...'),
256 256 helpbasic=True)
257 257 def debugsparse(ui, repo, *pats, **opts):
258 258 """make the current checkout sparse, or edit the existing checkout
259 259
260 260 The sparse command is used to make the current checkout sparse.
261 261 This means files that don't meet the sparse condition will not be
262 262 written to disk, or show up in any working copy operations. It does
263 263 not affect files in history in any way.
264 264
265 265 Passing no arguments prints the currently applied sparse rules.
266 266
267 267 --include and --exclude are used to add and remove files from the sparse
268 268 checkout. The effects of adding an include or exclude rule are applied
269 269 immediately. If applying the new rule would cause a file with pending
270 270 changes to be added or removed, the command will fail. Pass --force to
271 271 force a rule change even with pending changes (the changes on disk will
272 272 be preserved).
273 273
274 274 --delete removes an existing include/exclude rule. The effects are
275 275 immediate.
276 276
277 277 --refresh refreshes the files on disk based on the sparse rules. This is
278 278 only necessary if .hg/sparse was changed by hand.
279 279
280 280 --enable-profile and --disable-profile accept a path to a .hgsparse file.
281 281 This allows defining sparse checkouts and tracking them inside the
282 282 repository. This is useful for defining commonly used sparse checkouts for
283 283 many people to use. As the profile definition changes over time, the sparse
284 284 checkout will automatically be updated appropriately, depending on which
285 285 changeset is checked out. Changes to .hgsparse are not applied until they
286 286 have been committed.
287 287
288 288 --import-rules accepts a path to a file containing rules in the .hgsparse
289 289 format, allowing you to add --include, --exclude and --enable-profile rules
290 290 in bulk. Like the --include, --exclude and --enable-profile switches, the
291 291 changes are applied immediately.
292 292
293 293 --clear-rules removes all local include and exclude rules, while leaving
294 294 any enabled profiles in place.
295 295
296 296 Returns 0 if editing the sparse checkout succeeds.
297 297 """
298 298 opts = pycompat.byteskwargs(opts)
299 299 include = opts.get('include')
300 300 exclude = opts.get('exclude')
301 301 force = opts.get('force')
302 302 enableprofile = opts.get('enable_profile')
303 303 disableprofile = opts.get('disable_profile')
304 304 importrules = opts.get('import_rules')
305 305 clearrules = opts.get('clear_rules')
306 306 delete = opts.get('delete')
307 307 refresh = opts.get('refresh')
308 308 reset = opts.get('reset')
309 309 count = sum([include, exclude, enableprofile, disableprofile, delete,
310 310 importrules, refresh, clearrules, reset])
311 311 if count > 1:
312 312 raise error.Abort(_("too many flags specified"))
313 313
314 314 if count == 0:
315 315 if repo.vfs.exists('sparse'):
316 316 ui.status(repo.vfs.read("sparse") + "\n")
317 317 temporaryincludes = sparse.readtemporaryincludes(repo)
318 318 if temporaryincludes:
319 319 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
320 320 ui.status(("\n".join(temporaryincludes) + "\n"))
321 321 else:
322 322 ui.status(_('repo is not sparse\n'))
323 323 return
324 324
325 325 if include or exclude or delete or reset or enableprofile or disableprofile:
326 326 sparse.updateconfig(repo, pats, opts, include=include, exclude=exclude,
327 327 reset=reset, delete=delete,
328 328 enableprofile=enableprofile,
329 329 disableprofile=disableprofile, force=force)
330 330
331 331 if importrules:
332 332 sparse.importfromfiles(repo, opts, pats, force=force)
333 333
334 334 if clearrules:
335 335 sparse.clearrules(repo, force=force)
336 336
337 337 if refresh:
338 338 try:
339 339 wlock = repo.wlock()
340 340 fcounts = map(
341 341 len,
342 342 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
343 343 force=force))
344 344 sparse.printchanges(ui, opts, added=fcounts[0], dropped=fcounts[1],
345 345 conflicting=fcounts[2])
346 346 finally:
347 347 wlock.release()
@@ -1,109 +1,69 b''
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 55 XXX: we should have a flag in `hg debugsparse` to list the sparse profile
56 56 $ test -f .hg/sparse
57 57 [1]
58 58
59 59 $ cat .hg/requires
60 60 dotencode
61 61 fncache
62 62 generaldelta
63 63 narrowhg-experimental
64 64 revlogv1
65 65 sparserevlog
66 66 store
67 67 treemanifest (tree !)
68 68
69 69 $ hg debugrebuilddirstate
70 ** unknown exception encountered, please report by visiting
71 ** https://mercurial-scm.org/wiki/BugTracker
72 ** Python 2.7.12 (default, Nov 12 2018, 14:36:49) [GCC 5.4.0 20160609]
73 ** Mercurial Distributed SCM (version 4.8.1+588-479a5ea51ccc+20181224)
74 ** Extensions loaded: narrow, sparse
75 Traceback (most recent call last):
76 File "/place/vartmp/hgtests.zMelCK/install/bin/hg", line 43, in <module>
77 dispatch.run()
78 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/dispatch.py", line 99, in run
79 status = dispatch(req)
80 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/dispatch.py", line 225, in dispatch
81 ret = _runcatch(req) or 0
82 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/dispatch.py", line 376, in _runcatch
83 return _callcatch(ui, _runcatchfunc)
84 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/dispatch.py", line 384, in _callcatch
85 return scmutil.callcatch(ui, func)
86 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/scmutil.py", line 166, in callcatch
87 return func()
88 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/dispatch.py", line 367, in _runcatchfunc
89 return _dispatch(req)
90 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/dispatch.py", line 1021, in _dispatch
91 cmdpats, cmdoptions)
92 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/dispatch.py", line 756, in runcommand
93 ret = _runcommand(ui, options, cmd, d)
94 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/dispatch.py", line 1030, in _runcommand
95 return cmdfunc()
96 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/dispatch.py", line 1018, in <lambda>
97 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
98 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/util.py", line 1670, in check
99 return func(*args, **kwargs)
100 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/debugcommands.py", line 1998, in debugrebuilddirstate
101 dirstate.rebuild(ctx.node(), ctx.manifest(), changedfiles)
102 File "/place/vartmp/hgtests.zMelCK/install/lib/python/hgext/narrow/narrowdirstate.py", line 60, in rebuild
103 super(narrowdirstate, self).rebuild(parent, allfiles, changedfiles)
104 File "/place/vartmp/hgtests.zMelCK/install/lib/python/mercurial/extensions.py", line 437, in closure
105 return func(*(args + a), **kw)
106 File "/place/vartmp/hgtests.zMelCK/install/lib/python/hgext/sparse.py", line 213, in _rebuild
107 allfiles = allfiles.matches(matcher)
108 AttributeError: 'list' object has no attribute 'matches'
109 [1]
General Comments 0
You need to be logged in to leave comments. Login now