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