##// END OF EJS Templates
narrow: also warn when not deleting untracked or ignored files...
Martin von Zweigbergk -
r42347:aa8f8392 default draft
parent child Browse files
Show More
@@ -1,318 +1,322 b''
1 1 # narrowspec.py - methods for working with a narrow view of a repository
2 2 #
3 3 # Copyright 2017 Google, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 match as matchmod,
16 16 merge,
17 17 repository,
18 18 scmutil,
19 19 sparse,
20 20 util,
21 21 )
22 22
23 23 # The file in .hg/store/ that indicates which paths exit in the store
24 24 FILENAME = 'narrowspec'
25 25 # The file in .hg/ that indicates which paths exit in the dirstate
26 26 DIRSTATE_FILENAME = 'narrowspec.dirstate'
27 27
28 28 # Pattern prefixes that are allowed in narrow patterns. This list MUST
29 29 # only contain patterns that are fast and safe to evaluate. Keep in mind
30 30 # that patterns are supplied by clients and executed on remote servers
31 31 # as part of wire protocol commands. That means that changes to this
32 32 # data structure influence the wire protocol and should not be taken
33 33 # lightly - especially removals.
34 34 VALID_PREFIXES = (
35 35 b'path:',
36 36 b'rootfilesin:',
37 37 )
38 38
39 39 def normalizesplitpattern(kind, pat):
40 40 """Returns the normalized version of a pattern and kind.
41 41
42 42 Returns a tuple with the normalized kind and normalized pattern.
43 43 """
44 44 pat = pat.rstrip('/')
45 45 _validatepattern(pat)
46 46 return kind, pat
47 47
48 48 def _numlines(s):
49 49 """Returns the number of lines in s, including ending empty lines."""
50 50 # We use splitlines because it is Unicode-friendly and thus Python 3
51 51 # compatible. However, it does not count empty lines at the end, so trick
52 52 # it by adding a character at the end.
53 53 return len((s + 'x').splitlines())
54 54
55 55 def _validatepattern(pat):
56 56 """Validates the pattern and aborts if it is invalid.
57 57
58 58 Patterns are stored in the narrowspec as newline-separated
59 59 POSIX-style bytestring paths. There's no escaping.
60 60 """
61 61
62 62 # We use newlines as separators in the narrowspec file, so don't allow them
63 63 # in patterns.
64 64 if _numlines(pat) > 1:
65 65 raise error.Abort(_('newlines are not allowed in narrowspec paths'))
66 66
67 67 components = pat.split('/')
68 68 if '.' in components or '..' in components:
69 69 raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
70 70
71 71 def normalizepattern(pattern, defaultkind='path'):
72 72 """Returns the normalized version of a text-format pattern.
73 73
74 74 If the pattern has no kind, the default will be added.
75 75 """
76 76 kind, pat = matchmod._patsplit(pattern, defaultkind)
77 77 return '%s:%s' % normalizesplitpattern(kind, pat)
78 78
79 79 def parsepatterns(pats):
80 80 """Parses an iterable of patterns into a typed pattern set.
81 81
82 82 Patterns are assumed to be ``path:`` if no prefix is present.
83 83 For safety and performance reasons, only some prefixes are allowed.
84 84 See ``validatepatterns()``.
85 85
86 86 This function should be used on patterns that come from the user to
87 87 normalize and validate them to the internal data structure used for
88 88 representing patterns.
89 89 """
90 90 res = {normalizepattern(orig) for orig in pats}
91 91 validatepatterns(res)
92 92 return res
93 93
94 94 def validatepatterns(pats):
95 95 """Validate that patterns are in the expected data structure and format.
96 96
97 97 And that is a set of normalized patterns beginning with ``path:`` or
98 98 ``rootfilesin:``.
99 99
100 100 This function should be used to validate internal data structures
101 101 and patterns that are loaded from sources that use the internal,
102 102 prefixed pattern representation (but can't necessarily be fully trusted).
103 103 """
104 104 if not isinstance(pats, set):
105 105 raise error.ProgrammingError('narrow patterns should be a set; '
106 106 'got %r' % pats)
107 107
108 108 for pat in pats:
109 109 if not pat.startswith(VALID_PREFIXES):
110 110 # Use a Mercurial exception because this can happen due to user
111 111 # bugs (e.g. manually updating spec file).
112 112 raise error.Abort(_('invalid prefix on narrow pattern: %s') % pat,
113 113 hint=_('narrow patterns must begin with one of '
114 114 'the following: %s') %
115 115 ', '.join(VALID_PREFIXES))
116 116
117 117 def format(includes, excludes):
118 118 output = '[include]\n'
119 119 for i in sorted(includes - excludes):
120 120 output += i + '\n'
121 121 output += '[exclude]\n'
122 122 for e in sorted(excludes):
123 123 output += e + '\n'
124 124 return output
125 125
126 126 def match(root, include=None, exclude=None):
127 127 if not include:
128 128 # Passing empty include and empty exclude to matchmod.match()
129 129 # gives a matcher that matches everything, so explicitly use
130 130 # the nevermatcher.
131 131 return matchmod.never()
132 132 return matchmod.match(root, '', [], include=include or [],
133 133 exclude=exclude or [])
134 134
135 135 def parseconfig(ui, spec):
136 136 # maybe we should care about the profiles returned too
137 137 includepats, excludepats, profiles = sparse.parseconfig(ui, spec, 'narrow')
138 138 if profiles:
139 139 raise error.Abort(_("including other spec files using '%include' is not"
140 140 " supported in narrowspec"))
141 141
142 142 validatepatterns(includepats)
143 143 validatepatterns(excludepats)
144 144
145 145 return includepats, excludepats
146 146
147 147 def load(repo):
148 148 try:
149 149 spec = repo.svfs.read(FILENAME)
150 150 except IOError as e:
151 151 # Treat "narrowspec does not exist" the same as "narrowspec file exists
152 152 # and is empty".
153 153 if e.errno == errno.ENOENT:
154 154 return set(), set()
155 155 raise
156 156
157 157 return parseconfig(repo.ui, spec)
158 158
159 159 def save(repo, includepats, excludepats):
160 160 validatepatterns(includepats)
161 161 validatepatterns(excludepats)
162 162 spec = format(includepats, excludepats)
163 163 repo.svfs.write(FILENAME, spec)
164 164
165 165 def copytoworkingcopy(repo):
166 166 spec = repo.svfs.read(FILENAME)
167 167 repo.vfs.write(DIRSTATE_FILENAME, spec)
168 168
169 169 def savebackup(repo, backupname):
170 170 if repository.NARROW_REQUIREMENT not in repo.requirements:
171 171 return
172 172 svfs = repo.svfs
173 173 svfs.tryunlink(backupname)
174 174 util.copyfile(svfs.join(FILENAME), svfs.join(backupname), hardlink=True)
175 175
176 176 def restorebackup(repo, backupname):
177 177 if repository.NARROW_REQUIREMENT not in repo.requirements:
178 178 return
179 179 util.rename(repo.svfs.join(backupname), repo.svfs.join(FILENAME))
180 180
181 181 def savewcbackup(repo, backupname):
182 182 if repository.NARROW_REQUIREMENT not in repo.requirements:
183 183 return
184 184 vfs = repo.vfs
185 185 vfs.tryunlink(backupname)
186 186 # It may not exist in old repos
187 187 if vfs.exists(DIRSTATE_FILENAME):
188 188 util.copyfile(vfs.join(DIRSTATE_FILENAME), vfs.join(backupname),
189 189 hardlink=True)
190 190
191 191 def restorewcbackup(repo, backupname):
192 192 if repository.NARROW_REQUIREMENT not in repo.requirements:
193 193 return
194 194 # It may not exist in old repos
195 195 if repo.vfs.exists(backupname):
196 196 util.rename(repo.vfs.join(backupname), repo.vfs.join(DIRSTATE_FILENAME))
197 197
198 198 def clearwcbackup(repo, backupname):
199 199 if repository.NARROW_REQUIREMENT not in repo.requirements:
200 200 return
201 201 repo.vfs.tryunlink(backupname)
202 202
203 203 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
204 204 r""" Restricts the patterns according to repo settings,
205 205 results in a logical AND operation
206 206
207 207 :param req_includes: requested includes
208 208 :param req_excludes: requested excludes
209 209 :param repo_includes: repo includes
210 210 :param repo_excludes: repo excludes
211 211 :return: include patterns, exclude patterns, and invalid include patterns.
212 212
213 213 >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
214 214 (set(['f1']), {}, [])
215 215 >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
216 216 (set(['f1']), {}, [])
217 217 >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
218 218 (set(['f1/fc1']), {}, [])
219 219 >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
220 220 ([], set(['path:.']), [])
221 221 >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
222 222 (set(['f2/fc2']), {}, [])
223 223 >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
224 224 ([], set(['path:.']), [])
225 225 >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
226 226 (set(['f1/$non_exitent_var']), {}, [])
227 227 """
228 228 res_excludes = set(req_excludes)
229 229 res_excludes.update(repo_excludes)
230 230 invalid_includes = []
231 231 if not req_includes:
232 232 res_includes = set(repo_includes)
233 233 elif 'path:.' not in repo_includes:
234 234 res_includes = []
235 235 for req_include in req_includes:
236 236 req_include = util.expandpath(util.normpath(req_include))
237 237 if req_include in repo_includes:
238 238 res_includes.append(req_include)
239 239 continue
240 240 valid = False
241 241 for repo_include in repo_includes:
242 242 if req_include.startswith(repo_include + '/'):
243 243 valid = True
244 244 res_includes.append(req_include)
245 245 break
246 246 if not valid:
247 247 invalid_includes.append(req_include)
248 248 if len(res_includes) == 0:
249 249 res_excludes = {'path:.'}
250 250 else:
251 251 res_includes = set(res_includes)
252 252 else:
253 253 res_includes = set(req_includes)
254 254 return res_includes, res_excludes, invalid_includes
255 255
256 256 # These two are extracted for extensions (specifically for Google's CitC file
257 257 # system)
258 258 def _deletecleanfiles(repo, files):
259 259 for f in files:
260 260 repo.wvfs.unlinkpath(f)
261 261
262 262 def _writeaddedfiles(repo, pctx, files):
263 263 actions = merge.emptyactions()
264 264 addgaction = actions[merge.ACTION_GET].append
265 265 mf = repo['.'].manifest()
266 266 for f in files:
267 267 if not repo.wvfs.exists(f):
268 268 addgaction((f, (mf.flags(f), False), "narrowspec updated"))
269 269 merge.applyupdates(repo, actions, wctx=repo[None],
270 270 mctx=repo['.'], overwrite=False)
271 271
272 272 def checkworkingcopynarrowspec(repo):
273 273 storespec = repo.svfs.tryread(FILENAME)
274 274 wcspec = repo.vfs.tryread(DIRSTATE_FILENAME)
275 275 if wcspec != storespec:
276 276 raise error.Abort(_("working copy's narrowspec is stale"),
277 277 hint=_("run 'hg tracked --update-working-copy'"))
278 278
279 279 def updateworkingcopy(repo, assumeclean=False):
280 280 """updates the working copy and dirstate from the store narrowspec
281 281
282 282 When assumeclean=True, files that are not known to be clean will also
283 283 be deleted. It is then up to the caller to make sure they are clean.
284 284 """
285 285 oldspec = repo.vfs.tryread(DIRSTATE_FILENAME)
286 286 newspec = repo.svfs.tryread(FILENAME)
287 287
288 288 oldincludes, oldexcludes = parseconfig(repo.ui, oldspec)
289 289 newincludes, newexcludes = parseconfig(repo.ui, newspec)
290 290 oldmatch = match(repo.root, include=oldincludes, exclude=oldexcludes)
291 291 newmatch = match(repo.root, include=newincludes, exclude=newexcludes)
292 292 addedmatch = matchmod.differencematcher(newmatch, oldmatch)
293 293 removedmatch = matchmod.differencematcher(oldmatch, newmatch)
294 294
295 295 ds = repo.dirstate
296 lookup, status = ds.status(removedmatch, subrepos=[], ignored=False,
297 clean=True, unknown=False)
296 lookup, status = ds.status(removedmatch, subrepos=[], ignored=True,
297 clean=True, unknown=True)
298 298 trackeddirty = status.modified + status.added
299 299 clean = status.clean
300 300 if assumeclean:
301 301 assert not trackeddirty
302 302 clean.extend(lookup)
303 303 else:
304 304 trackeddirty.extend(lookup)
305 305 _deletecleanfiles(repo, clean)
306 306 uipathfn = scmutil.getuipathfn(repo)
307 307 for f in sorted(trackeddirty):
308 308 repo.ui.status(_('not deleting possibly dirty file %s\n') % uipathfn(f))
309 for f in sorted(status.unknown):
310 repo.ui.status(_('not deleting unknown file %s\n') % uipathfn(f))
311 for f in sorted(status.ignored):
312 repo.ui.status(_('not deleting ignored file %s\n') % uipathfn(f))
309 313 for f in clean + trackeddirty:
310 314 ds.drop(f)
311 315
312 316 repo.narrowpats = newincludes, newexcludes
313 317 repo._narrowmatch = newmatch
314 318 pctx = repo['.']
315 319 newfiles = [f for f in pctx.manifest().walk(addedmatch) if f not in ds]
316 320 for f in newfiles:
317 321 ds.normallookup(f)
318 322 _writeaddedfiles(repo, pctx, newfiles)
@@ -1,178 +1,191 b''
1 1 #testcases flat tree
2 2
3 3 $ . "$TESTDIR/narrow-library.sh"
4 4
5 5 #if tree
6 6 $ cat << EOF >> $HGRCPATH
7 7 > [experimental]
8 8 > treemanifest = 1
9 9 > EOF
10 10 #endif
11 11
12 12 $ cat << EOF >> $HGRCPATH
13 13 > [extensions]
14 14 > share =
15 15 > EOF
16 16
17 17 $ hg init remote
18 18 $ cd remote
19 19 $ for x in `$TESTDIR/seq.py 0 10`
20 20 > do
21 21 > mkdir d$x
22 22 > echo $x > d$x/f
23 23 > hg add d$x/f
24 24 > hg commit -m "add d$x/f"
25 25 > done
26 26 $ cd ..
27 27
28 28 $ hg clone --narrow ssh://user@dummy/remote main -q \
29 29 > --include d1 --include d3 --include d5 --include d7
30 30
31 Ignore file called "ignored"
32 $ echo ignored > main/.hgignore
33
31 34 $ hg share main share
32 35 updating working directory
33 36 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 37 $ hg -R share tracked
35 38 I path:d1
36 39 I path:d3
37 40 I path:d5
38 41 I path:d7
39 42 $ hg -R share files
40 43 share/d1/f
41 44 share/d3/f
42 45 share/d5/f
43 46 share/d7/f
44 47
45 48 Narrow the share and check that the main repo's working copy gets updated
46 49
47 50 # Make sure the files that are supposed to be known-clean get their timestamps set in the dirstate
48 51 $ sleep 2
49 52 $ hg -R main st
50 53 $ hg -R main debugdirstate --no-dates
51 54 n 644 2 set d1/f
52 55 n 644 2 set d3/f
53 56 n 644 2 set d5/f
54 57 n 644 2 set d7/f
55 58 # Make d3/f dirty
56 59 $ echo x >> main/d3/f
57 60 $ echo y >> main/d3/g
61 $ touch main/d3/ignored
62 $ touch main/d3/untracked
58 63 $ hg add main/d3/g
59 64 $ hg -R main st
60 65 M d3/f
61 66 A d3/g
67 ? d3/untracked
62 68 # Make d5/f not match the dirstate timestamp even though it's clean
63 69 $ sleep 2
64 70 $ hg -R main st
65 71 M d3/f
66 72 A d3/g
73 ? d3/untracked
67 74 $ hg -R main debugdirstate --no-dates
68 75 n 644 2 set d1/f
69 76 n 644 2 set d3/f
70 77 a 0 -1 unset d3/g
71 78 n 644 2 set d5/f
72 79 n 644 2 set d7/f
73 80 $ touch main/d5/f
74 81 $ hg -R share tracked --removeinclude d1 --removeinclude d3 --removeinclude d5
75 82 comparing with ssh://user@dummy/remote
76 83 searching for changes
77 84 looking for local changes to affected paths
78 85 deleting data/d1/f.i
79 86 deleting data/d3/f.i
80 87 deleting data/d5/f.i
81 88 deleting meta/d1/00manifest.i (tree !)
82 89 deleting meta/d3/00manifest.i (tree !)
83 90 deleting meta/d5/00manifest.i (tree !)
84 91 $ hg -R main tracked
85 92 I path:d7
86 93 $ hg -R main files
87 94 abort: working copy's narrowspec is stale
88 95 (run 'hg tracked --update-working-copy')
89 96 [255]
90 97 $ hg -R main tracked --update-working-copy
91 98 not deleting possibly dirty file d3/f
92 99 not deleting possibly dirty file d3/g
93 100 not deleting possibly dirty file d5/f
101 not deleting unknown file d3/untracked
102 not deleting ignored file d3/ignored
94 103 # d1/f, d3/f, d3/g and d5/f should no longer be reported
95 104 $ hg -R main files
96 105 main/d7/f
97 106 # d1/f should no longer be there, d3/f should be since it was dirty, d3/g should be there since
98 107 # it was added, and d5/f should be since we couldn't be sure it was clean
99 108 $ find main/d* -type f | sort
100 109 main/d3/f
101 110 main/d3/g
111 main/d3/ignored
112 main/d3/untracked
102 113 main/d5/f
103 114 main/d7/f
104 115
105 116 Widen the share and check that the main repo's working copy gets updated
106 117
107 118 $ hg -R share tracked --addinclude d1 --addinclude d3 -q
108 119 $ hg -R share tracked
109 120 I path:d1
110 121 I path:d3
111 122 I path:d7
112 123 $ hg -R share files
113 124 share/d1/f
114 125 share/d3/f
115 126 share/d7/f
116 127 $ hg -R main tracked
117 128 I path:d1
118 129 I path:d3
119 130 I path:d7
120 131 $ hg -R main files
121 132 abort: working copy's narrowspec is stale
122 133 (run 'hg tracked --update-working-copy')
123 134 [255]
124 135 $ hg -R main tracked --update-working-copy
125 136 # d1/f, d3/f should be back
126 137 $ hg -R main files
127 138 main/d1/f
128 139 main/d3/f
129 140 main/d7/f
130 141 # d3/f should be modified (not clobbered by the widening), and d3/g should be untracked
131 142 $ hg -R main st --all
132 143 M d3/f
133 144 ? d3/g
145 ? d3/untracked
146 I d3/ignored
134 147 C d1/f
135 148 C d7/f
136 149
137 150 We should also be able to unshare without breaking everything:
138 151
139 152 $ hg share main share-unshare
140 153 updating working directory
141 154 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
142 155 $ cd share-unshare
143 156 $ hg unshare
144 157 $ hg verify
145 158 checking changesets
146 159 checking manifests
147 160 checking directory manifests (tree !)
148 161 crosschecking files in changesets and manifests
149 162 checking files
150 163 checked 11 changesets with 3 changes to 3 files
151 164 $ cd ..
152 165
153 166 Dirstate should be left alone when upgrading from version of hg that didn't support narrow+share
154 167
155 168 $ hg share main share-upgrade
156 169 updating working directory
157 170 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
158 171 $ cd share-upgrade
159 172 $ echo x >> d1/f
160 173 $ echo y >> d3/g
161 174 $ hg add d3/g
162 175 $ hg rm d7/f
163 176 $ hg st
164 177 M d1/f
165 178 A d3/g
166 179 R d7/f
167 180 Make it look like a repo from before narrow+share was supported
168 181 $ rm .hg/narrowspec.dirstate
169 182 $ hg ci -Am test
170 183 abort: working copy's narrowspec is stale
171 184 (run 'hg tracked --update-working-copy')
172 185 [255]
173 186 $ hg tracked --update-working-copy
174 187 $ hg st
175 188 M d1/f
176 189 A d3/g
177 190 R d7/f
178 191 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now