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