##// END OF EJS Templates
sparse: use set for capturing profiles...
Gregory Szorc -
r33550:32f348d7 default
parent child Browse files
Show More
@@ -1,678 +1,676 b''
1 # sparse.py - functionality for sparse checkouts
1 # sparse.py - functionality for sparse checkouts
2 #
2 #
3 # Copyright 2014 Facebook, Inc.
3 # Copyright 2014 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import hashlib
11 import hashlib
12 import os
12 import os
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import nullid
15 from .node import nullid
16 from . import (
16 from . import (
17 error,
17 error,
18 match as matchmod,
18 match as matchmod,
19 merge as mergemod,
19 merge as mergemod,
20 pycompat,
20 pycompat,
21 util,
21 util,
22 )
22 )
23
23
24 # Whether sparse features are enabled. This variable is intended to be
24 # Whether sparse features are enabled. This variable is intended to be
25 # temporary to facilitate porting sparse to core. It should eventually be
25 # temporary to facilitate porting sparse to core. It should eventually be
26 # a per-repo option, possibly a repo requirement.
26 # a per-repo option, possibly a repo requirement.
27 enabled = False
27 enabled = False
28
28
29 def parseconfig(ui, raw):
29 def parseconfig(ui, raw):
30 """Parse sparse config file content.
30 """Parse sparse config file content.
31
31
32 Returns a tuple of includes, excludes, and profiles.
32 Returns a tuple of includes, excludes, and profiles.
33 """
33 """
34 includes = set()
34 includes = set()
35 excludes = set()
35 excludes = set()
36 current = includes
36 current = includes
37 profiles = []
37 profiles = set()
38 for line in raw.split('\n'):
38 for line in raw.split('\n'):
39 line = line.strip()
39 line = line.strip()
40 if not line or line.startswith('#'):
40 if not line or line.startswith('#'):
41 # empty or comment line, skip
41 # empty or comment line, skip
42 continue
42 continue
43 elif line.startswith('%include '):
43 elif line.startswith('%include '):
44 line = line[9:].strip()
44 line = line[9:].strip()
45 if line:
45 if line:
46 profiles.append(line)
46 profiles.add(line)
47 elif line == '[include]':
47 elif line == '[include]':
48 if current != includes:
48 if current != includes:
49 # TODO pass filename into this API so we can report it.
49 # TODO pass filename into this API so we can report it.
50 raise error.Abort(_('sparse config cannot have includes ' +
50 raise error.Abort(_('sparse config cannot have includes ' +
51 'after excludes'))
51 'after excludes'))
52 continue
52 continue
53 elif line == '[exclude]':
53 elif line == '[exclude]':
54 current = excludes
54 current = excludes
55 elif line:
55 elif line:
56 if line.strip().startswith('/'):
56 if line.strip().startswith('/'):
57 ui.warn(_('warning: sparse profile cannot use' +
57 ui.warn(_('warning: sparse profile cannot use' +
58 ' paths starting with /, ignoring %s\n') % line)
58 ' paths starting with /, ignoring %s\n') % line)
59 continue
59 continue
60 current.add(line)
60 current.add(line)
61
61
62 return includes, excludes, profiles
62 return includes, excludes, profiles
63
63
64 # Exists as separate function to facilitate monkeypatching.
64 # Exists as separate function to facilitate monkeypatching.
65 def readprofile(repo, profile, changeid):
65 def readprofile(repo, profile, changeid):
66 """Resolve the raw content of a sparse profile file."""
66 """Resolve the raw content of a sparse profile file."""
67 # TODO add some kind of cache here because this incurs a manifest
67 # TODO add some kind of cache here because this incurs a manifest
68 # resolve and can be slow.
68 # resolve and can be slow.
69 return repo.filectx(profile, changeid=changeid).data()
69 return repo.filectx(profile, changeid=changeid).data()
70
70
71 def patternsforrev(repo, rev):
71 def patternsforrev(repo, rev):
72 """Obtain sparse checkout patterns for the given rev.
72 """Obtain sparse checkout patterns for the given rev.
73
73
74 Returns a tuple of iterables representing includes, excludes, and
74 Returns a tuple of iterables representing includes, excludes, and
75 patterns.
75 patterns.
76 """
76 """
77 # Feature isn't enabled. No-op.
77 # Feature isn't enabled. No-op.
78 if not enabled:
78 if not enabled:
79 return set(), set(), []
79 return set(), set(), set()
80
80
81 raw = repo.vfs.tryread('sparse')
81 raw = repo.vfs.tryread('sparse')
82 if not raw:
82 if not raw:
83 return set(), set(), []
83 return set(), set(), set()
84
84
85 if rev is None:
85 if rev is None:
86 raise error.Abort(_('cannot parse sparse patterns from working '
86 raise error.Abort(_('cannot parse sparse patterns from working '
87 'directory'))
87 'directory'))
88
88
89 includes, excludes, profiles = parseconfig(repo.ui, raw)
89 includes, excludes, profiles = parseconfig(repo.ui, raw)
90 ctx = repo[rev]
90 ctx = repo[rev]
91
91
92 if profiles:
92 if profiles:
93 visited = set()
93 visited = set()
94 while profiles:
94 while profiles:
95 profile = profiles.pop()
95 profile = profiles.pop()
96 if profile in visited:
96 if profile in visited:
97 continue
97 continue
98
98
99 visited.add(profile)
99 visited.add(profile)
100
100
101 try:
101 try:
102 raw = readprofile(repo, profile, rev)
102 raw = readprofile(repo, profile, rev)
103 except error.ManifestLookupError:
103 except error.ManifestLookupError:
104 msg = (
104 msg = (
105 "warning: sparse profile '%s' not found "
105 "warning: sparse profile '%s' not found "
106 "in rev %s - ignoring it\n" % (profile, ctx))
106 "in rev %s - ignoring it\n" % (profile, ctx))
107 # experimental config: sparse.missingwarning
107 # experimental config: sparse.missingwarning
108 if repo.ui.configbool(
108 if repo.ui.configbool(
109 'sparse', 'missingwarning'):
109 'sparse', 'missingwarning'):
110 repo.ui.warn(msg)
110 repo.ui.warn(msg)
111 else:
111 else:
112 repo.ui.debug(msg)
112 repo.ui.debug(msg)
113 continue
113 continue
114
114
115 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
115 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
116 includes.update(pincludes)
116 includes.update(pincludes)
117 excludes.update(pexcludes)
117 excludes.update(pexcludes)
118 for subprofile in subprofs:
118 profiles.update(subprofs)
119 profiles.append(subprofile)
120
119
121 profiles = visited
120 profiles = visited
122
121
123 if includes:
122 if includes:
124 includes.add('.hg*')
123 includes.add('.hg*')
125
124
126 return includes, excludes, profiles
125 return includes, excludes, profiles
127
126
128 def activeconfig(repo):
127 def activeconfig(repo):
129 """Determine the active sparse config rules.
128 """Determine the active sparse config rules.
130
129
131 Rules are constructed by reading the current sparse config and bringing in
130 Rules are constructed by reading the current sparse config and bringing in
132 referenced profiles from parents of the working directory.
131 referenced profiles from parents of the working directory.
133 """
132 """
134 revs = [repo.changelog.rev(node) for node in
133 revs = [repo.changelog.rev(node) for node in
135 repo.dirstate.parents() if node != nullid]
134 repo.dirstate.parents() if node != nullid]
136
135
137 allincludes = set()
136 allincludes = set()
138 allexcludes = set()
137 allexcludes = set()
139 allprofiles = set()
138 allprofiles = set()
140
139
141 for rev in revs:
140 for rev in revs:
142 includes, excludes, profiles = patternsforrev(repo, rev)
141 includes, excludes, profiles = patternsforrev(repo, rev)
143 allincludes |= includes
142 allincludes |= includes
144 allexcludes |= excludes
143 allexcludes |= excludes
145 allprofiles |= set(profiles)
144 allprofiles |= profiles
146
145
147 return allincludes, allexcludes, allprofiles
146 return allincludes, allexcludes, allprofiles
148
147
149 def configsignature(repo, includetemp=True):
148 def configsignature(repo, includetemp=True):
150 """Obtain the signature string for the current sparse configuration.
149 """Obtain the signature string for the current sparse configuration.
151
150
152 This is used to construct a cache key for matchers.
151 This is used to construct a cache key for matchers.
153 """
152 """
154 cache = repo._sparsesignaturecache
153 cache = repo._sparsesignaturecache
155
154
156 signature = cache.get('signature')
155 signature = cache.get('signature')
157
156
158 if includetemp:
157 if includetemp:
159 tempsignature = cache.get('tempsignature')
158 tempsignature = cache.get('tempsignature')
160 else:
159 else:
161 tempsignature = '0'
160 tempsignature = '0'
162
161
163 if signature is None or (includetemp and tempsignature is None):
162 if signature is None or (includetemp and tempsignature is None):
164 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
163 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
165 cache['signature'] = signature
164 cache['signature'] = signature
166
165
167 if includetemp:
166 if includetemp:
168 raw = repo.vfs.tryread('tempsparse')
167 raw = repo.vfs.tryread('tempsparse')
169 tempsignature = hashlib.sha1(raw).hexdigest()
168 tempsignature = hashlib.sha1(raw).hexdigest()
170 cache['tempsignature'] = tempsignature
169 cache['tempsignature'] = tempsignature
171
170
172 return '%s %s' % (signature, tempsignature)
171 return '%s %s' % (signature, tempsignature)
173
172
174 def writeconfig(repo, includes, excludes, profiles):
173 def writeconfig(repo, includes, excludes, profiles):
175 """Write the sparse config file given a sparse configuration."""
174 """Write the sparse config file given a sparse configuration."""
176 with repo.vfs('sparse', 'wb') as fh:
175 with repo.vfs('sparse', 'wb') as fh:
177 for p in sorted(profiles):
176 for p in sorted(profiles):
178 fh.write('%%include %s\n' % p)
177 fh.write('%%include %s\n' % p)
179
178
180 if includes:
179 if includes:
181 fh.write('[include]\n')
180 fh.write('[include]\n')
182 for i in sorted(includes):
181 for i in sorted(includes):
183 fh.write(i)
182 fh.write(i)
184 fh.write('\n')
183 fh.write('\n')
185
184
186 if excludes:
185 if excludes:
187 fh.write('[exclude]\n')
186 fh.write('[exclude]\n')
188 for e in sorted(excludes):
187 for e in sorted(excludes):
189 fh.write(e)
188 fh.write(e)
190 fh.write('\n')
189 fh.write('\n')
191
190
192 repo._sparsesignaturecache.clear()
191 repo._sparsesignaturecache.clear()
193
192
194 def readtemporaryincludes(repo):
193 def readtemporaryincludes(repo):
195 raw = repo.vfs.tryread('tempsparse')
194 raw = repo.vfs.tryread('tempsparse')
196 if not raw:
195 if not raw:
197 return set()
196 return set()
198
197
199 return set(raw.split('\n'))
198 return set(raw.split('\n'))
200
199
201 def writetemporaryincludes(repo, includes):
200 def writetemporaryincludes(repo, includes):
202 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
201 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
203 repo._sparsesignaturecache.clear()
202 repo._sparsesignaturecache.clear()
204
203
205 def addtemporaryincludes(repo, additional):
204 def addtemporaryincludes(repo, additional):
206 includes = readtemporaryincludes(repo)
205 includes = readtemporaryincludes(repo)
207 for i in additional:
206 for i in additional:
208 includes.add(i)
207 includes.add(i)
209 writetemporaryincludes(repo, includes)
208 writetemporaryincludes(repo, includes)
210
209
211 def prunetemporaryincludes(repo):
210 def prunetemporaryincludes(repo):
212 if not enabled or not repo.vfs.exists('tempsparse'):
211 if not enabled or not repo.vfs.exists('tempsparse'):
213 return
212 return
214
213
215 s = repo.status()
214 s = repo.status()
216 if s.modified or s.added or s.removed or s.deleted:
215 if s.modified or s.added or s.removed or s.deleted:
217 # Still have pending changes. Don't bother trying to prune.
216 # Still have pending changes. Don't bother trying to prune.
218 return
217 return
219
218
220 sparsematch = matcher(repo, includetemp=False)
219 sparsematch = matcher(repo, includetemp=False)
221 dirstate = repo.dirstate
220 dirstate = repo.dirstate
222 actions = []
221 actions = []
223 dropped = []
222 dropped = []
224 tempincludes = readtemporaryincludes(repo)
223 tempincludes = readtemporaryincludes(repo)
225 for file in tempincludes:
224 for file in tempincludes:
226 if file in dirstate and not sparsematch(file):
225 if file in dirstate and not sparsematch(file):
227 message = _('dropping temporarily included sparse files')
226 message = _('dropping temporarily included sparse files')
228 actions.append((file, None, message))
227 actions.append((file, None, message))
229 dropped.append(file)
228 dropped.append(file)
230
229
231 typeactions = collections.defaultdict(list)
230 typeactions = collections.defaultdict(list)
232 typeactions['r'] = actions
231 typeactions['r'] = actions
233 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
232 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
234
233
235 # Fix dirstate
234 # Fix dirstate
236 for file in dropped:
235 for file in dropped:
237 dirstate.drop(file)
236 dirstate.drop(file)
238
237
239 repo.vfs.unlink('tempsparse')
238 repo.vfs.unlink('tempsparse')
240 repo._sparsesignaturecache.clear()
239 repo._sparsesignaturecache.clear()
241 msg = _('cleaned up %d temporarily added file(s) from the '
240 msg = _('cleaned up %d temporarily added file(s) from the '
242 'sparse checkout\n')
241 'sparse checkout\n')
243 repo.ui.status(msg % len(tempincludes))
242 repo.ui.status(msg % len(tempincludes))
244
243
245 def forceincludematcher(matcher, includes):
244 def forceincludematcher(matcher, includes):
246 """Returns a matcher that returns true for any of the forced includes
245 """Returns a matcher that returns true for any of the forced includes
247 before testing against the actual matcher."""
246 before testing against the actual matcher."""
248 kindpats = [('path', include, '') for include in includes]
247 kindpats = [('path', include, '') for include in includes]
249 includematcher = matchmod.includematcher('', '', kindpats)
248 includematcher = matchmod.includematcher('', '', kindpats)
250 return matchmod.unionmatcher([includematcher, matcher])
249 return matchmod.unionmatcher([includematcher, matcher])
251
250
252 def matcher(repo, revs=None, includetemp=True):
251 def matcher(repo, revs=None, includetemp=True):
253 """Obtain a matcher for sparse working directories for the given revs.
252 """Obtain a matcher for sparse working directories for the given revs.
254
253
255 If multiple revisions are specified, the matcher is the union of all
254 If multiple revisions are specified, the matcher is the union of all
256 revs.
255 revs.
257
256
258 ``includetemp`` indicates whether to use the temporary sparse profile.
257 ``includetemp`` indicates whether to use the temporary sparse profile.
259 """
258 """
260 # If sparse isn't enabled, sparse matcher matches everything.
259 # If sparse isn't enabled, sparse matcher matches everything.
261 if not enabled:
260 if not enabled:
262 return matchmod.always(repo.root, '')
261 return matchmod.always(repo.root, '')
263
262
264 if not revs or revs == [None]:
263 if not revs or revs == [None]:
265 revs = [repo.changelog.rev(node)
264 revs = [repo.changelog.rev(node)
266 for node in repo.dirstate.parents() if node != nullid]
265 for node in repo.dirstate.parents() if node != nullid]
267
266
268 signature = configsignature(repo, includetemp=includetemp)
267 signature = configsignature(repo, includetemp=includetemp)
269
268
270 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
269 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
271
270
272 result = repo._sparsematchercache.get(key)
271 result = repo._sparsematchercache.get(key)
273 if result:
272 if result:
274 return result
273 return result
275
274
276 matchers = []
275 matchers = []
277 for rev in revs:
276 for rev in revs:
278 try:
277 try:
279 includes, excludes, profiles = patternsforrev(repo, rev)
278 includes, excludes, profiles = patternsforrev(repo, rev)
280
279
281 if includes or excludes:
280 if includes or excludes:
282 # Explicitly include subdirectories of includes so
281 # Explicitly include subdirectories of includes so
283 # status will walk them down to the actual include.
282 # status will walk them down to the actual include.
284 subdirs = set()
283 subdirs = set()
285 for include in includes:
284 for include in includes:
286 # TODO consider using posix path functions here so Windows
285 # TODO consider using posix path functions here so Windows
287 # \ directory separators don't come into play.
286 # \ directory separators don't come into play.
288 dirname = os.path.dirname(include)
287 dirname = os.path.dirname(include)
289 # basename is used to avoid issues with absolute
288 # basename is used to avoid issues with absolute
290 # paths (which on Windows can include the drive).
289 # paths (which on Windows can include the drive).
291 while os.path.basename(dirname):
290 while os.path.basename(dirname):
292 subdirs.add(dirname)
291 subdirs.add(dirname)
293 dirname = os.path.dirname(dirname)
292 dirname = os.path.dirname(dirname)
294
293
295 matcher = matchmod.match(repo.root, '', [],
294 matcher = matchmod.match(repo.root, '', [],
296 include=includes, exclude=excludes,
295 include=includes, exclude=excludes,
297 default='relpath')
296 default='relpath')
298 if subdirs:
297 if subdirs:
299 matcher = forceincludematcher(matcher, subdirs)
298 matcher = forceincludematcher(matcher, subdirs)
300 matchers.append(matcher)
299 matchers.append(matcher)
301 except IOError:
300 except IOError:
302 pass
301 pass
303
302
304 if not matchers:
303 if not matchers:
305 result = matchmod.always(repo.root, '')
304 result = matchmod.always(repo.root, '')
306 elif len(matchers) == 1:
305 elif len(matchers) == 1:
307 result = matchers[0]
306 result = matchers[0]
308 else:
307 else:
309 result = matchmod.unionmatcher(matchers)
308 result = matchmod.unionmatcher(matchers)
310
309
311 if includetemp:
310 if includetemp:
312 tempincludes = readtemporaryincludes(repo)
311 tempincludes = readtemporaryincludes(repo)
313 result = forceincludematcher(result, tempincludes)
312 result = forceincludematcher(result, tempincludes)
314
313
315 repo._sparsematchercache[key] = result
314 repo._sparsematchercache[key] = result
316
315
317 return result
316 return result
318
317
319 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
318 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
320 """Filter updates to only lay out files that match the sparse rules."""
319 """Filter updates to only lay out files that match the sparse rules."""
321 if not enabled:
320 if not enabled:
322 return actions
321 return actions
323
322
324 oldrevs = [pctx.rev() for pctx in wctx.parents()]
323 oldrevs = [pctx.rev() for pctx in wctx.parents()]
325 oldsparsematch = matcher(repo, oldrevs)
324 oldsparsematch = matcher(repo, oldrevs)
326
325
327 if oldsparsematch.always():
326 if oldsparsematch.always():
328 return actions
327 return actions
329
328
330 files = set()
329 files = set()
331 prunedactions = {}
330 prunedactions = {}
332
331
333 if branchmerge:
332 if branchmerge:
334 # If we're merging, use the wctx filter, since we're merging into
333 # If we're merging, use the wctx filter, since we're merging into
335 # the wctx.
334 # the wctx.
336 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
335 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
337 else:
336 else:
338 # If we're updating, use the target context's filter, since we're
337 # If we're updating, use the target context's filter, since we're
339 # moving to the target context.
338 # moving to the target context.
340 sparsematch = matcher(repo, [mctx.rev()])
339 sparsematch = matcher(repo, [mctx.rev()])
341
340
342 temporaryfiles = []
341 temporaryfiles = []
343 for file, action in actions.iteritems():
342 for file, action in actions.iteritems():
344 type, args, msg = action
343 type, args, msg = action
345 files.add(file)
344 files.add(file)
346 if sparsematch(file):
345 if sparsematch(file):
347 prunedactions[file] = action
346 prunedactions[file] = action
348 elif type == 'm':
347 elif type == 'm':
349 temporaryfiles.append(file)
348 temporaryfiles.append(file)
350 prunedactions[file] = action
349 prunedactions[file] = action
351 elif branchmerge:
350 elif branchmerge:
352 if type != 'k':
351 if type != 'k':
353 temporaryfiles.append(file)
352 temporaryfiles.append(file)
354 prunedactions[file] = action
353 prunedactions[file] = action
355 elif type == 'f':
354 elif type == 'f':
356 prunedactions[file] = action
355 prunedactions[file] = action
357 elif file in wctx:
356 elif file in wctx:
358 prunedactions[file] = ('r', args, msg)
357 prunedactions[file] = ('r', args, msg)
359
358
360 if len(temporaryfiles) > 0:
359 if len(temporaryfiles) > 0:
361 repo.ui.status(_('temporarily included %d file(s) in the sparse '
360 repo.ui.status(_('temporarily included %d file(s) in the sparse '
362 'checkout for merging\n') % len(temporaryfiles))
361 'checkout for merging\n') % len(temporaryfiles))
363 addtemporaryincludes(repo, temporaryfiles)
362 addtemporaryincludes(repo, temporaryfiles)
364
363
365 # Add the new files to the working copy so they can be merged, etc
364 # Add the new files to the working copy so they can be merged, etc
366 actions = []
365 actions = []
367 message = 'temporarily adding to sparse checkout'
366 message = 'temporarily adding to sparse checkout'
368 wctxmanifest = repo[None].manifest()
367 wctxmanifest = repo[None].manifest()
369 for file in temporaryfiles:
368 for file in temporaryfiles:
370 if file in wctxmanifest:
369 if file in wctxmanifest:
371 fctx = repo[None][file]
370 fctx = repo[None][file]
372 actions.append((file, (fctx.flags(), False), message))
371 actions.append((file, (fctx.flags(), False), message))
373
372
374 typeactions = collections.defaultdict(list)
373 typeactions = collections.defaultdict(list)
375 typeactions['g'] = actions
374 typeactions['g'] = actions
376 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
375 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
377 False)
376 False)
378
377
379 dirstate = repo.dirstate
378 dirstate = repo.dirstate
380 for file, flags, msg in actions:
379 for file, flags, msg in actions:
381 dirstate.normal(file)
380 dirstate.normal(file)
382
381
383 profiles = activeconfig(repo)[2]
382 profiles = activeconfig(repo)[2]
384 changedprofiles = profiles & files
383 changedprofiles = profiles & files
385 # If an active profile changed during the update, refresh the checkout.
384 # If an active profile changed during the update, refresh the checkout.
386 # Don't do this during a branch merge, since all incoming changes should
385 # Don't do this during a branch merge, since all incoming changes should
387 # have been handled by the temporary includes above.
386 # have been handled by the temporary includes above.
388 if changedprofiles and not branchmerge:
387 if changedprofiles and not branchmerge:
389 mf = mctx.manifest()
388 mf = mctx.manifest()
390 for file in mf:
389 for file in mf:
391 old = oldsparsematch(file)
390 old = oldsparsematch(file)
392 new = sparsematch(file)
391 new = sparsematch(file)
393 if not old and new:
392 if not old and new:
394 flags = mf.flags(file)
393 flags = mf.flags(file)
395 prunedactions[file] = ('g', (flags, False), '')
394 prunedactions[file] = ('g', (flags, False), '')
396 elif old and not new:
395 elif old and not new:
397 prunedactions[file] = ('r', [], '')
396 prunedactions[file] = ('r', [], '')
398
397
399 return prunedactions
398 return prunedactions
400
399
401 def refreshwdir(repo, origstatus, origsparsematch, force=False):
400 def refreshwdir(repo, origstatus, origsparsematch, force=False):
402 """Refreshes working directory by taking sparse config into account.
401 """Refreshes working directory by taking sparse config into account.
403
402
404 The old status and sparse matcher is compared against the current sparse
403 The old status and sparse matcher is compared against the current sparse
405 matcher.
404 matcher.
406
405
407 Will abort if a file with pending changes is being excluded or included
406 Will abort if a file with pending changes is being excluded or included
408 unless ``force`` is True.
407 unless ``force`` is True.
409 """
408 """
410 # Verify there are no pending changes
409 # Verify there are no pending changes
411 pending = set()
410 pending = set()
412 pending.update(origstatus.modified)
411 pending.update(origstatus.modified)
413 pending.update(origstatus.added)
412 pending.update(origstatus.added)
414 pending.update(origstatus.removed)
413 pending.update(origstatus.removed)
415 sparsematch = matcher(repo)
414 sparsematch = matcher(repo)
416 abort = False
415 abort = False
417
416
418 for f in pending:
417 for f in pending:
419 if not sparsematch(f):
418 if not sparsematch(f):
420 repo.ui.warn(_("pending changes to '%s'\n") % f)
419 repo.ui.warn(_("pending changes to '%s'\n") % f)
421 abort = not force
420 abort = not force
422
421
423 if abort:
422 if abort:
424 raise error.Abort(_('could not update sparseness due to pending '
423 raise error.Abort(_('could not update sparseness due to pending '
425 'changes'))
424 'changes'))
426
425
427 # Calculate actions
426 # Calculate actions
428 dirstate = repo.dirstate
427 dirstate = repo.dirstate
429 ctx = repo['.']
428 ctx = repo['.']
430 added = []
429 added = []
431 lookup = []
430 lookup = []
432 dropped = []
431 dropped = []
433 mf = ctx.manifest()
432 mf = ctx.manifest()
434 files = set(mf)
433 files = set(mf)
435
434
436 actions = {}
435 actions = {}
437
436
438 for file in files:
437 for file in files:
439 old = origsparsematch(file)
438 old = origsparsematch(file)
440 new = sparsematch(file)
439 new = sparsematch(file)
441 # Add files that are newly included, or that don't exist in
440 # Add files that are newly included, or that don't exist in
442 # the dirstate yet.
441 # the dirstate yet.
443 if (new and not old) or (old and new and not file in dirstate):
442 if (new and not old) or (old and new and not file in dirstate):
444 fl = mf.flags(file)
443 fl = mf.flags(file)
445 if repo.wvfs.exists(file):
444 if repo.wvfs.exists(file):
446 actions[file] = ('e', (fl,), '')
445 actions[file] = ('e', (fl,), '')
447 lookup.append(file)
446 lookup.append(file)
448 else:
447 else:
449 actions[file] = ('g', (fl, False), '')
448 actions[file] = ('g', (fl, False), '')
450 added.append(file)
449 added.append(file)
451 # Drop files that are newly excluded, or that still exist in
450 # Drop files that are newly excluded, or that still exist in
452 # the dirstate.
451 # the dirstate.
453 elif (old and not new) or (not old and not new and file in dirstate):
452 elif (old and not new) or (not old and not new and file in dirstate):
454 dropped.append(file)
453 dropped.append(file)
455 if file not in pending:
454 if file not in pending:
456 actions[file] = ('r', [], '')
455 actions[file] = ('r', [], '')
457
456
458 # Verify there are no pending changes in newly included files
457 # Verify there are no pending changes in newly included files
459 abort = False
458 abort = False
460 for file in lookup:
459 for file in lookup:
461 repo.ui.warn(_("pending changes to '%s'\n") % file)
460 repo.ui.warn(_("pending changes to '%s'\n") % file)
462 abort = not force
461 abort = not force
463 if abort:
462 if abort:
464 raise error.Abort(_('cannot change sparseness due to pending '
463 raise error.Abort(_('cannot change sparseness due to pending '
465 'changes (delete the files or use '
464 'changes (delete the files or use '
466 '--force to bring them back dirty)'))
465 '--force to bring them back dirty)'))
467
466
468 # Check for files that were only in the dirstate.
467 # Check for files that were only in the dirstate.
469 for file, state in dirstate.iteritems():
468 for file, state in dirstate.iteritems():
470 if not file in files:
469 if not file in files:
471 old = origsparsematch(file)
470 old = origsparsematch(file)
472 new = sparsematch(file)
471 new = sparsematch(file)
473 if old and not new:
472 if old and not new:
474 dropped.append(file)
473 dropped.append(file)
475
474
476 # Apply changes to disk
475 # Apply changes to disk
477 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
476 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
478 for f, (m, args, msg) in actions.iteritems():
477 for f, (m, args, msg) in actions.iteritems():
479 if m not in typeactions:
478 if m not in typeactions:
480 typeactions[m] = []
479 typeactions[m] = []
481 typeactions[m].append((f, args, msg))
480 typeactions[m].append((f, args, msg))
482
481
483 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
482 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
484
483
485 # Fix dirstate
484 # Fix dirstate
486 for file in added:
485 for file in added:
487 dirstate.normal(file)
486 dirstate.normal(file)
488
487
489 for file in dropped:
488 for file in dropped:
490 dirstate.drop(file)
489 dirstate.drop(file)
491
490
492 for file in lookup:
491 for file in lookup:
493 # File exists on disk, and we're bringing it back in an unknown state.
492 # File exists on disk, and we're bringing it back in an unknown state.
494 dirstate.normallookup(file)
493 dirstate.normallookup(file)
495
494
496 return added, dropped, lookup
495 return added, dropped, lookup
497
496
498 def aftercommit(repo, node):
497 def aftercommit(repo, node):
499 """Perform actions after a working directory commit."""
498 """Perform actions after a working directory commit."""
500 # This function is called unconditionally, even if sparse isn't
499 # This function is called unconditionally, even if sparse isn't
501 # enabled.
500 # enabled.
502 ctx = repo[node]
501 ctx = repo[node]
503
502
504 profiles = patternsforrev(repo, ctx.rev())[2]
503 profiles = patternsforrev(repo, ctx.rev())[2]
505
504
506 # profiles will only have data if sparse is enabled.
505 # profiles will only have data if sparse is enabled.
507 if set(profiles) & set(ctx.files()):
506 if profiles & set(ctx.files()):
508 origstatus = repo.status()
507 origstatus = repo.status()
509 origsparsematch = matcher(repo)
508 origsparsematch = matcher(repo)
510 refreshwdir(repo, origstatus, origsparsematch, force=True)
509 refreshwdir(repo, origstatus, origsparsematch, force=True)
511
510
512 prunetemporaryincludes(repo)
511 prunetemporaryincludes(repo)
513
512
514 def clearrules(repo, force=False):
513 def clearrules(repo, force=False):
515 """Clears include/exclude rules from the sparse config.
514 """Clears include/exclude rules from the sparse config.
516
515
517 The remaining sparse config only has profiles, if defined. The working
516 The remaining sparse config only has profiles, if defined. The working
518 directory is refreshed, as needed.
517 directory is refreshed, as needed.
519 """
518 """
520 with repo.wlock():
519 with repo.wlock():
521 raw = repo.vfs.tryread('sparse')
520 raw = repo.vfs.tryread('sparse')
522 includes, excludes, profiles = parseconfig(repo.ui, raw)
521 includes, excludes, profiles = parseconfig(repo.ui, raw)
523
522
524 if not includes and not excludes:
523 if not includes and not excludes:
525 return
524 return
526
525
527 oldstatus = repo.status()
526 oldstatus = repo.status()
528 oldmatch = matcher(repo)
527 oldmatch = matcher(repo)
529 writeconfig(repo, set(), set(), profiles)
528 writeconfig(repo, set(), set(), profiles)
530 refreshwdir(repo, oldstatus, oldmatch, force=force)
529 refreshwdir(repo, oldstatus, oldmatch, force=force)
531
530
532 def importfromfiles(repo, opts, paths, force=False):
531 def importfromfiles(repo, opts, paths, force=False):
533 """Import sparse config rules from files.
532 """Import sparse config rules from files.
534
533
535 The updated sparse config is written out and the working directory
534 The updated sparse config is written out and the working directory
536 is refreshed, as needed.
535 is refreshed, as needed.
537 """
536 """
538 with repo.wlock():
537 with repo.wlock():
539 # read current configuration
538 # read current configuration
540 raw = repo.vfs.tryread('sparse')
539 raw = repo.vfs.tryread('sparse')
541 oincludes, oexcludes, oprofiles = parseconfig(repo.ui, raw)
540 oincludes, oexcludes, oprofiles = parseconfig(repo.ui, raw)
542 includes, excludes, profiles = map(
541 includes, excludes, profiles = map(
543 set, (oincludes, oexcludes, oprofiles))
542 set, (oincludes, oexcludes, oprofiles))
544
543
545 aincludes, aexcludes, aprofiles = activeconfig(repo)
544 aincludes, aexcludes, aprofiles = activeconfig(repo)
546
545
547 # Import rules on top; only take in rules that are not yet
546 # Import rules on top; only take in rules that are not yet
548 # part of the active rules.
547 # part of the active rules.
549 changed = False
548 changed = False
550 for p in paths:
549 for p in paths:
551 with util.posixfile(util.expandpath(p)) as fh:
550 with util.posixfile(util.expandpath(p)) as fh:
552 raw = fh.read()
551 raw = fh.read()
553
552
554 iincludes, iexcludes, iprofiles = parseconfig(repo.ui, raw)
553 iincludes, iexcludes, iprofiles = parseconfig(repo.ui, raw)
555 oldsize = len(includes) + len(excludes) + len(profiles)
554 oldsize = len(includes) + len(excludes) + len(profiles)
556 includes.update(iincludes - aincludes)
555 includes.update(iincludes - aincludes)
557 excludes.update(iexcludes - aexcludes)
556 excludes.update(iexcludes - aexcludes)
558 profiles.update(set(iprofiles) - aprofiles)
557 profiles.update(iprofiles - aprofiles)
559 if len(includes) + len(excludes) + len(profiles) > oldsize:
558 if len(includes) + len(excludes) + len(profiles) > oldsize:
560 changed = True
559 changed = True
561
560
562 profilecount = includecount = excludecount = 0
561 profilecount = includecount = excludecount = 0
563 fcounts = (0, 0, 0)
562 fcounts = (0, 0, 0)
564
563
565 if changed:
564 if changed:
566 profilecount = len(profiles - aprofiles)
565 profilecount = len(profiles - aprofiles)
567 includecount = len(includes - aincludes)
566 includecount = len(includes - aincludes)
568 excludecount = len(excludes - aexcludes)
567 excludecount = len(excludes - aexcludes)
569
568
570 oldstatus = repo.status()
569 oldstatus = repo.status()
571 oldsparsematch = matcher(repo)
570 oldsparsematch = matcher(repo)
572
571
573 # TODO remove this try..except once the matcher integrates better
572 # TODO remove this try..except once the matcher integrates better
574 # with dirstate. We currently have to write the updated config
573 # with dirstate. We currently have to write the updated config
575 # because that will invalidate the matcher cache and force a
574 # because that will invalidate the matcher cache and force a
576 # re-read. We ideally want to update the cached matcher on the
575 # re-read. We ideally want to update the cached matcher on the
577 # repo instance then flush the new config to disk once wdir is
576 # repo instance then flush the new config to disk once wdir is
578 # updated. But this requires massive rework to matcher() and its
577 # updated. But this requires massive rework to matcher() and its
579 # consumers.
578 # consumers.
580 writeconfig(repo, includes, excludes, profiles)
579 writeconfig(repo, includes, excludes, profiles)
581
580
582 try:
581 try:
583 fcounts = map(
582 fcounts = map(
584 len,
583 len,
585 refreshwdir(repo, oldstatus, oldsparsematch, force=force))
584 refreshwdir(repo, oldstatus, oldsparsematch, force=force))
586 except Exception:
585 except Exception:
587 writeconfig(repo, oincludes, oexcludes, oprofiles)
586 writeconfig(repo, oincludes, oexcludes, oprofiles)
588 raise
587 raise
589
588
590 printchanges(repo.ui, opts, profilecount, includecount, excludecount,
589 printchanges(repo.ui, opts, profilecount, includecount, excludecount,
591 *fcounts)
590 *fcounts)
592
591
593 def updateconfig(repo, pats, opts, include=False, exclude=False, reset=False,
592 def updateconfig(repo, pats, opts, include=False, exclude=False, reset=False,
594 delete=False, enableprofile=False, disableprofile=False,
593 delete=False, enableprofile=False, disableprofile=False,
595 force=False):
594 force=False):
596 """Perform a sparse config update.
595 """Perform a sparse config update.
597
596
598 Only one of the actions may be performed.
597 Only one of the actions may be performed.
599
598
600 The new config is written out and a working directory refresh is performed.
599 The new config is written out and a working directory refresh is performed.
601 """
600 """
602 with repo.wlock():
601 with repo.wlock():
603 oldmatcher = matcher(repo)
602 oldmatcher = matcher(repo)
604
603
605 raw = repo.vfs.tryread('sparse')
604 raw = repo.vfs.tryread('sparse')
606 oldinclude, oldexclude, oldprofiles = parseconfig(repo.ui, raw)
605 oldinclude, oldexclude, oldprofiles = parseconfig(repo.ui, raw)
607 oldprofiles = set(oldprofiles)
608
606
609 if reset:
607 if reset:
610 newinclude = set()
608 newinclude = set()
611 newexclude = set()
609 newexclude = set()
612 newprofiles = set()
610 newprofiles = set()
613 else:
611 else:
614 newinclude = set(oldinclude)
612 newinclude = set(oldinclude)
615 newexclude = set(oldexclude)
613 newexclude = set(oldexclude)
616 newprofiles = set(oldprofiles)
614 newprofiles = set(oldprofiles)
617
615
618 oldstatus = repo.status()
616 oldstatus = repo.status()
619
617
620 if any(pat.startswith('/') for pat in pats):
618 if any(pat.startswith('/') for pat in pats):
621 repo.ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
619 repo.ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
622 % ([pat for pat in pats if pat.startswith('/')]))
620 % ([pat for pat in pats if pat.startswith('/')]))
623 elif include:
621 elif include:
624 newinclude.update(pats)
622 newinclude.update(pats)
625 elif exclude:
623 elif exclude:
626 newexclude.update(pats)
624 newexclude.update(pats)
627 elif enableprofile:
625 elif enableprofile:
628 newprofiles.update(pats)
626 newprofiles.update(pats)
629 elif disableprofile:
627 elif disableprofile:
630 newprofiles.difference_update(pats)
628 newprofiles.difference_update(pats)
631 elif delete:
629 elif delete:
632 newinclude.difference_update(pats)
630 newinclude.difference_update(pats)
633 newexclude.difference_update(pats)
631 newexclude.difference_update(pats)
634
632
635 profilecount = (len(newprofiles - oldprofiles) -
633 profilecount = (len(newprofiles - oldprofiles) -
636 len(oldprofiles - newprofiles))
634 len(oldprofiles - newprofiles))
637 includecount = (len(newinclude - oldinclude) -
635 includecount = (len(newinclude - oldinclude) -
638 len(oldinclude - newinclude))
636 len(oldinclude - newinclude))
639 excludecount = (len(newexclude - oldexclude) -
637 excludecount = (len(newexclude - oldexclude) -
640 len(oldexclude - newexclude))
638 len(oldexclude - newexclude))
641
639
642 # TODO clean up this writeconfig() + try..except pattern once we can.
640 # TODO clean up this writeconfig() + try..except pattern once we can.
643 # See comment in importfromfiles() explaining it.
641 # See comment in importfromfiles() explaining it.
644 writeconfig(repo, newinclude, newexclude, newprofiles)
642 writeconfig(repo, newinclude, newexclude, newprofiles)
645
643
646 try:
644 try:
647 fcounts = map(
645 fcounts = map(
648 len,
646 len,
649 refreshwdir(repo, oldstatus, oldmatcher, force=force))
647 refreshwdir(repo, oldstatus, oldmatcher, force=force))
650
648
651 printchanges(repo.ui, opts, profilecount, includecount,
649 printchanges(repo.ui, opts, profilecount, includecount,
652 excludecount, *fcounts)
650 excludecount, *fcounts)
653 except Exception:
651 except Exception:
654 writeconfig(repo, oldinclude, oldexclude, oldprofiles)
652 writeconfig(repo, oldinclude, oldexclude, oldprofiles)
655 raise
653 raise
656
654
657 def printchanges(ui, opts, profilecount=0, includecount=0, excludecount=0,
655 def printchanges(ui, opts, profilecount=0, includecount=0, excludecount=0,
658 added=0, dropped=0, conflicting=0):
656 added=0, dropped=0, conflicting=0):
659 """Print output summarizing sparse config changes."""
657 """Print output summarizing sparse config changes."""
660 with ui.formatter('sparse', opts) as fm:
658 with ui.formatter('sparse', opts) as fm:
661 fm.startitem()
659 fm.startitem()
662 fm.condwrite(ui.verbose, 'profiles_added', _('Profiles changed: %d\n'),
660 fm.condwrite(ui.verbose, 'profiles_added', _('Profiles changed: %d\n'),
663 profilecount)
661 profilecount)
664 fm.condwrite(ui.verbose, 'include_rules_added',
662 fm.condwrite(ui.verbose, 'include_rules_added',
665 _('Include rules changed: %d\n'), includecount)
663 _('Include rules changed: %d\n'), includecount)
666 fm.condwrite(ui.verbose, 'exclude_rules_added',
664 fm.condwrite(ui.verbose, 'exclude_rules_added',
667 _('Exclude rules changed: %d\n'), excludecount)
665 _('Exclude rules changed: %d\n'), excludecount)
668
666
669 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
667 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
670 # files are added or removed outside of the templating formatter
668 # files are added or removed outside of the templating formatter
671 # framework. No point in repeating ourselves in that case.
669 # framework. No point in repeating ourselves in that case.
672 if not fm.isplain():
670 if not fm.isplain():
673 fm.condwrite(ui.verbose, 'files_added', _('Files added: %d\n'),
671 fm.condwrite(ui.verbose, 'files_added', _('Files added: %d\n'),
674 added)
672 added)
675 fm.condwrite(ui.verbose, 'files_dropped', _('Files dropped: %d\n'),
673 fm.condwrite(ui.verbose, 'files_dropped', _('Files dropped: %d\n'),
676 dropped)
674 dropped)
677 fm.condwrite(ui.verbose, 'files_conflicting',
675 fm.condwrite(ui.verbose, 'files_conflicting',
678 _('Files conflicting: %d\n'), conflicting)
676 _('Files conflicting: %d\n'), conflicting)
General Comments 0
You need to be logged in to leave comments. Login now