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