##// END OF EJS Templates
dirstate: distinct transaction callback from largefile...
marmoute -
r51018:4e95341c default
parent child Browse files
Show More
@@ -1,819 +1,820 b''
1 # Copyright 2009-2010 Gregory P. Ward
1 # Copyright 2009-2010 Gregory P. Ward
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 # Copyright 2010-2011 Fog Creek Software
3 # Copyright 2010-2011 Fog Creek Software
4 # Copyright 2010-2011 Unity Technologies
4 # Copyright 2010-2011 Unity Technologies
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''largefiles utility code: must not import other modules in this package.'''
9 '''largefiles utility code: must not import other modules in this package.'''
10
10
11 import contextlib
11 import contextlib
12 import copy
12 import copy
13 import os
13 import os
14 import stat
14 import stat
15
15
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17 from mercurial.node import hex
17 from mercurial.node import hex
18 from mercurial.pycompat import open
18 from mercurial.pycompat import open
19
19
20 from mercurial import (
20 from mercurial import (
21 dirstate,
21 dirstate,
22 encoding,
22 encoding,
23 error,
23 error,
24 httpconnection,
24 httpconnection,
25 match as matchmod,
25 match as matchmod,
26 pycompat,
26 pycompat,
27 requirements,
27 requirements,
28 scmutil,
28 scmutil,
29 sparse,
29 sparse,
30 util,
30 util,
31 vfs as vfsmod,
31 vfs as vfsmod,
32 )
32 )
33 from mercurial.utils import hashutil
33 from mercurial.utils import hashutil
34 from mercurial.dirstateutils import timestamp
34 from mercurial.dirstateutils import timestamp
35
35
36 shortname = b'.hglf'
36 shortname = b'.hglf'
37 shortnameslash = shortname + b'/'
37 shortnameslash = shortname + b'/'
38 longname = b'largefiles'
38 longname = b'largefiles'
39
39
40 # -- Private worker functions ------------------------------------------
40 # -- Private worker functions ------------------------------------------
41
41
42
42
43 @contextlib.contextmanager
43 @contextlib.contextmanager
44 def lfstatus(repo, value=True):
44 def lfstatus(repo, value=True):
45 oldvalue = getattr(repo, 'lfstatus', False)
45 oldvalue = getattr(repo, 'lfstatus', False)
46 repo.lfstatus = value
46 repo.lfstatus = value
47 try:
47 try:
48 yield
48 yield
49 finally:
49 finally:
50 repo.lfstatus = oldvalue
50 repo.lfstatus = oldvalue
51
51
52
52
53 def getminsize(ui, assumelfiles, opt, default=10):
53 def getminsize(ui, assumelfiles, opt, default=10):
54 lfsize = opt
54 lfsize = opt
55 if not lfsize and assumelfiles:
55 if not lfsize and assumelfiles:
56 lfsize = ui.config(longname, b'minsize', default=default)
56 lfsize = ui.config(longname, b'minsize', default=default)
57 if lfsize:
57 if lfsize:
58 try:
58 try:
59 lfsize = float(lfsize)
59 lfsize = float(lfsize)
60 except ValueError:
60 except ValueError:
61 raise error.Abort(
61 raise error.Abort(
62 _(b'largefiles: size must be number (not %s)\n') % lfsize
62 _(b'largefiles: size must be number (not %s)\n') % lfsize
63 )
63 )
64 if lfsize is None:
64 if lfsize is None:
65 raise error.Abort(_(b'minimum size for largefiles must be specified'))
65 raise error.Abort(_(b'minimum size for largefiles must be specified'))
66 return lfsize
66 return lfsize
67
67
68
68
69 def link(src, dest):
69 def link(src, dest):
70 """Try to create hardlink - if that fails, efficiently make a copy."""
70 """Try to create hardlink - if that fails, efficiently make a copy."""
71 util.makedirs(os.path.dirname(dest))
71 util.makedirs(os.path.dirname(dest))
72 try:
72 try:
73 util.oslink(src, dest)
73 util.oslink(src, dest)
74 except OSError:
74 except OSError:
75 # if hardlinks fail, fallback on atomic copy
75 # if hardlinks fail, fallback on atomic copy
76 with open(src, b'rb') as srcf, util.atomictempfile(dest) as dstf:
76 with open(src, b'rb') as srcf, util.atomictempfile(dest) as dstf:
77 for chunk in util.filechunkiter(srcf):
77 for chunk in util.filechunkiter(srcf):
78 dstf.write(chunk)
78 dstf.write(chunk)
79 os.chmod(dest, os.stat(src).st_mode)
79 os.chmod(dest, os.stat(src).st_mode)
80
80
81
81
82 def usercachepath(ui, hash):
82 def usercachepath(ui, hash):
83 """Return the correct location in the "global" largefiles cache for a file
83 """Return the correct location in the "global" largefiles cache for a file
84 with the given hash.
84 with the given hash.
85 This cache is used for sharing of largefiles across repositories - both
85 This cache is used for sharing of largefiles across repositories - both
86 to preserve download bandwidth and storage space."""
86 to preserve download bandwidth and storage space."""
87 return os.path.join(_usercachedir(ui), hash)
87 return os.path.join(_usercachedir(ui), hash)
88
88
89
89
90 def _usercachedir(ui, name=longname):
90 def _usercachedir(ui, name=longname):
91 '''Return the location of the "global" largefiles cache.'''
91 '''Return the location of the "global" largefiles cache.'''
92 path = ui.configpath(name, b'usercache')
92 path = ui.configpath(name, b'usercache')
93 if path:
93 if path:
94 return path
94 return path
95
95
96 hint = None
96 hint = None
97
97
98 if pycompat.iswindows:
98 if pycompat.iswindows:
99 appdata = encoding.environ.get(
99 appdata = encoding.environ.get(
100 b'LOCALAPPDATA', encoding.environ.get(b'APPDATA')
100 b'LOCALAPPDATA', encoding.environ.get(b'APPDATA')
101 )
101 )
102 if appdata:
102 if appdata:
103 return os.path.join(appdata, name)
103 return os.path.join(appdata, name)
104
104
105 hint = _(b"define %s or %s in the environment, or set %s.usercache") % (
105 hint = _(b"define %s or %s in the environment, or set %s.usercache") % (
106 b"LOCALAPPDATA",
106 b"LOCALAPPDATA",
107 b"APPDATA",
107 b"APPDATA",
108 name,
108 name,
109 )
109 )
110 elif pycompat.isdarwin:
110 elif pycompat.isdarwin:
111 home = encoding.environ.get(b'HOME')
111 home = encoding.environ.get(b'HOME')
112 if home:
112 if home:
113 return os.path.join(home, b'Library', b'Caches', name)
113 return os.path.join(home, b'Library', b'Caches', name)
114
114
115 hint = _(b"define %s in the environment, or set %s.usercache") % (
115 hint = _(b"define %s in the environment, or set %s.usercache") % (
116 b"HOME",
116 b"HOME",
117 name,
117 name,
118 )
118 )
119 elif pycompat.isposix:
119 elif pycompat.isposix:
120 path = encoding.environ.get(b'XDG_CACHE_HOME')
120 path = encoding.environ.get(b'XDG_CACHE_HOME')
121 if path:
121 if path:
122 return os.path.join(path, name)
122 return os.path.join(path, name)
123 home = encoding.environ.get(b'HOME')
123 home = encoding.environ.get(b'HOME')
124 if home:
124 if home:
125 return os.path.join(home, b'.cache', name)
125 return os.path.join(home, b'.cache', name)
126
126
127 hint = _(b"define %s or %s in the environment, or set %s.usercache") % (
127 hint = _(b"define %s or %s in the environment, or set %s.usercache") % (
128 b"XDG_CACHE_HOME",
128 b"XDG_CACHE_HOME",
129 b"HOME",
129 b"HOME",
130 name,
130 name,
131 )
131 )
132 else:
132 else:
133 raise error.Abort(
133 raise error.Abort(
134 _(b'unknown operating system: %s\n') % pycompat.osname
134 _(b'unknown operating system: %s\n') % pycompat.osname
135 )
135 )
136
136
137 raise error.Abort(_(b'unknown %s usercache location') % name, hint=hint)
137 raise error.Abort(_(b'unknown %s usercache location') % name, hint=hint)
138
138
139
139
140 def inusercache(ui, hash):
140 def inusercache(ui, hash):
141 path = usercachepath(ui, hash)
141 path = usercachepath(ui, hash)
142 return os.path.exists(path)
142 return os.path.exists(path)
143
143
144
144
145 def findfile(repo, hash):
145 def findfile(repo, hash):
146 """Return store path of the largefile with the specified hash.
146 """Return store path of the largefile with the specified hash.
147 As a side effect, the file might be linked from user cache.
147 As a side effect, the file might be linked from user cache.
148 Return None if the file can't be found locally."""
148 Return None if the file can't be found locally."""
149 path, exists = findstorepath(repo, hash)
149 path, exists = findstorepath(repo, hash)
150 if exists:
150 if exists:
151 repo.ui.note(_(b'found %s in store\n') % hash)
151 repo.ui.note(_(b'found %s in store\n') % hash)
152 return path
152 return path
153 elif inusercache(repo.ui, hash):
153 elif inusercache(repo.ui, hash):
154 repo.ui.note(_(b'found %s in system cache\n') % hash)
154 repo.ui.note(_(b'found %s in system cache\n') % hash)
155 path = storepath(repo, hash)
155 path = storepath(repo, hash)
156 link(usercachepath(repo.ui, hash), path)
156 link(usercachepath(repo.ui, hash), path)
157 return path
157 return path
158 return None
158 return None
159
159
160
160
161 class largefilesdirstate(dirstate.dirstate):
161 class largefilesdirstate(dirstate.dirstate):
162 _large_file_dirstate = True
162 _large_file_dirstate = True
163 _tr_key_suffix = b'-large-files'
163
164
164 def __getitem__(self, key):
165 def __getitem__(self, key):
165 return super(largefilesdirstate, self).__getitem__(unixpath(key))
166 return super(largefilesdirstate, self).__getitem__(unixpath(key))
166
167
167 def set_tracked(self, f):
168 def set_tracked(self, f):
168 return super(largefilesdirstate, self).set_tracked(unixpath(f))
169 return super(largefilesdirstate, self).set_tracked(unixpath(f))
169
170
170 def set_untracked(self, f):
171 def set_untracked(self, f):
171 return super(largefilesdirstate, self).set_untracked(unixpath(f))
172 return super(largefilesdirstate, self).set_untracked(unixpath(f))
172
173
173 def normal(self, f, parentfiledata=None):
174 def normal(self, f, parentfiledata=None):
174 # not sure if we should pass the `parentfiledata` down or throw it
175 # not sure if we should pass the `parentfiledata` down or throw it
175 # away. So throwing it away to stay on the safe side.
176 # away. So throwing it away to stay on the safe side.
176 return super(largefilesdirstate, self).normal(unixpath(f))
177 return super(largefilesdirstate, self).normal(unixpath(f))
177
178
178 def remove(self, f):
179 def remove(self, f):
179 return super(largefilesdirstate, self).remove(unixpath(f))
180 return super(largefilesdirstate, self).remove(unixpath(f))
180
181
181 def add(self, f):
182 def add(self, f):
182 return super(largefilesdirstate, self).add(unixpath(f))
183 return super(largefilesdirstate, self).add(unixpath(f))
183
184
184 def drop(self, f):
185 def drop(self, f):
185 return super(largefilesdirstate, self).drop(unixpath(f))
186 return super(largefilesdirstate, self).drop(unixpath(f))
186
187
187 def forget(self, f):
188 def forget(self, f):
188 return super(largefilesdirstate, self).forget(unixpath(f))
189 return super(largefilesdirstate, self).forget(unixpath(f))
189
190
190 def normallookup(self, f):
191 def normallookup(self, f):
191 return super(largefilesdirstate, self).normallookup(unixpath(f))
192 return super(largefilesdirstate, self).normallookup(unixpath(f))
192
193
193 def _ignore(self, f):
194 def _ignore(self, f):
194 return False
195 return False
195
196
196 def write(self, tr):
197 def write(self, tr):
197 # (1) disable PENDING mode always
198 # (1) disable PENDING mode always
198 # (lfdirstate isn't yet managed as a part of the transaction)
199 # (lfdirstate isn't yet managed as a part of the transaction)
199 # (2) avoid develwarn 'use dirstate.write with ....'
200 # (2) avoid develwarn 'use dirstate.write with ....'
200 if tr:
201 if tr:
201 tr.addbackup(b'largefiles/dirstate', location=b'plain')
202 tr.addbackup(b'largefiles/dirstate', location=b'plain')
202 super(largefilesdirstate, self).write(None)
203 super(largefilesdirstate, self).write(None)
203
204
204
205
205 def openlfdirstate(ui, repo, create=True):
206 def openlfdirstate(ui, repo, create=True):
206 """
207 """
207 Return a dirstate object that tracks largefiles: i.e. its root is
208 Return a dirstate object that tracks largefiles: i.e. its root is
208 the repo root, but it is saved in .hg/largefiles/dirstate.
209 the repo root, but it is saved in .hg/largefiles/dirstate.
209
210
210 If a dirstate object already exists and is being used for a 'changing_*'
211 If a dirstate object already exists and is being used for a 'changing_*'
211 context, it will be returned.
212 context, it will be returned.
212 """
213 """
213 sub_dirstate = getattr(repo.dirstate, '_sub_dirstate', None)
214 sub_dirstate = getattr(repo.dirstate, '_sub_dirstate', None)
214 if sub_dirstate is not None:
215 if sub_dirstate is not None:
215 return sub_dirstate
216 return sub_dirstate
216 vfs = repo.vfs
217 vfs = repo.vfs
217 lfstoredir = longname
218 lfstoredir = longname
218 opener = vfsmod.vfs(vfs.join(lfstoredir))
219 opener = vfsmod.vfs(vfs.join(lfstoredir))
219 use_dirstate_v2 = requirements.DIRSTATE_V2_REQUIREMENT in repo.requirements
220 use_dirstate_v2 = requirements.DIRSTATE_V2_REQUIREMENT in repo.requirements
220 lfdirstate = largefilesdirstate(
221 lfdirstate = largefilesdirstate(
221 opener,
222 opener,
222 ui,
223 ui,
223 repo.root,
224 repo.root,
224 repo.dirstate._validate,
225 repo.dirstate._validate,
225 lambda: sparse.matcher(repo),
226 lambda: sparse.matcher(repo),
226 repo.nodeconstants,
227 repo.nodeconstants,
227 use_dirstate_v2,
228 use_dirstate_v2,
228 )
229 )
229
230
230 # If the largefiles dirstate does not exist, populate and create
231 # If the largefiles dirstate does not exist, populate and create
231 # it. This ensures that we create it on the first meaningful
232 # it. This ensures that we create it on the first meaningful
232 # largefiles operation in a new clone.
233 # largefiles operation in a new clone.
233 if create and not vfs.exists(vfs.join(lfstoredir, b'dirstate')):
234 if create and not vfs.exists(vfs.join(lfstoredir, b'dirstate')):
234 try:
235 try:
235 with repo.wlock(wait=False):
236 with repo.wlock(wait=False):
236 matcher = getstandinmatcher(repo)
237 matcher = getstandinmatcher(repo)
237 standins = repo.dirstate.walk(
238 standins = repo.dirstate.walk(
238 matcher, subrepos=[], unknown=False, ignored=False
239 matcher, subrepos=[], unknown=False, ignored=False
239 )
240 )
240
241
241 if len(standins) > 0:
242 if len(standins) > 0:
242 vfs.makedirs(lfstoredir)
243 vfs.makedirs(lfstoredir)
243
244
244 for standin in standins:
245 for standin in standins:
245 lfile = splitstandin(standin)
246 lfile = splitstandin(standin)
246 lfdirstate.hacky_extension_update_file(
247 lfdirstate.hacky_extension_update_file(
247 lfile,
248 lfile,
248 p1_tracked=True,
249 p1_tracked=True,
249 wc_tracked=True,
250 wc_tracked=True,
250 possibly_dirty=True,
251 possibly_dirty=True,
251 )
252 )
252 # avoid getting dirty dirstate before other operations
253 # avoid getting dirty dirstate before other operations
253 lfdirstate.write(repo.currenttransaction())
254 lfdirstate.write(repo.currenttransaction())
254 except error.LockError:
255 except error.LockError:
255 # Assume that whatever was holding the lock was important.
256 # Assume that whatever was holding the lock was important.
256 # If we were doing something important, we would already have
257 # If we were doing something important, we would already have
257 # either the lock or a largefile dirstate.
258 # either the lock or a largefile dirstate.
258 pass
259 pass
259 return lfdirstate
260 return lfdirstate
260
261
261
262
262 def lfdirstatestatus(lfdirstate, repo):
263 def lfdirstatestatus(lfdirstate, repo):
263 pctx = repo[b'.']
264 pctx = repo[b'.']
264 match = matchmod.always()
265 match = matchmod.always()
265 unsure, s, mtime_boundary = lfdirstate.status(
266 unsure, s, mtime_boundary = lfdirstate.status(
266 match, subrepos=[], ignored=False, clean=False, unknown=False
267 match, subrepos=[], ignored=False, clean=False, unknown=False
267 )
268 )
268 modified, clean = s.modified, s.clean
269 modified, clean = s.modified, s.clean
269 wctx = repo[None]
270 wctx = repo[None]
270 for lfile in unsure:
271 for lfile in unsure:
271 try:
272 try:
272 fctx = pctx[standin(lfile)]
273 fctx = pctx[standin(lfile)]
273 except LookupError:
274 except LookupError:
274 fctx = None
275 fctx = None
275 if not fctx or readasstandin(fctx) != hashfile(repo.wjoin(lfile)):
276 if not fctx or readasstandin(fctx) != hashfile(repo.wjoin(lfile)):
276 modified.append(lfile)
277 modified.append(lfile)
277 else:
278 else:
278 clean.append(lfile)
279 clean.append(lfile)
279 st = wctx[lfile].lstat()
280 st = wctx[lfile].lstat()
280 mode = st.st_mode
281 mode = st.st_mode
281 size = st.st_size
282 size = st.st_size
282 mtime = timestamp.reliable_mtime_of(st, mtime_boundary)
283 mtime = timestamp.reliable_mtime_of(st, mtime_boundary)
283 if mtime is not None:
284 if mtime is not None:
284 cache_data = (mode, size, mtime)
285 cache_data = (mode, size, mtime)
285 lfdirstate.set_clean(lfile, cache_data)
286 lfdirstate.set_clean(lfile, cache_data)
286 return s
287 return s
287
288
288
289
289 def listlfiles(repo, rev=None, matcher=None):
290 def listlfiles(repo, rev=None, matcher=None):
290 """return a list of largefiles in the working copy or the
291 """return a list of largefiles in the working copy or the
291 specified changeset"""
292 specified changeset"""
292
293
293 if matcher is None:
294 if matcher is None:
294 matcher = getstandinmatcher(repo)
295 matcher = getstandinmatcher(repo)
295
296
296 # ignore unknown files in working directory
297 # ignore unknown files in working directory
297 return [
298 return [
298 splitstandin(f)
299 splitstandin(f)
299 for f in repo[rev].walk(matcher)
300 for f in repo[rev].walk(matcher)
300 if rev is not None or repo.dirstate.get_entry(f).any_tracked
301 if rev is not None or repo.dirstate.get_entry(f).any_tracked
301 ]
302 ]
302
303
303
304
304 def instore(repo, hash, forcelocal=False):
305 def instore(repo, hash, forcelocal=False):
305 '''Return true if a largefile with the given hash exists in the store'''
306 '''Return true if a largefile with the given hash exists in the store'''
306 return os.path.exists(storepath(repo, hash, forcelocal))
307 return os.path.exists(storepath(repo, hash, forcelocal))
307
308
308
309
309 def storepath(repo, hash, forcelocal=False):
310 def storepath(repo, hash, forcelocal=False):
310 """Return the correct location in the repository largefiles store for a
311 """Return the correct location in the repository largefiles store for a
311 file with the given hash."""
312 file with the given hash."""
312 if not forcelocal and repo.shared():
313 if not forcelocal and repo.shared():
313 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
314 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
314 return repo.vfs.join(longname, hash)
315 return repo.vfs.join(longname, hash)
315
316
316
317
317 def findstorepath(repo, hash):
318 def findstorepath(repo, hash):
318 """Search through the local store path(s) to find the file for the given
319 """Search through the local store path(s) to find the file for the given
319 hash. If the file is not found, its path in the primary store is returned.
320 hash. If the file is not found, its path in the primary store is returned.
320 The return value is a tuple of (path, exists(path)).
321 The return value is a tuple of (path, exists(path)).
321 """
322 """
322 # For shared repos, the primary store is in the share source. But for
323 # For shared repos, the primary store is in the share source. But for
323 # backward compatibility, force a lookup in the local store if it wasn't
324 # backward compatibility, force a lookup in the local store if it wasn't
324 # found in the share source.
325 # found in the share source.
325 path = storepath(repo, hash, False)
326 path = storepath(repo, hash, False)
326
327
327 if instore(repo, hash):
328 if instore(repo, hash):
328 return (path, True)
329 return (path, True)
329 elif repo.shared() and instore(repo, hash, True):
330 elif repo.shared() and instore(repo, hash, True):
330 return storepath(repo, hash, True), True
331 return storepath(repo, hash, True), True
331
332
332 return (path, False)
333 return (path, False)
333
334
334
335
335 def copyfromcache(repo, hash, filename):
336 def copyfromcache(repo, hash, filename):
336 """Copy the specified largefile from the repo or system cache to
337 """Copy the specified largefile from the repo or system cache to
337 filename in the repository. Return true on success or false if the
338 filename in the repository. Return true on success or false if the
338 file was not found in either cache (which should not happened:
339 file was not found in either cache (which should not happened:
339 this is meant to be called only after ensuring that the needed
340 this is meant to be called only after ensuring that the needed
340 largefile exists in the cache)."""
341 largefile exists in the cache)."""
341 wvfs = repo.wvfs
342 wvfs = repo.wvfs
342 path = findfile(repo, hash)
343 path = findfile(repo, hash)
343 if path is None:
344 if path is None:
344 return False
345 return False
345 wvfs.makedirs(wvfs.dirname(wvfs.join(filename)))
346 wvfs.makedirs(wvfs.dirname(wvfs.join(filename)))
346 # The write may fail before the file is fully written, but we
347 # The write may fail before the file is fully written, but we
347 # don't use atomic writes in the working copy.
348 # don't use atomic writes in the working copy.
348 with open(path, b'rb') as srcfd, wvfs(filename, b'wb') as destfd:
349 with open(path, b'rb') as srcfd, wvfs(filename, b'wb') as destfd:
349 gothash = copyandhash(util.filechunkiter(srcfd), destfd)
350 gothash = copyandhash(util.filechunkiter(srcfd), destfd)
350 if gothash != hash:
351 if gothash != hash:
351 repo.ui.warn(
352 repo.ui.warn(
352 _(b'%s: data corruption in %s with hash %s\n')
353 _(b'%s: data corruption in %s with hash %s\n')
353 % (filename, path, gothash)
354 % (filename, path, gothash)
354 )
355 )
355 wvfs.unlink(filename)
356 wvfs.unlink(filename)
356 return False
357 return False
357 return True
358 return True
358
359
359
360
360 def copytostore(repo, ctx, file, fstandin):
361 def copytostore(repo, ctx, file, fstandin):
361 wvfs = repo.wvfs
362 wvfs = repo.wvfs
362 hash = readasstandin(ctx[fstandin])
363 hash = readasstandin(ctx[fstandin])
363 if instore(repo, hash):
364 if instore(repo, hash):
364 return
365 return
365 if wvfs.exists(file):
366 if wvfs.exists(file):
366 copytostoreabsolute(repo, wvfs.join(file), hash)
367 copytostoreabsolute(repo, wvfs.join(file), hash)
367 else:
368 else:
368 repo.ui.warn(
369 repo.ui.warn(
369 _(b"%s: largefile %s not available from local store\n")
370 _(b"%s: largefile %s not available from local store\n")
370 % (file, hash)
371 % (file, hash)
371 )
372 )
372
373
373
374
374 def copyalltostore(repo, node):
375 def copyalltostore(repo, node):
375 '''Copy all largefiles in a given revision to the store'''
376 '''Copy all largefiles in a given revision to the store'''
376
377
377 ctx = repo[node]
378 ctx = repo[node]
378 for filename in ctx.files():
379 for filename in ctx.files():
379 realfile = splitstandin(filename)
380 realfile = splitstandin(filename)
380 if realfile is not None and filename in ctx.manifest():
381 if realfile is not None and filename in ctx.manifest():
381 copytostore(repo, ctx, realfile, filename)
382 copytostore(repo, ctx, realfile, filename)
382
383
383
384
384 def copytostoreabsolute(repo, file, hash):
385 def copytostoreabsolute(repo, file, hash):
385 if inusercache(repo.ui, hash):
386 if inusercache(repo.ui, hash):
386 link(usercachepath(repo.ui, hash), storepath(repo, hash))
387 link(usercachepath(repo.ui, hash), storepath(repo, hash))
387 else:
388 else:
388 util.makedirs(os.path.dirname(storepath(repo, hash)))
389 util.makedirs(os.path.dirname(storepath(repo, hash)))
389 with open(file, b'rb') as srcf:
390 with open(file, b'rb') as srcf:
390 with util.atomictempfile(
391 with util.atomictempfile(
391 storepath(repo, hash), createmode=repo.store.createmode
392 storepath(repo, hash), createmode=repo.store.createmode
392 ) as dstf:
393 ) as dstf:
393 for chunk in util.filechunkiter(srcf):
394 for chunk in util.filechunkiter(srcf):
394 dstf.write(chunk)
395 dstf.write(chunk)
395 linktousercache(repo, hash)
396 linktousercache(repo, hash)
396
397
397
398
398 def linktousercache(repo, hash):
399 def linktousercache(repo, hash):
399 """Link / copy the largefile with the specified hash from the store
400 """Link / copy the largefile with the specified hash from the store
400 to the cache."""
401 to the cache."""
401 path = usercachepath(repo.ui, hash)
402 path = usercachepath(repo.ui, hash)
402 link(storepath(repo, hash), path)
403 link(storepath(repo, hash), path)
403
404
404
405
405 def getstandinmatcher(repo, rmatcher=None):
406 def getstandinmatcher(repo, rmatcher=None):
406 '''Return a match object that applies rmatcher to the standin directory'''
407 '''Return a match object that applies rmatcher to the standin directory'''
407 wvfs = repo.wvfs
408 wvfs = repo.wvfs
408 standindir = shortname
409 standindir = shortname
409
410
410 # no warnings about missing files or directories
411 # no warnings about missing files or directories
411 badfn = lambda f, msg: None
412 badfn = lambda f, msg: None
412
413
413 if rmatcher and not rmatcher.always():
414 if rmatcher and not rmatcher.always():
414 pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()]
415 pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()]
415 if not pats:
416 if not pats:
416 pats = [wvfs.join(standindir)]
417 pats = [wvfs.join(standindir)]
417 match = scmutil.match(repo[None], pats, badfn=badfn)
418 match = scmutil.match(repo[None], pats, badfn=badfn)
418 else:
419 else:
419 # no patterns: relative to repo root
420 # no patterns: relative to repo root
420 match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn)
421 match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn)
421 return match
422 return match
422
423
423
424
424 def composestandinmatcher(repo, rmatcher):
425 def composestandinmatcher(repo, rmatcher):
425 """Return a matcher that accepts standins corresponding to the
426 """Return a matcher that accepts standins corresponding to the
426 files accepted by rmatcher. Pass the list of files in the matcher
427 files accepted by rmatcher. Pass the list of files in the matcher
427 as the paths specified by the user."""
428 as the paths specified by the user."""
428 smatcher = getstandinmatcher(repo, rmatcher)
429 smatcher = getstandinmatcher(repo, rmatcher)
429 isstandin = smatcher.matchfn
430 isstandin = smatcher.matchfn
430
431
431 def composedmatchfn(f):
432 def composedmatchfn(f):
432 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
433 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
433
434
434 smatcher.matchfn = composedmatchfn
435 smatcher.matchfn = composedmatchfn
435
436
436 return smatcher
437 return smatcher
437
438
438
439
439 def standin(filename):
440 def standin(filename):
440 """Return the repo-relative path to the standin for the specified big
441 """Return the repo-relative path to the standin for the specified big
441 file."""
442 file."""
442 # Notes:
443 # Notes:
443 # 1) Some callers want an absolute path, but for instance addlargefiles
444 # 1) Some callers want an absolute path, but for instance addlargefiles
444 # needs it repo-relative so it can be passed to repo[None].add(). So
445 # needs it repo-relative so it can be passed to repo[None].add(). So
445 # leave it up to the caller to use repo.wjoin() to get an absolute path.
446 # leave it up to the caller to use repo.wjoin() to get an absolute path.
446 # 2) Join with '/' because that's what dirstate always uses, even on
447 # 2) Join with '/' because that's what dirstate always uses, even on
447 # Windows. Change existing separator to '/' first in case we are
448 # Windows. Change existing separator to '/' first in case we are
448 # passed filenames from an external source (like the command line).
449 # passed filenames from an external source (like the command line).
449 return shortnameslash + util.pconvert(filename)
450 return shortnameslash + util.pconvert(filename)
450
451
451
452
452 def isstandin(filename):
453 def isstandin(filename):
453 """Return true if filename is a big file standin. filename must be
454 """Return true if filename is a big file standin. filename must be
454 in Mercurial's internal form (slash-separated)."""
455 in Mercurial's internal form (slash-separated)."""
455 return filename.startswith(shortnameslash)
456 return filename.startswith(shortnameslash)
456
457
457
458
458 def splitstandin(filename):
459 def splitstandin(filename):
459 # Split on / because that's what dirstate always uses, even on Windows.
460 # Split on / because that's what dirstate always uses, even on Windows.
460 # Change local separator to / first just in case we are passed filenames
461 # Change local separator to / first just in case we are passed filenames
461 # from an external source (like the command line).
462 # from an external source (like the command line).
462 bits = util.pconvert(filename).split(b'/', 1)
463 bits = util.pconvert(filename).split(b'/', 1)
463 if len(bits) == 2 and bits[0] == shortname:
464 if len(bits) == 2 and bits[0] == shortname:
464 return bits[1]
465 return bits[1]
465 else:
466 else:
466 return None
467 return None
467
468
468
469
469 def updatestandin(repo, lfile, standin):
470 def updatestandin(repo, lfile, standin):
470 """Re-calculate hash value of lfile and write it into standin
471 """Re-calculate hash value of lfile and write it into standin
471
472
472 This assumes that "lfutil.standin(lfile) == standin", for efficiency.
473 This assumes that "lfutil.standin(lfile) == standin", for efficiency.
473 """
474 """
474 file = repo.wjoin(lfile)
475 file = repo.wjoin(lfile)
475 if repo.wvfs.exists(lfile):
476 if repo.wvfs.exists(lfile):
476 hash = hashfile(file)
477 hash = hashfile(file)
477 executable = getexecutable(file)
478 executable = getexecutable(file)
478 writestandin(repo, standin, hash, executable)
479 writestandin(repo, standin, hash, executable)
479 else:
480 else:
480 raise error.Abort(_(b'%s: file not found!') % lfile)
481 raise error.Abort(_(b'%s: file not found!') % lfile)
481
482
482
483
483 def readasstandin(fctx):
484 def readasstandin(fctx):
484 """read hex hash from given filectx of standin file
485 """read hex hash from given filectx of standin file
485
486
486 This encapsulates how "standin" data is stored into storage layer."""
487 This encapsulates how "standin" data is stored into storage layer."""
487 return fctx.data().strip()
488 return fctx.data().strip()
488
489
489
490
490 def writestandin(repo, standin, hash, executable):
491 def writestandin(repo, standin, hash, executable):
491 '''write hash to <repo.root>/<standin>'''
492 '''write hash to <repo.root>/<standin>'''
492 repo.wwrite(standin, hash + b'\n', executable and b'x' or b'')
493 repo.wwrite(standin, hash + b'\n', executable and b'x' or b'')
493
494
494
495
495 def copyandhash(instream, outfile):
496 def copyandhash(instream, outfile):
496 """Read bytes from instream (iterable) and write them to outfile,
497 """Read bytes from instream (iterable) and write them to outfile,
497 computing the SHA-1 hash of the data along the way. Return the hash."""
498 computing the SHA-1 hash of the data along the way. Return the hash."""
498 hasher = hashutil.sha1(b'')
499 hasher = hashutil.sha1(b'')
499 for data in instream:
500 for data in instream:
500 hasher.update(data)
501 hasher.update(data)
501 outfile.write(data)
502 outfile.write(data)
502 return hex(hasher.digest())
503 return hex(hasher.digest())
503
504
504
505
505 def hashfile(file):
506 def hashfile(file):
506 if not os.path.exists(file):
507 if not os.path.exists(file):
507 return b''
508 return b''
508 with open(file, b'rb') as fd:
509 with open(file, b'rb') as fd:
509 return hexsha1(fd)
510 return hexsha1(fd)
510
511
511
512
512 def getexecutable(filename):
513 def getexecutable(filename):
513 mode = os.stat(filename).st_mode
514 mode = os.stat(filename).st_mode
514 return (
515 return (
515 (mode & stat.S_IXUSR)
516 (mode & stat.S_IXUSR)
516 and (mode & stat.S_IXGRP)
517 and (mode & stat.S_IXGRP)
517 and (mode & stat.S_IXOTH)
518 and (mode & stat.S_IXOTH)
518 )
519 )
519
520
520
521
521 def urljoin(first, second, *arg):
522 def urljoin(first, second, *arg):
522 def join(left, right):
523 def join(left, right):
523 if not left.endswith(b'/'):
524 if not left.endswith(b'/'):
524 left += b'/'
525 left += b'/'
525 if right.startswith(b'/'):
526 if right.startswith(b'/'):
526 right = right[1:]
527 right = right[1:]
527 return left + right
528 return left + right
528
529
529 url = join(first, second)
530 url = join(first, second)
530 for a in arg:
531 for a in arg:
531 url = join(url, a)
532 url = join(url, a)
532 return url
533 return url
533
534
534
535
535 def hexsha1(fileobj):
536 def hexsha1(fileobj):
536 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
537 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
537 object data"""
538 object data"""
538 h = hashutil.sha1()
539 h = hashutil.sha1()
539 for chunk in util.filechunkiter(fileobj):
540 for chunk in util.filechunkiter(fileobj):
540 h.update(chunk)
541 h.update(chunk)
541 return hex(h.digest())
542 return hex(h.digest())
542
543
543
544
544 def httpsendfile(ui, filename):
545 def httpsendfile(ui, filename):
545 return httpconnection.httpsendfile(ui, filename, b'rb')
546 return httpconnection.httpsendfile(ui, filename, b'rb')
546
547
547
548
548 def unixpath(path):
549 def unixpath(path):
549 '''Return a version of path normalized for use with the lfdirstate.'''
550 '''Return a version of path normalized for use with the lfdirstate.'''
550 return util.pconvert(os.path.normpath(path))
551 return util.pconvert(os.path.normpath(path))
551
552
552
553
553 def islfilesrepo(repo):
554 def islfilesrepo(repo):
554 '''Return true if the repo is a largefile repo.'''
555 '''Return true if the repo is a largefile repo.'''
555 if b'largefiles' in repo.requirements and any(
556 if b'largefiles' in repo.requirements and any(
556 shortnameslash in f[1] for f in repo.store.datafiles()
557 shortnameslash in f[1] for f in repo.store.datafiles()
557 ):
558 ):
558 return True
559 return True
559
560
560 return any(openlfdirstate(repo.ui, repo, False))
561 return any(openlfdirstate(repo.ui, repo, False))
561
562
562
563
563 class storeprotonotcapable(Exception):
564 class storeprotonotcapable(Exception):
564 def __init__(self, storetypes):
565 def __init__(self, storetypes):
565 self.storetypes = storetypes
566 self.storetypes = storetypes
566
567
567
568
568 def getstandinsstate(repo):
569 def getstandinsstate(repo):
569 standins = []
570 standins = []
570 matcher = getstandinmatcher(repo)
571 matcher = getstandinmatcher(repo)
571 wctx = repo[None]
572 wctx = repo[None]
572 for standin in repo.dirstate.walk(
573 for standin in repo.dirstate.walk(
573 matcher, subrepos=[], unknown=False, ignored=False
574 matcher, subrepos=[], unknown=False, ignored=False
574 ):
575 ):
575 lfile = splitstandin(standin)
576 lfile = splitstandin(standin)
576 try:
577 try:
577 hash = readasstandin(wctx[standin])
578 hash = readasstandin(wctx[standin])
578 except IOError:
579 except IOError:
579 hash = None
580 hash = None
580 standins.append((lfile, hash))
581 standins.append((lfile, hash))
581 return standins
582 return standins
582
583
583
584
584 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
585 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
585 lfstandin = standin(lfile)
586 lfstandin = standin(lfile)
586 if lfstandin not in repo.dirstate:
587 if lfstandin not in repo.dirstate:
587 lfdirstate.hacky_extension_update_file(
588 lfdirstate.hacky_extension_update_file(
588 lfile,
589 lfile,
589 p1_tracked=False,
590 p1_tracked=False,
590 wc_tracked=False,
591 wc_tracked=False,
591 )
592 )
592 else:
593 else:
593 entry = repo.dirstate.get_entry(lfstandin)
594 entry = repo.dirstate.get_entry(lfstandin)
594 lfdirstate.hacky_extension_update_file(
595 lfdirstate.hacky_extension_update_file(
595 lfile,
596 lfile,
596 wc_tracked=entry.tracked,
597 wc_tracked=entry.tracked,
597 p1_tracked=entry.p1_tracked,
598 p1_tracked=entry.p1_tracked,
598 p2_info=entry.p2_info,
599 p2_info=entry.p2_info,
599 possibly_dirty=True,
600 possibly_dirty=True,
600 )
601 )
601
602
602
603
603 def markcommitted(orig, ctx, node):
604 def markcommitted(orig, ctx, node):
604 repo = ctx.repo()
605 repo = ctx.repo()
605
606
606 with repo.dirstate.changing_parents(repo):
607 with repo.dirstate.changing_parents(repo):
607 orig(node)
608 orig(node)
608
609
609 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
610 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
610 # because files coming from the 2nd parent are omitted in the latter.
611 # because files coming from the 2nd parent are omitted in the latter.
611 #
612 #
612 # The former should be used to get targets of "synclfdirstate",
613 # The former should be used to get targets of "synclfdirstate",
613 # because such files:
614 # because such files:
614 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
615 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
615 # - have to be marked as "n" after commit, but
616 # - have to be marked as "n" after commit, but
616 # - aren't listed in "repo[node].files()"
617 # - aren't listed in "repo[node].files()"
617
618
618 lfdirstate = openlfdirstate(repo.ui, repo)
619 lfdirstate = openlfdirstate(repo.ui, repo)
619 for f in ctx.files():
620 for f in ctx.files():
620 lfile = splitstandin(f)
621 lfile = splitstandin(f)
621 if lfile is not None:
622 if lfile is not None:
622 synclfdirstate(repo, lfdirstate, lfile, False)
623 synclfdirstate(repo, lfdirstate, lfile, False)
623
624
624 # As part of committing, copy all of the largefiles into the cache.
625 # As part of committing, copy all of the largefiles into the cache.
625 #
626 #
626 # Using "node" instead of "ctx" implies additional "repo[node]"
627 # Using "node" instead of "ctx" implies additional "repo[node]"
627 # lookup while copyalltostore(), but can omit redundant check for
628 # lookup while copyalltostore(), but can omit redundant check for
628 # files comming from the 2nd parent, which should exist in store
629 # files comming from the 2nd parent, which should exist in store
629 # at merging.
630 # at merging.
630 copyalltostore(repo, node)
631 copyalltostore(repo, node)
631
632
632
633
633 def getlfilestoupdate(oldstandins, newstandins):
634 def getlfilestoupdate(oldstandins, newstandins):
634 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
635 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
635 filelist = []
636 filelist = []
636 for f in changedstandins:
637 for f in changedstandins:
637 if f[0] not in filelist:
638 if f[0] not in filelist:
638 filelist.append(f[0])
639 filelist.append(f[0])
639 return filelist
640 return filelist
640
641
641
642
642 def getlfilestoupload(repo, missing, addfunc):
643 def getlfilestoupload(repo, missing, addfunc):
643 makeprogress = repo.ui.makeprogress
644 makeprogress = repo.ui.makeprogress
644 with makeprogress(
645 with makeprogress(
645 _(b'finding outgoing largefiles'),
646 _(b'finding outgoing largefiles'),
646 unit=_(b'revisions'),
647 unit=_(b'revisions'),
647 total=len(missing),
648 total=len(missing),
648 ) as progress:
649 ) as progress:
649 for i, n in enumerate(missing):
650 for i, n in enumerate(missing):
650 progress.update(i)
651 progress.update(i)
651 parents = [p for p in repo[n].parents() if p != repo.nullid]
652 parents = [p for p in repo[n].parents() if p != repo.nullid]
652
653
653 with lfstatus(repo, value=False):
654 with lfstatus(repo, value=False):
654 ctx = repo[n]
655 ctx = repo[n]
655
656
656 files = set(ctx.files())
657 files = set(ctx.files())
657 if len(parents) == 2:
658 if len(parents) == 2:
658 mc = ctx.manifest()
659 mc = ctx.manifest()
659 mp1 = ctx.p1().manifest()
660 mp1 = ctx.p1().manifest()
660 mp2 = ctx.p2().manifest()
661 mp2 = ctx.p2().manifest()
661 for f in mp1:
662 for f in mp1:
662 if f not in mc:
663 if f not in mc:
663 files.add(f)
664 files.add(f)
664 for f in mp2:
665 for f in mp2:
665 if f not in mc:
666 if f not in mc:
666 files.add(f)
667 files.add(f)
667 for f in mc:
668 for f in mc:
668 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
669 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
669 files.add(f)
670 files.add(f)
670 for fn in files:
671 for fn in files:
671 if isstandin(fn) and fn in ctx:
672 if isstandin(fn) and fn in ctx:
672 addfunc(fn, readasstandin(ctx[fn]))
673 addfunc(fn, readasstandin(ctx[fn]))
673
674
674
675
675 def updatestandinsbymatch(repo, match):
676 def updatestandinsbymatch(repo, match):
676 """Update standins in the working directory according to specified match
677 """Update standins in the working directory according to specified match
677
678
678 This returns (possibly modified) ``match`` object to be used for
679 This returns (possibly modified) ``match`` object to be used for
679 subsequent commit process.
680 subsequent commit process.
680 """
681 """
681
682
682 ui = repo.ui
683 ui = repo.ui
683
684
684 # Case 1: user calls commit with no specific files or
685 # Case 1: user calls commit with no specific files or
685 # include/exclude patterns: refresh and commit all files that
686 # include/exclude patterns: refresh and commit all files that
686 # are "dirty".
687 # are "dirty".
687 if match is None or match.always():
688 if match is None or match.always():
688 # Spend a bit of time here to get a list of files we know
689 # Spend a bit of time here to get a list of files we know
689 # are modified so we can compare only against those.
690 # are modified so we can compare only against those.
690 # It can cost a lot of time (several seconds)
691 # It can cost a lot of time (several seconds)
691 # otherwise to update all standins if the largefiles are
692 # otherwise to update all standins if the largefiles are
692 # large.
693 # large.
693 lfdirstate = openlfdirstate(ui, repo)
694 lfdirstate = openlfdirstate(ui, repo)
694 dirtymatch = matchmod.always()
695 dirtymatch = matchmod.always()
695 unsure, s, mtime_boundary = lfdirstate.status(
696 unsure, s, mtime_boundary = lfdirstate.status(
696 dirtymatch, subrepos=[], ignored=False, clean=False, unknown=False
697 dirtymatch, subrepos=[], ignored=False, clean=False, unknown=False
697 )
698 )
698 modifiedfiles = unsure + s.modified + s.added + s.removed
699 modifiedfiles = unsure + s.modified + s.added + s.removed
699 lfiles = listlfiles(repo)
700 lfiles = listlfiles(repo)
700 # this only loops through largefiles that exist (not
701 # this only loops through largefiles that exist (not
701 # removed/renamed)
702 # removed/renamed)
702 for lfile in lfiles:
703 for lfile in lfiles:
703 if lfile in modifiedfiles:
704 if lfile in modifiedfiles:
704 fstandin = standin(lfile)
705 fstandin = standin(lfile)
705 if repo.wvfs.exists(fstandin):
706 if repo.wvfs.exists(fstandin):
706 # this handles the case where a rebase is being
707 # this handles the case where a rebase is being
707 # performed and the working copy is not updated
708 # performed and the working copy is not updated
708 # yet.
709 # yet.
709 if repo.wvfs.exists(lfile):
710 if repo.wvfs.exists(lfile):
710 updatestandin(repo, lfile, fstandin)
711 updatestandin(repo, lfile, fstandin)
711
712
712 return match
713 return match
713
714
714 lfiles = listlfiles(repo)
715 lfiles = listlfiles(repo)
715 match._files = repo._subdirlfs(match.files(), lfiles)
716 match._files = repo._subdirlfs(match.files(), lfiles)
716
717
717 # Case 2: user calls commit with specified patterns: refresh
718 # Case 2: user calls commit with specified patterns: refresh
718 # any matching big files.
719 # any matching big files.
719 smatcher = composestandinmatcher(repo, match)
720 smatcher = composestandinmatcher(repo, match)
720 standins = repo.dirstate.walk(
721 standins = repo.dirstate.walk(
721 smatcher, subrepos=[], unknown=False, ignored=False
722 smatcher, subrepos=[], unknown=False, ignored=False
722 )
723 )
723
724
724 # No matching big files: get out of the way and pass control to
725 # No matching big files: get out of the way and pass control to
725 # the usual commit() method.
726 # the usual commit() method.
726 if not standins:
727 if not standins:
727 return match
728 return match
728
729
729 # Refresh all matching big files. It's possible that the
730 # Refresh all matching big files. It's possible that the
730 # commit will end up failing, in which case the big files will
731 # commit will end up failing, in which case the big files will
731 # stay refreshed. No harm done: the user modified them and
732 # stay refreshed. No harm done: the user modified them and
732 # asked to commit them, so sooner or later we're going to
733 # asked to commit them, so sooner or later we're going to
733 # refresh the standins. Might as well leave them refreshed.
734 # refresh the standins. Might as well leave them refreshed.
734 lfdirstate = openlfdirstate(ui, repo)
735 lfdirstate = openlfdirstate(ui, repo)
735 for fstandin in standins:
736 for fstandin in standins:
736 lfile = splitstandin(fstandin)
737 lfile = splitstandin(fstandin)
737 if lfdirstate.get_entry(lfile).tracked:
738 if lfdirstate.get_entry(lfile).tracked:
738 updatestandin(repo, lfile, fstandin)
739 updatestandin(repo, lfile, fstandin)
739
740
740 # Cook up a new matcher that only matches regular files or
741 # Cook up a new matcher that only matches regular files or
741 # standins corresponding to the big files requested by the
742 # standins corresponding to the big files requested by the
742 # user. Have to modify _files to prevent commit() from
743 # user. Have to modify _files to prevent commit() from
743 # complaining "not tracked" for big files.
744 # complaining "not tracked" for big files.
744 match = copy.copy(match)
745 match = copy.copy(match)
745 origmatchfn = match.matchfn
746 origmatchfn = match.matchfn
746
747
747 # Check both the list of largefiles and the list of
748 # Check both the list of largefiles and the list of
748 # standins because if a largefile was removed, it
749 # standins because if a largefile was removed, it
749 # won't be in the list of largefiles at this point
750 # won't be in the list of largefiles at this point
750 match._files += sorted(standins)
751 match._files += sorted(standins)
751
752
752 actualfiles = []
753 actualfiles = []
753 for f in match._files:
754 for f in match._files:
754 fstandin = standin(f)
755 fstandin = standin(f)
755
756
756 # For largefiles, only one of the normal and standin should be
757 # For largefiles, only one of the normal and standin should be
757 # committed (except if one of them is a remove). In the case of a
758 # committed (except if one of them is a remove). In the case of a
758 # standin removal, drop the normal file if it is unknown to dirstate.
759 # standin removal, drop the normal file if it is unknown to dirstate.
759 # Thus, skip plain largefile names but keep the standin.
760 # Thus, skip plain largefile names but keep the standin.
760 if f in lfiles or fstandin in standins:
761 if f in lfiles or fstandin in standins:
761 if not repo.dirstate.get_entry(fstandin).removed:
762 if not repo.dirstate.get_entry(fstandin).removed:
762 if not repo.dirstate.get_entry(f).removed:
763 if not repo.dirstate.get_entry(f).removed:
763 continue
764 continue
764 elif not repo.dirstate.get_entry(f).any_tracked:
765 elif not repo.dirstate.get_entry(f).any_tracked:
765 continue
766 continue
766
767
767 actualfiles.append(f)
768 actualfiles.append(f)
768 match._files = actualfiles
769 match._files = actualfiles
769
770
770 def matchfn(f):
771 def matchfn(f):
771 if origmatchfn(f):
772 if origmatchfn(f):
772 return f not in lfiles
773 return f not in lfiles
773 else:
774 else:
774 return f in standins
775 return f in standins
775
776
776 match.matchfn = matchfn
777 match.matchfn = matchfn
777
778
778 return match
779 return match
779
780
780
781
781 class automatedcommithook:
782 class automatedcommithook:
782 """Stateful hook to update standins at the 1st commit of resuming
783 """Stateful hook to update standins at the 1st commit of resuming
783
784
784 For efficiency, updating standins in the working directory should
785 For efficiency, updating standins in the working directory should
785 be avoided while automated committing (like rebase, transplant and
786 be avoided while automated committing (like rebase, transplant and
786 so on), because they should be updated before committing.
787 so on), because they should be updated before committing.
787
788
788 But the 1st commit of resuming automated committing (e.g. ``rebase
789 But the 1st commit of resuming automated committing (e.g. ``rebase
789 --continue``) should update them, because largefiles may be
790 --continue``) should update them, because largefiles may be
790 modified manually.
791 modified manually.
791 """
792 """
792
793
793 def __init__(self, resuming):
794 def __init__(self, resuming):
794 self.resuming = resuming
795 self.resuming = resuming
795
796
796 def __call__(self, repo, match):
797 def __call__(self, repo, match):
797 if self.resuming:
798 if self.resuming:
798 self.resuming = False # avoids updating at subsequent commits
799 self.resuming = False # avoids updating at subsequent commits
799 return updatestandinsbymatch(repo, match)
800 return updatestandinsbymatch(repo, match)
800 else:
801 else:
801 return match
802 return match
802
803
803
804
804 def getstatuswriter(ui, repo, forcibly=None):
805 def getstatuswriter(ui, repo, forcibly=None):
805 """Return the function to write largefiles specific status out
806 """Return the function to write largefiles specific status out
806
807
807 If ``forcibly`` is ``None``, this returns the last element of
808 If ``forcibly`` is ``None``, this returns the last element of
808 ``repo._lfstatuswriters`` as "default" writer function.
809 ``repo._lfstatuswriters`` as "default" writer function.
809
810
810 Otherwise, this returns the function to always write out (or
811 Otherwise, this returns the function to always write out (or
811 ignore if ``not forcibly``) status.
812 ignore if ``not forcibly``) status.
812 """
813 """
813 if forcibly is None and util.safehasattr(repo, b'_largefilesenabled'):
814 if forcibly is None and util.safehasattr(repo, b'_largefilesenabled'):
814 return repo._lfstatuswriters[-1]
815 return repo._lfstatuswriters[-1]
815 else:
816 else:
816 if forcibly:
817 if forcibly:
817 return ui.status # forcibly WRITE OUT
818 return ui.status # forcibly WRITE OUT
818 else:
819 else:
819 return lambda *msg, **opts: None # forcibly IGNORE
820 return lambda *msg, **opts: None # forcibly IGNORE
@@ -1,1694 +1,1701 b''
1 # dirstate.py - working directory tracking for mercurial
1 # dirstate.py - working directory tracking for mercurial
2 #
2 #
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
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
8
9 import collections
9 import collections
10 import contextlib
10 import contextlib
11 import os
11 import os
12 import stat
12 import stat
13 import uuid
13 import uuid
14
14
15 from .i18n import _
15 from .i18n import _
16 from .pycompat import delattr
16 from .pycompat import delattr
17
17
18 from hgdemandimport import tracing
18 from hgdemandimport import tracing
19
19
20 from . import (
20 from . import (
21 dirstatemap,
21 dirstatemap,
22 encoding,
22 encoding,
23 error,
23 error,
24 match as matchmod,
24 match as matchmod,
25 node,
25 node,
26 pathutil,
26 pathutil,
27 policy,
27 policy,
28 pycompat,
28 pycompat,
29 scmutil,
29 scmutil,
30 util,
30 util,
31 )
31 )
32
32
33 from .dirstateutils import (
33 from .dirstateutils import (
34 timestamp,
34 timestamp,
35 )
35 )
36
36
37 from .interfaces import (
37 from .interfaces import (
38 dirstate as intdirstate,
38 dirstate as intdirstate,
39 util as interfaceutil,
39 util as interfaceutil,
40 )
40 )
41
41
42 parsers = policy.importmod('parsers')
42 parsers = policy.importmod('parsers')
43 rustmod = policy.importrust('dirstate')
43 rustmod = policy.importrust('dirstate')
44
44
45 HAS_FAST_DIRSTATE_V2 = rustmod is not None
45 HAS_FAST_DIRSTATE_V2 = rustmod is not None
46
46
47 propertycache = util.propertycache
47 propertycache = util.propertycache
48 filecache = scmutil.filecache
48 filecache = scmutil.filecache
49 _rangemask = dirstatemap.rangemask
49 _rangemask = dirstatemap.rangemask
50
50
51 DirstateItem = dirstatemap.DirstateItem
51 DirstateItem = dirstatemap.DirstateItem
52
52
53
53
54 class repocache(filecache):
54 class repocache(filecache):
55 """filecache for files in .hg/"""
55 """filecache for files in .hg/"""
56
56
57 def join(self, obj, fname):
57 def join(self, obj, fname):
58 return obj._opener.join(fname)
58 return obj._opener.join(fname)
59
59
60
60
61 class rootcache(filecache):
61 class rootcache(filecache):
62 """filecache for files in the repository root"""
62 """filecache for files in the repository root"""
63
63
64 def join(self, obj, fname):
64 def join(self, obj, fname):
65 return obj._join(fname)
65 return obj._join(fname)
66
66
67
67
68 def check_invalidated(func):
68 def check_invalidated(func):
69 """check we func is called a non-invalidated dirstate
69 """check we func is called a non-invalidated dirstate
70
70
71 The dirstate is in an "invalidated state" after an error occured during its
71 The dirstate is in an "invalidated state" after an error occured during its
72 modification and remains so until we exited the top level scope that framed
72 modification and remains so until we exited the top level scope that framed
73 such change.
73 such change.
74 """
74 """
75
75
76 def wrap(self, *args, **kwargs):
76 def wrap(self, *args, **kwargs):
77 if self._invalidated_context:
77 if self._invalidated_context:
78 msg = 'calling `%s` after the dirstate was invalidated'
78 msg = 'calling `%s` after the dirstate was invalidated'
79 msg %= func.__name__
79 msg %= func.__name__
80 raise error.ProgrammingError(msg)
80 raise error.ProgrammingError(msg)
81 return func(self, *args, **kwargs)
81 return func(self, *args, **kwargs)
82
82
83 return wrap
83 return wrap
84
84
85
85
86 def requires_changing_parents(func):
86 def requires_changing_parents(func):
87 def wrap(self, *args, **kwargs):
87 def wrap(self, *args, **kwargs):
88 if not self.is_changing_parents:
88 if not self.is_changing_parents:
89 msg = 'calling `%s` outside of a changing_parents context'
89 msg = 'calling `%s` outside of a changing_parents context'
90 msg %= func.__name__
90 msg %= func.__name__
91 raise error.ProgrammingError(msg)
91 raise error.ProgrammingError(msg)
92 return func(self, *args, **kwargs)
92 return func(self, *args, **kwargs)
93
93
94 return check_invalidated(wrap)
94 return check_invalidated(wrap)
95
95
96
96
97 def requires_changing_files(func):
97 def requires_changing_files(func):
98 def wrap(self, *args, **kwargs):
98 def wrap(self, *args, **kwargs):
99 if not self.is_changing_files:
99 if not self.is_changing_files:
100 msg = 'calling `%s` outside of a `changing_files`'
100 msg = 'calling `%s` outside of a `changing_files`'
101 msg %= func.__name__
101 msg %= func.__name__
102 raise error.ProgrammingError(msg)
102 raise error.ProgrammingError(msg)
103 return func(self, *args, **kwargs)
103 return func(self, *args, **kwargs)
104
104
105 return check_invalidated(wrap)
105 return check_invalidated(wrap)
106
106
107
107
108 def requires_changing_any(func):
108 def requires_changing_any(func):
109 def wrap(self, *args, **kwargs):
109 def wrap(self, *args, **kwargs):
110 if not self.is_changing_any:
110 if not self.is_changing_any:
111 msg = 'calling `%s` outside of a changing context'
111 msg = 'calling `%s` outside of a changing context'
112 msg %= func.__name__
112 msg %= func.__name__
113 raise error.ProgrammingError(msg)
113 raise error.ProgrammingError(msg)
114 return func(self, *args, **kwargs)
114 return func(self, *args, **kwargs)
115
115
116 return check_invalidated(wrap)
116 return check_invalidated(wrap)
117
117
118
118
119 def requires_not_changing_parents(func):
119 def requires_not_changing_parents(func):
120 def wrap(self, *args, **kwargs):
120 def wrap(self, *args, **kwargs):
121 if self.is_changing_parents:
121 if self.is_changing_parents:
122 msg = 'calling `%s` inside of a changing_parents context'
122 msg = 'calling `%s` inside of a changing_parents context'
123 msg %= func.__name__
123 msg %= func.__name__
124 raise error.ProgrammingError(msg)
124 raise error.ProgrammingError(msg)
125 return func(self, *args, **kwargs)
125 return func(self, *args, **kwargs)
126
126
127 return check_invalidated(wrap)
127 return check_invalidated(wrap)
128
128
129
129
130 CHANGE_TYPE_PARENTS = "parents"
130 CHANGE_TYPE_PARENTS = "parents"
131 CHANGE_TYPE_FILES = "files"
131 CHANGE_TYPE_FILES = "files"
132
132
133
133
134 @interfaceutil.implementer(intdirstate.idirstate)
134 @interfaceutil.implementer(intdirstate.idirstate)
135 class dirstate:
135 class dirstate:
136
137 # used by largefile to avoid overwritting transaction callbacK
138 _tr_key_suffix = b''
139
136 def __init__(
140 def __init__(
137 self,
141 self,
138 opener,
142 opener,
139 ui,
143 ui,
140 root,
144 root,
141 validate,
145 validate,
142 sparsematchfn,
146 sparsematchfn,
143 nodeconstants,
147 nodeconstants,
144 use_dirstate_v2,
148 use_dirstate_v2,
145 use_tracked_hint=False,
149 use_tracked_hint=False,
146 ):
150 ):
147 """Create a new dirstate object.
151 """Create a new dirstate object.
148
152
149 opener is an open()-like callable that can be used to open the
153 opener is an open()-like callable that can be used to open the
150 dirstate file; root is the root of the directory tracked by
154 dirstate file; root is the root of the directory tracked by
151 the dirstate.
155 the dirstate.
152 """
156 """
153 self._use_dirstate_v2 = use_dirstate_v2
157 self._use_dirstate_v2 = use_dirstate_v2
154 self._use_tracked_hint = use_tracked_hint
158 self._use_tracked_hint = use_tracked_hint
155 self._nodeconstants = nodeconstants
159 self._nodeconstants = nodeconstants
156 self._opener = opener
160 self._opener = opener
157 self._validate = validate
161 self._validate = validate
158 self._root = root
162 self._root = root
159 # Either build a sparse-matcher or None if sparse is disabled
163 # Either build a sparse-matcher or None if sparse is disabled
160 self._sparsematchfn = sparsematchfn
164 self._sparsematchfn = sparsematchfn
161 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
165 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
162 # UNC path pointing to root share (issue4557)
166 # UNC path pointing to root share (issue4557)
163 self._rootdir = pathutil.normasprefix(root)
167 self._rootdir = pathutil.normasprefix(root)
164 # True is any internal state may be different
168 # True is any internal state may be different
165 self._dirty = False
169 self._dirty = False
166 # True if the set of tracked file may be different
170 # True if the set of tracked file may be different
167 self._dirty_tracked_set = False
171 self._dirty_tracked_set = False
168 self._ui = ui
172 self._ui = ui
169 self._filecache = {}
173 self._filecache = {}
170 # nesting level of `changing_parents` context
174 # nesting level of `changing_parents` context
171 self._changing_level = 0
175 self._changing_level = 0
172 # the change currently underway
176 # the change currently underway
173 self._change_type = None
177 self._change_type = None
174 # True if the current dirstate changing operations have been
178 # True if the current dirstate changing operations have been
175 # invalidated (used to make sure all nested contexts have been exited)
179 # invalidated (used to make sure all nested contexts have been exited)
176 self._invalidated_context = False
180 self._invalidated_context = False
177 self._attached_to_a_transaction = False
181 self._attached_to_a_transaction = False
178 self._filename = b'dirstate'
182 self._filename = b'dirstate'
179 self._filename_th = b'dirstate-tracked-hint'
183 self._filename_th = b'dirstate-tracked-hint'
180 self._pendingfilename = b'%s.pending' % self._filename
184 self._pendingfilename = b'%s.pending' % self._filename
181 self._plchangecallbacks = {}
185 self._plchangecallbacks = {}
182 self._origpl = None
186 self._origpl = None
183 self._mapcls = dirstatemap.dirstatemap
187 self._mapcls = dirstatemap.dirstatemap
184 # Access and cache cwd early, so we don't access it for the first time
188 # Access and cache cwd early, so we don't access it for the first time
185 # after a working-copy update caused it to not exist (accessing it then
189 # after a working-copy update caused it to not exist (accessing it then
186 # raises an exception).
190 # raises an exception).
187 self._cwd
191 self._cwd
188
192
189 def prefetch_parents(self):
193 def prefetch_parents(self):
190 """make sure the parents are loaded
194 """make sure the parents are loaded
191
195
192 Used to avoid a race condition.
196 Used to avoid a race condition.
193 """
197 """
194 self._pl
198 self._pl
195
199
196 @contextlib.contextmanager
200 @contextlib.contextmanager
197 @check_invalidated
201 @check_invalidated
198 def _changing(self, repo, change_type):
202 def _changing(self, repo, change_type):
199 if repo.currentwlock() is None:
203 if repo.currentwlock() is None:
200 msg = b"trying to change the dirstate without holding the wlock"
204 msg = b"trying to change the dirstate without holding the wlock"
201 raise error.ProgrammingError(msg)
205 raise error.ProgrammingError(msg)
202
206
203 has_tr = repo.currenttransaction() is not None
207 has_tr = repo.currenttransaction() is not None
204 if not has_tr and self._changing_level == 0 and self._dirty:
208 if not has_tr and self._changing_level == 0 and self._dirty:
205 msg = "entering a changing context, but dirstate is already dirty"
209 msg = "entering a changing context, but dirstate is already dirty"
206 raise error.ProgrammingError(msg)
210 raise error.ProgrammingError(msg)
207
211
208 assert self._changing_level >= 0
212 assert self._changing_level >= 0
209 # different type of change are mutually exclusive
213 # different type of change are mutually exclusive
210 if self._change_type is None:
214 if self._change_type is None:
211 assert self._changing_level == 0
215 assert self._changing_level == 0
212 self._change_type = change_type
216 self._change_type = change_type
213 elif self._change_type != change_type:
217 elif self._change_type != change_type:
214 msg = (
218 msg = (
215 'trying to open "%s" dirstate-changing context while a "%s" is'
219 'trying to open "%s" dirstate-changing context while a "%s" is'
216 ' already open'
220 ' already open'
217 )
221 )
218 msg %= (change_type, self._change_type)
222 msg %= (change_type, self._change_type)
219 raise error.ProgrammingError(msg)
223 raise error.ProgrammingError(msg)
220 should_write = False
224 should_write = False
221 self._changing_level += 1
225 self._changing_level += 1
222 try:
226 try:
223 yield
227 yield
224 except: # re-raises
228 except: # re-raises
225 self.invalidate() # this will set `_invalidated_context`
229 self.invalidate() # this will set `_invalidated_context`
226 raise
230 raise
227 finally:
231 finally:
228 assert self._changing_level > 0
232 assert self._changing_level > 0
229 self._changing_level -= 1
233 self._changing_level -= 1
230 # If the dirstate is being invalidated, call invalidate again.
234 # If the dirstate is being invalidated, call invalidate again.
231 # This will throw away anything added by a upper context and
235 # This will throw away anything added by a upper context and
232 # reset the `_invalidated_context` flag when relevant
236 # reset the `_invalidated_context` flag when relevant
233 if self._changing_level <= 0:
237 if self._changing_level <= 0:
234 self._change_type = None
238 self._change_type = None
235 assert self._changing_level == 0
239 assert self._changing_level == 0
236 if self._invalidated_context:
240 if self._invalidated_context:
237 # make sure we invalidate anything an upper context might
241 # make sure we invalidate anything an upper context might
238 # have changed.
242 # have changed.
239 self.invalidate()
243 self.invalidate()
240 else:
244 else:
241 should_write = self._changing_level <= 0
245 should_write = self._changing_level <= 0
242 tr = repo.currenttransaction()
246 tr = repo.currenttransaction()
243 if has_tr != (tr is not None):
247 if has_tr != (tr is not None):
244 if has_tr:
248 if has_tr:
245 m = "transaction vanished while changing dirstate"
249 m = "transaction vanished while changing dirstate"
246 else:
250 else:
247 m = "transaction appeared while changing dirstate"
251 m = "transaction appeared while changing dirstate"
248 raise error.ProgrammingError(m)
252 raise error.ProgrammingError(m)
249 if should_write:
253 if should_write:
250 self.write(tr)
254 self.write(tr)
251
255
252 @contextlib.contextmanager
256 @contextlib.contextmanager
253 def changing_parents(self, repo):
257 def changing_parents(self, repo):
254 with self._changing(repo, CHANGE_TYPE_PARENTS) as c:
258 with self._changing(repo, CHANGE_TYPE_PARENTS) as c:
255 yield c
259 yield c
256
260
257 @contextlib.contextmanager
261 @contextlib.contextmanager
258 def changing_files(self, repo):
262 def changing_files(self, repo):
259 with self._changing(repo, CHANGE_TYPE_FILES) as c:
263 with self._changing(repo, CHANGE_TYPE_FILES) as c:
260 yield c
264 yield c
261
265
262 # here to help migration to the new code
266 # here to help migration to the new code
263 def parentchange(self):
267 def parentchange(self):
264 msg = (
268 msg = (
265 "Mercurial 6.4 and later requires call to "
269 "Mercurial 6.4 and later requires call to "
266 "`dirstate.changing_parents(repo)`"
270 "`dirstate.changing_parents(repo)`"
267 )
271 )
268 raise error.ProgrammingError(msg)
272 raise error.ProgrammingError(msg)
269
273
270 @property
274 @property
271 def is_changing_any(self):
275 def is_changing_any(self):
272 """Returns true if the dirstate is in the middle of a set of changes.
276 """Returns true if the dirstate is in the middle of a set of changes.
273
277
274 This returns True for any kind of change.
278 This returns True for any kind of change.
275 """
279 """
276 return self._changing_level > 0
280 return self._changing_level > 0
277
281
278 def pendingparentchange(self):
282 def pendingparentchange(self):
279 return self.is_changing_parent()
283 return self.is_changing_parent()
280
284
281 def is_changing_parent(self):
285 def is_changing_parent(self):
282 """Returns true if the dirstate is in the middle of a set of changes
286 """Returns true if the dirstate is in the middle of a set of changes
283 that modify the dirstate parent.
287 that modify the dirstate parent.
284 """
288 """
285 self._ui.deprecwarn(b"dirstate.is_changing_parents", b"6.5")
289 self._ui.deprecwarn(b"dirstate.is_changing_parents", b"6.5")
286 return self.is_changing_parents
290 return self.is_changing_parents
287
291
288 @property
292 @property
289 def is_changing_parents(self):
293 def is_changing_parents(self):
290 """Returns true if the dirstate is in the middle of a set of changes
294 """Returns true if the dirstate is in the middle of a set of changes
291 that modify the dirstate parent.
295 that modify the dirstate parent.
292 """
296 """
293 if self._changing_level <= 0:
297 if self._changing_level <= 0:
294 return False
298 return False
295 return self._change_type == CHANGE_TYPE_PARENTS
299 return self._change_type == CHANGE_TYPE_PARENTS
296
300
297 @property
301 @property
298 def is_changing_files(self):
302 def is_changing_files(self):
299 """Returns true if the dirstate is in the middle of a set of changes
303 """Returns true if the dirstate is in the middle of a set of changes
300 that modify the files tracked or their sources.
304 that modify the files tracked or their sources.
301 """
305 """
302 if self._changing_level <= 0:
306 if self._changing_level <= 0:
303 return False
307 return False
304 return self._change_type == CHANGE_TYPE_FILES
308 return self._change_type == CHANGE_TYPE_FILES
305
309
306 @propertycache
310 @propertycache
307 def _map(self):
311 def _map(self):
308 """Return the dirstate contents (see documentation for dirstatemap)."""
312 """Return the dirstate contents (see documentation for dirstatemap)."""
309 self._map = self._mapcls(
313 self._map = self._mapcls(
310 self._ui,
314 self._ui,
311 self._opener,
315 self._opener,
312 self._root,
316 self._root,
313 self._nodeconstants,
317 self._nodeconstants,
314 self._use_dirstate_v2,
318 self._use_dirstate_v2,
315 )
319 )
316 return self._map
320 return self._map
317
321
318 @property
322 @property
319 def _sparsematcher(self):
323 def _sparsematcher(self):
320 """The matcher for the sparse checkout.
324 """The matcher for the sparse checkout.
321
325
322 The working directory may not include every file from a manifest. The
326 The working directory may not include every file from a manifest. The
323 matcher obtained by this property will match a path if it is to be
327 matcher obtained by this property will match a path if it is to be
324 included in the working directory.
328 included in the working directory.
325
329
326 When sparse if disabled, return None.
330 When sparse if disabled, return None.
327 """
331 """
328 if self._sparsematchfn is None:
332 if self._sparsematchfn is None:
329 return None
333 return None
330 # TODO there is potential to cache this property. For now, the matcher
334 # TODO there is potential to cache this property. For now, the matcher
331 # is resolved on every access. (But the called function does use a
335 # is resolved on every access. (But the called function does use a
332 # cache to keep the lookup fast.)
336 # cache to keep the lookup fast.)
333 return self._sparsematchfn()
337 return self._sparsematchfn()
334
338
335 @repocache(b'branch')
339 @repocache(b'branch')
336 def _branch(self):
340 def _branch(self):
337 try:
341 try:
338 return self._opener.read(b"branch").strip() or b"default"
342 return self._opener.read(b"branch").strip() or b"default"
339 except FileNotFoundError:
343 except FileNotFoundError:
340 return b"default"
344 return b"default"
341
345
342 @property
346 @property
343 def _pl(self):
347 def _pl(self):
344 return self._map.parents()
348 return self._map.parents()
345
349
346 def hasdir(self, d):
350 def hasdir(self, d):
347 return self._map.hastrackeddir(d)
351 return self._map.hastrackeddir(d)
348
352
349 @rootcache(b'.hgignore')
353 @rootcache(b'.hgignore')
350 def _ignore(self):
354 def _ignore(self):
351 files = self._ignorefiles()
355 files = self._ignorefiles()
352 if not files:
356 if not files:
353 return matchmod.never()
357 return matchmod.never()
354
358
355 pats = [b'include:%s' % f for f in files]
359 pats = [b'include:%s' % f for f in files]
356 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
360 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
357
361
358 @propertycache
362 @propertycache
359 def _slash(self):
363 def _slash(self):
360 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
364 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
361
365
362 @propertycache
366 @propertycache
363 def _checklink(self):
367 def _checklink(self):
364 return util.checklink(self._root)
368 return util.checklink(self._root)
365
369
366 @propertycache
370 @propertycache
367 def _checkexec(self):
371 def _checkexec(self):
368 return bool(util.checkexec(self._root))
372 return bool(util.checkexec(self._root))
369
373
370 @propertycache
374 @propertycache
371 def _checkcase(self):
375 def _checkcase(self):
372 return not util.fscasesensitive(self._join(b'.hg'))
376 return not util.fscasesensitive(self._join(b'.hg'))
373
377
374 def _join(self, f):
378 def _join(self, f):
375 # much faster than os.path.join()
379 # much faster than os.path.join()
376 # it's safe because f is always a relative path
380 # it's safe because f is always a relative path
377 return self._rootdir + f
381 return self._rootdir + f
378
382
379 def flagfunc(self, buildfallback):
383 def flagfunc(self, buildfallback):
380 """build a callable that returns flags associated with a filename
384 """build a callable that returns flags associated with a filename
381
385
382 The information is extracted from three possible layers:
386 The information is extracted from three possible layers:
383 1. the file system if it supports the information
387 1. the file system if it supports the information
384 2. the "fallback" information stored in the dirstate if any
388 2. the "fallback" information stored in the dirstate if any
385 3. a more expensive mechanism inferring the flags from the parents.
389 3. a more expensive mechanism inferring the flags from the parents.
386 """
390 """
387
391
388 # small hack to cache the result of buildfallback()
392 # small hack to cache the result of buildfallback()
389 fallback_func = []
393 fallback_func = []
390
394
391 def get_flags(x):
395 def get_flags(x):
392 entry = None
396 entry = None
393 fallback_value = None
397 fallback_value = None
394 try:
398 try:
395 st = os.lstat(self._join(x))
399 st = os.lstat(self._join(x))
396 except OSError:
400 except OSError:
397 return b''
401 return b''
398
402
399 if self._checklink:
403 if self._checklink:
400 if util.statislink(st):
404 if util.statislink(st):
401 return b'l'
405 return b'l'
402 else:
406 else:
403 entry = self.get_entry(x)
407 entry = self.get_entry(x)
404 if entry.has_fallback_symlink:
408 if entry.has_fallback_symlink:
405 if entry.fallback_symlink:
409 if entry.fallback_symlink:
406 return b'l'
410 return b'l'
407 else:
411 else:
408 if not fallback_func:
412 if not fallback_func:
409 fallback_func.append(buildfallback())
413 fallback_func.append(buildfallback())
410 fallback_value = fallback_func[0](x)
414 fallback_value = fallback_func[0](x)
411 if b'l' in fallback_value:
415 if b'l' in fallback_value:
412 return b'l'
416 return b'l'
413
417
414 if self._checkexec:
418 if self._checkexec:
415 if util.statisexec(st):
419 if util.statisexec(st):
416 return b'x'
420 return b'x'
417 else:
421 else:
418 if entry is None:
422 if entry is None:
419 entry = self.get_entry(x)
423 entry = self.get_entry(x)
420 if entry.has_fallback_exec:
424 if entry.has_fallback_exec:
421 if entry.fallback_exec:
425 if entry.fallback_exec:
422 return b'x'
426 return b'x'
423 else:
427 else:
424 if fallback_value is None:
428 if fallback_value is None:
425 if not fallback_func:
429 if not fallback_func:
426 fallback_func.append(buildfallback())
430 fallback_func.append(buildfallback())
427 fallback_value = fallback_func[0](x)
431 fallback_value = fallback_func[0](x)
428 if b'x' in fallback_value:
432 if b'x' in fallback_value:
429 return b'x'
433 return b'x'
430 return b''
434 return b''
431
435
432 return get_flags
436 return get_flags
433
437
434 @propertycache
438 @propertycache
435 def _cwd(self):
439 def _cwd(self):
436 # internal config: ui.forcecwd
440 # internal config: ui.forcecwd
437 forcecwd = self._ui.config(b'ui', b'forcecwd')
441 forcecwd = self._ui.config(b'ui', b'forcecwd')
438 if forcecwd:
442 if forcecwd:
439 return forcecwd
443 return forcecwd
440 return encoding.getcwd()
444 return encoding.getcwd()
441
445
442 def getcwd(self):
446 def getcwd(self):
443 """Return the path from which a canonical path is calculated.
447 """Return the path from which a canonical path is calculated.
444
448
445 This path should be used to resolve file patterns or to convert
449 This path should be used to resolve file patterns or to convert
446 canonical paths back to file paths for display. It shouldn't be
450 canonical paths back to file paths for display. It shouldn't be
447 used to get real file paths. Use vfs functions instead.
451 used to get real file paths. Use vfs functions instead.
448 """
452 """
449 cwd = self._cwd
453 cwd = self._cwd
450 if cwd == self._root:
454 if cwd == self._root:
451 return b''
455 return b''
452 # self._root ends with a path separator if self._root is '/' or 'C:\'
456 # self._root ends with a path separator if self._root is '/' or 'C:\'
453 rootsep = self._root
457 rootsep = self._root
454 if not util.endswithsep(rootsep):
458 if not util.endswithsep(rootsep):
455 rootsep += pycompat.ossep
459 rootsep += pycompat.ossep
456 if cwd.startswith(rootsep):
460 if cwd.startswith(rootsep):
457 return cwd[len(rootsep) :]
461 return cwd[len(rootsep) :]
458 else:
462 else:
459 # we're outside the repo. return an absolute path.
463 # we're outside the repo. return an absolute path.
460 return cwd
464 return cwd
461
465
462 def pathto(self, f, cwd=None):
466 def pathto(self, f, cwd=None):
463 if cwd is None:
467 if cwd is None:
464 cwd = self.getcwd()
468 cwd = self.getcwd()
465 path = util.pathto(self._root, cwd, f)
469 path = util.pathto(self._root, cwd, f)
466 if self._slash:
470 if self._slash:
467 return util.pconvert(path)
471 return util.pconvert(path)
468 return path
472 return path
469
473
470 def get_entry(self, path):
474 def get_entry(self, path):
471 """return a DirstateItem for the associated path"""
475 """return a DirstateItem for the associated path"""
472 entry = self._map.get(path)
476 entry = self._map.get(path)
473 if entry is None:
477 if entry is None:
474 return DirstateItem()
478 return DirstateItem()
475 return entry
479 return entry
476
480
477 def __contains__(self, key):
481 def __contains__(self, key):
478 return key in self._map
482 return key in self._map
479
483
480 def __iter__(self):
484 def __iter__(self):
481 return iter(sorted(self._map))
485 return iter(sorted(self._map))
482
486
483 def items(self):
487 def items(self):
484 return self._map.items()
488 return self._map.items()
485
489
486 iteritems = items
490 iteritems = items
487
491
488 def parents(self):
492 def parents(self):
489 return [self._validate(p) for p in self._pl]
493 return [self._validate(p) for p in self._pl]
490
494
491 def p1(self):
495 def p1(self):
492 return self._validate(self._pl[0])
496 return self._validate(self._pl[0])
493
497
494 def p2(self):
498 def p2(self):
495 return self._validate(self._pl[1])
499 return self._validate(self._pl[1])
496
500
497 @property
501 @property
498 def in_merge(self):
502 def in_merge(self):
499 """True if a merge is in progress"""
503 """True if a merge is in progress"""
500 return self._pl[1] != self._nodeconstants.nullid
504 return self._pl[1] != self._nodeconstants.nullid
501
505
502 def branch(self):
506 def branch(self):
503 return encoding.tolocal(self._branch)
507 return encoding.tolocal(self._branch)
504
508
505 @requires_changing_parents
509 @requires_changing_parents
506 def setparents(self, p1, p2=None):
510 def setparents(self, p1, p2=None):
507 """Set dirstate parents to p1 and p2.
511 """Set dirstate parents to p1 and p2.
508
512
509 When moving from two parents to one, "merged" entries a
513 When moving from two parents to one, "merged" entries a
510 adjusted to normal and previous copy records discarded and
514 adjusted to normal and previous copy records discarded and
511 returned by the call.
515 returned by the call.
512
516
513 See localrepo.setparents()
517 See localrepo.setparents()
514 """
518 """
515 if p2 is None:
519 if p2 is None:
516 p2 = self._nodeconstants.nullid
520 p2 = self._nodeconstants.nullid
517 if self._changing_level == 0:
521 if self._changing_level == 0:
518 raise ValueError(
522 raise ValueError(
519 b"cannot set dirstate parent outside of "
523 b"cannot set dirstate parent outside of "
520 b"dirstate.changing_parents context manager"
524 b"dirstate.changing_parents context manager"
521 )
525 )
522
526
523 self._dirty = True
527 self._dirty = True
524 oldp2 = self._pl[1]
528 oldp2 = self._pl[1]
525 if self._origpl is None:
529 if self._origpl is None:
526 self._origpl = self._pl
530 self._origpl = self._pl
527 nullid = self._nodeconstants.nullid
531 nullid = self._nodeconstants.nullid
528 # True if we need to fold p2 related state back to a linear case
532 # True if we need to fold p2 related state back to a linear case
529 fold_p2 = oldp2 != nullid and p2 == nullid
533 fold_p2 = oldp2 != nullid and p2 == nullid
530 return self._map.setparents(p1, p2, fold_p2=fold_p2)
534 return self._map.setparents(p1, p2, fold_p2=fold_p2)
531
535
532 def setbranch(self, branch):
536 def setbranch(self, branch):
533 self.__class__._branch.set(self, encoding.fromlocal(branch))
537 self.__class__._branch.set(self, encoding.fromlocal(branch))
534 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
538 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
535 try:
539 try:
536 f.write(self._branch + b'\n')
540 f.write(self._branch + b'\n')
537 f.close()
541 f.close()
538
542
539 # make sure filecache has the correct stat info for _branch after
543 # make sure filecache has the correct stat info for _branch after
540 # replacing the underlying file
544 # replacing the underlying file
541 ce = self._filecache[b'_branch']
545 ce = self._filecache[b'_branch']
542 if ce:
546 if ce:
543 ce.refresh()
547 ce.refresh()
544 except: # re-raises
548 except: # re-raises
545 f.discard()
549 f.discard()
546 raise
550 raise
547
551
548 def invalidate(self):
552 def invalidate(self):
549 """Causes the next access to reread the dirstate.
553 """Causes the next access to reread the dirstate.
550
554
551 This is different from localrepo.invalidatedirstate() because it always
555 This is different from localrepo.invalidatedirstate() because it always
552 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
556 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
553 check whether the dirstate has changed before rereading it."""
557 check whether the dirstate has changed before rereading it."""
554
558
555 for a in ("_map", "_branch", "_ignore"):
559 for a in ("_map", "_branch", "_ignore"):
556 if a in self.__dict__:
560 if a in self.__dict__:
557 delattr(self, a)
561 delattr(self, a)
558 self._dirty = False
562 self._dirty = False
559 self._dirty_tracked_set = False
563 self._dirty_tracked_set = False
560 self._invalidated_context = (
564 self._invalidated_context = (
561 self._changing_level > 0 or self._attached_to_a_transaction
565 self._changing_level > 0 or self._attached_to_a_transaction
562 )
566 )
563 self._origpl = None
567 self._origpl = None
564
568
565 @requires_changing_any
569 @requires_changing_any
566 def copy(self, source, dest):
570 def copy(self, source, dest):
567 """Mark dest as a copy of source. Unmark dest if source is None."""
571 """Mark dest as a copy of source. Unmark dest if source is None."""
568 if source == dest:
572 if source == dest:
569 return
573 return
570 self._dirty = True
574 self._dirty = True
571 if source is not None:
575 if source is not None:
572 self._check_sparse(source)
576 self._check_sparse(source)
573 self._map.copymap[dest] = source
577 self._map.copymap[dest] = source
574 else:
578 else:
575 self._map.copymap.pop(dest, None)
579 self._map.copymap.pop(dest, None)
576
580
577 def copied(self, file):
581 def copied(self, file):
578 return self._map.copymap.get(file, None)
582 return self._map.copymap.get(file, None)
579
583
580 def copies(self):
584 def copies(self):
581 return self._map.copymap
585 return self._map.copymap
582
586
583 @requires_changing_files
587 @requires_changing_files
584 def set_tracked(self, filename, reset_copy=False):
588 def set_tracked(self, filename, reset_copy=False):
585 """a "public" method for generic code to mark a file as tracked
589 """a "public" method for generic code to mark a file as tracked
586
590
587 This function is to be called outside of "update/merge" case. For
591 This function is to be called outside of "update/merge" case. For
588 example by a command like `hg add X`.
592 example by a command like `hg add X`.
589
593
590 if reset_copy is set, any existing copy information will be dropped.
594 if reset_copy is set, any existing copy information will be dropped.
591
595
592 return True the file was previously untracked, False otherwise.
596 return True the file was previously untracked, False otherwise.
593 """
597 """
594 self._dirty = True
598 self._dirty = True
595 entry = self._map.get(filename)
599 entry = self._map.get(filename)
596 if entry is None or not entry.tracked:
600 if entry is None or not entry.tracked:
597 self._check_new_tracked_filename(filename)
601 self._check_new_tracked_filename(filename)
598 pre_tracked = self._map.set_tracked(filename)
602 pre_tracked = self._map.set_tracked(filename)
599 if reset_copy:
603 if reset_copy:
600 self._map.copymap.pop(filename, None)
604 self._map.copymap.pop(filename, None)
601 if pre_tracked:
605 if pre_tracked:
602 self._dirty_tracked_set = True
606 self._dirty_tracked_set = True
603 return pre_tracked
607 return pre_tracked
604
608
605 @requires_changing_files
609 @requires_changing_files
606 def set_untracked(self, filename):
610 def set_untracked(self, filename):
607 """a "public" method for generic code to mark a file as untracked
611 """a "public" method for generic code to mark a file as untracked
608
612
609 This function is to be called outside of "update/merge" case. For
613 This function is to be called outside of "update/merge" case. For
610 example by a command like `hg remove X`.
614 example by a command like `hg remove X`.
611
615
612 return True the file was previously tracked, False otherwise.
616 return True the file was previously tracked, False otherwise.
613 """
617 """
614 ret = self._map.set_untracked(filename)
618 ret = self._map.set_untracked(filename)
615 if ret:
619 if ret:
616 self._dirty = True
620 self._dirty = True
617 self._dirty_tracked_set = True
621 self._dirty_tracked_set = True
618 return ret
622 return ret
619
623
620 @requires_not_changing_parents
624 @requires_not_changing_parents
621 def set_clean(self, filename, parentfiledata):
625 def set_clean(self, filename, parentfiledata):
622 """record that the current state of the file on disk is known to be clean"""
626 """record that the current state of the file on disk is known to be clean"""
623 self._dirty = True
627 self._dirty = True
624 if not self._map[filename].tracked:
628 if not self._map[filename].tracked:
625 self._check_new_tracked_filename(filename)
629 self._check_new_tracked_filename(filename)
626 (mode, size, mtime) = parentfiledata
630 (mode, size, mtime) = parentfiledata
627 self._map.set_clean(filename, mode, size, mtime)
631 self._map.set_clean(filename, mode, size, mtime)
628
632
629 @requires_not_changing_parents
633 @requires_not_changing_parents
630 def set_possibly_dirty(self, filename):
634 def set_possibly_dirty(self, filename):
631 """record that the current state of the file on disk is unknown"""
635 """record that the current state of the file on disk is unknown"""
632 self._dirty = True
636 self._dirty = True
633 self._map.set_possibly_dirty(filename)
637 self._map.set_possibly_dirty(filename)
634
638
635 @requires_changing_parents
639 @requires_changing_parents
636 def update_file_p1(
640 def update_file_p1(
637 self,
641 self,
638 filename,
642 filename,
639 p1_tracked,
643 p1_tracked,
640 ):
644 ):
641 """Set a file as tracked in the parent (or not)
645 """Set a file as tracked in the parent (or not)
642
646
643 This is to be called when adjust the dirstate to a new parent after an history
647 This is to be called when adjust the dirstate to a new parent after an history
644 rewriting operation.
648 rewriting operation.
645
649
646 It should not be called during a merge (p2 != nullid) and only within
650 It should not be called during a merge (p2 != nullid) and only within
647 a `with dirstate.changing_parents(repo):` context.
651 a `with dirstate.changing_parents(repo):` context.
648 """
652 """
649 if self.in_merge:
653 if self.in_merge:
650 msg = b'update_file_reference should not be called when merging'
654 msg = b'update_file_reference should not be called when merging'
651 raise error.ProgrammingError(msg)
655 raise error.ProgrammingError(msg)
652 entry = self._map.get(filename)
656 entry = self._map.get(filename)
653 if entry is None:
657 if entry is None:
654 wc_tracked = False
658 wc_tracked = False
655 else:
659 else:
656 wc_tracked = entry.tracked
660 wc_tracked = entry.tracked
657 if not (p1_tracked or wc_tracked):
661 if not (p1_tracked or wc_tracked):
658 # the file is no longer relevant to anyone
662 # the file is no longer relevant to anyone
659 if self._map.get(filename) is not None:
663 if self._map.get(filename) is not None:
660 self._map.reset_state(filename)
664 self._map.reset_state(filename)
661 self._dirty = True
665 self._dirty = True
662 elif (not p1_tracked) and wc_tracked:
666 elif (not p1_tracked) and wc_tracked:
663 if entry is not None and entry.added:
667 if entry is not None and entry.added:
664 return # avoid dropping copy information (maybe?)
668 return # avoid dropping copy information (maybe?)
665
669
666 self._map.reset_state(
670 self._map.reset_state(
667 filename,
671 filename,
668 wc_tracked,
672 wc_tracked,
669 p1_tracked,
673 p1_tracked,
670 # the underlying reference might have changed, we will have to
674 # the underlying reference might have changed, we will have to
671 # check it.
675 # check it.
672 has_meaningful_mtime=False,
676 has_meaningful_mtime=False,
673 )
677 )
674
678
675 @requires_changing_parents
679 @requires_changing_parents
676 def update_file(
680 def update_file(
677 self,
681 self,
678 filename,
682 filename,
679 wc_tracked,
683 wc_tracked,
680 p1_tracked,
684 p1_tracked,
681 p2_info=False,
685 p2_info=False,
682 possibly_dirty=False,
686 possibly_dirty=False,
683 parentfiledata=None,
687 parentfiledata=None,
684 ):
688 ):
685 """update the information about a file in the dirstate
689 """update the information about a file in the dirstate
686
690
687 This is to be called when the direstates parent changes to keep track
691 This is to be called when the direstates parent changes to keep track
688 of what is the file situation in regards to the working copy and its parent.
692 of what is the file situation in regards to the working copy and its parent.
689
693
690 This function must be called within a `dirstate.changing_parents` context.
694 This function must be called within a `dirstate.changing_parents` context.
691
695
692 note: the API is at an early stage and we might need to adjust it
696 note: the API is at an early stage and we might need to adjust it
693 depending of what information ends up being relevant and useful to
697 depending of what information ends up being relevant and useful to
694 other processing.
698 other processing.
695 """
699 """
696 self._update_file(
700 self._update_file(
697 filename=filename,
701 filename=filename,
698 wc_tracked=wc_tracked,
702 wc_tracked=wc_tracked,
699 p1_tracked=p1_tracked,
703 p1_tracked=p1_tracked,
700 p2_info=p2_info,
704 p2_info=p2_info,
701 possibly_dirty=possibly_dirty,
705 possibly_dirty=possibly_dirty,
702 parentfiledata=parentfiledata,
706 parentfiledata=parentfiledata,
703 )
707 )
704
708
705 # XXX since this make the dirstate dirty, we should enforce that it is done
709 # XXX since this make the dirstate dirty, we should enforce that it is done
706 # withing an appropriate change-context that scope the change and ensure it
710 # withing an appropriate change-context that scope the change and ensure it
707 # eventually get written on disk (or rolled back)
711 # eventually get written on disk (or rolled back)
708 def hacky_extension_update_file(self, *args, **kwargs):
712 def hacky_extension_update_file(self, *args, **kwargs):
709 """NEVER USE THIS, YOU DO NOT NEED IT
713 """NEVER USE THIS, YOU DO NOT NEED IT
710
714
711 This function is a variant of "update_file" to be called by a small set
715 This function is a variant of "update_file" to be called by a small set
712 of extensions, it also adjust the internal state of file, but can be
716 of extensions, it also adjust the internal state of file, but can be
713 called outside an `changing_parents` context.
717 called outside an `changing_parents` context.
714
718
715 A very small number of extension meddle with the working copy content
719 A very small number of extension meddle with the working copy content
716 in a way that requires to adjust the dirstate accordingly. At the time
720 in a way that requires to adjust the dirstate accordingly. At the time
717 this command is written they are :
721 this command is written they are :
718 - keyword,
722 - keyword,
719 - largefile,
723 - largefile,
720 PLEASE DO NOT GROW THIS LIST ANY FURTHER.
724 PLEASE DO NOT GROW THIS LIST ANY FURTHER.
721
725
722 This function could probably be replaced by more semantic one (like
726 This function could probably be replaced by more semantic one (like
723 "adjust expected size" or "always revalidate file content", etc)
727 "adjust expected size" or "always revalidate file content", etc)
724 however at the time where this is writen, this is too much of a detour
728 however at the time where this is writen, this is too much of a detour
725 to be considered.
729 to be considered.
726 """
730 """
727 self._update_file(
731 self._update_file(
728 *args,
732 *args,
729 **kwargs,
733 **kwargs,
730 )
734 )
731
735
732 def _update_file(
736 def _update_file(
733 self,
737 self,
734 filename,
738 filename,
735 wc_tracked,
739 wc_tracked,
736 p1_tracked,
740 p1_tracked,
737 p2_info=False,
741 p2_info=False,
738 possibly_dirty=False,
742 possibly_dirty=False,
739 parentfiledata=None,
743 parentfiledata=None,
740 ):
744 ):
741
745
742 # note: I do not think we need to double check name clash here since we
746 # note: I do not think we need to double check name clash here since we
743 # are in a update/merge case that should already have taken care of
747 # are in a update/merge case that should already have taken care of
744 # this. The test agrees
748 # this. The test agrees
745
749
746 self._dirty = True
750 self._dirty = True
747 old_entry = self._map.get(filename)
751 old_entry = self._map.get(filename)
748 if old_entry is None:
752 if old_entry is None:
749 prev_tracked = False
753 prev_tracked = False
750 else:
754 else:
751 prev_tracked = old_entry.tracked
755 prev_tracked = old_entry.tracked
752 if prev_tracked != wc_tracked:
756 if prev_tracked != wc_tracked:
753 self._dirty_tracked_set = True
757 self._dirty_tracked_set = True
754
758
755 self._map.reset_state(
759 self._map.reset_state(
756 filename,
760 filename,
757 wc_tracked,
761 wc_tracked,
758 p1_tracked,
762 p1_tracked,
759 p2_info=p2_info,
763 p2_info=p2_info,
760 has_meaningful_mtime=not possibly_dirty,
764 has_meaningful_mtime=not possibly_dirty,
761 parentfiledata=parentfiledata,
765 parentfiledata=parentfiledata,
762 )
766 )
763
767
764 def _check_new_tracked_filename(self, filename):
768 def _check_new_tracked_filename(self, filename):
765 scmutil.checkfilename(filename)
769 scmutil.checkfilename(filename)
766 if self._map.hastrackeddir(filename):
770 if self._map.hastrackeddir(filename):
767 msg = _(b'directory %r already in dirstate')
771 msg = _(b'directory %r already in dirstate')
768 msg %= pycompat.bytestr(filename)
772 msg %= pycompat.bytestr(filename)
769 raise error.Abort(msg)
773 raise error.Abort(msg)
770 # shadows
774 # shadows
771 for d in pathutil.finddirs(filename):
775 for d in pathutil.finddirs(filename):
772 if self._map.hastrackeddir(d):
776 if self._map.hastrackeddir(d):
773 break
777 break
774 entry = self._map.get(d)
778 entry = self._map.get(d)
775 if entry is not None and not entry.removed:
779 if entry is not None and not entry.removed:
776 msg = _(b'file %r in dirstate clashes with %r')
780 msg = _(b'file %r in dirstate clashes with %r')
777 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
781 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
778 raise error.Abort(msg)
782 raise error.Abort(msg)
779 self._check_sparse(filename)
783 self._check_sparse(filename)
780
784
781 def _check_sparse(self, filename):
785 def _check_sparse(self, filename):
782 """Check that a filename is inside the sparse profile"""
786 """Check that a filename is inside the sparse profile"""
783 sparsematch = self._sparsematcher
787 sparsematch = self._sparsematcher
784 if sparsematch is not None and not sparsematch.always():
788 if sparsematch is not None and not sparsematch.always():
785 if not sparsematch(filename):
789 if not sparsematch(filename):
786 msg = _(b"cannot add '%s' - it is outside the sparse checkout")
790 msg = _(b"cannot add '%s' - it is outside the sparse checkout")
787 hint = _(
791 hint = _(
788 b'include file with `hg debugsparse --include <pattern>` or use '
792 b'include file with `hg debugsparse --include <pattern>` or use '
789 b'`hg add -s <file>` to include file directory while adding'
793 b'`hg add -s <file>` to include file directory while adding'
790 )
794 )
791 raise error.Abort(msg % filename, hint=hint)
795 raise error.Abort(msg % filename, hint=hint)
792
796
793 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
797 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
794 if exists is None:
798 if exists is None:
795 exists = os.path.lexists(os.path.join(self._root, path))
799 exists = os.path.lexists(os.path.join(self._root, path))
796 if not exists:
800 if not exists:
797 # Maybe a path component exists
801 # Maybe a path component exists
798 if not ignoremissing and b'/' in path:
802 if not ignoremissing and b'/' in path:
799 d, f = path.rsplit(b'/', 1)
803 d, f = path.rsplit(b'/', 1)
800 d = self._normalize(d, False, ignoremissing, None)
804 d = self._normalize(d, False, ignoremissing, None)
801 folded = d + b"/" + f
805 folded = d + b"/" + f
802 else:
806 else:
803 # No path components, preserve original case
807 # No path components, preserve original case
804 folded = path
808 folded = path
805 else:
809 else:
806 # recursively normalize leading directory components
810 # recursively normalize leading directory components
807 # against dirstate
811 # against dirstate
808 if b'/' in normed:
812 if b'/' in normed:
809 d, f = normed.rsplit(b'/', 1)
813 d, f = normed.rsplit(b'/', 1)
810 d = self._normalize(d, False, ignoremissing, True)
814 d = self._normalize(d, False, ignoremissing, True)
811 r = self._root + b"/" + d
815 r = self._root + b"/" + d
812 folded = d + b"/" + util.fspath(f, r)
816 folded = d + b"/" + util.fspath(f, r)
813 else:
817 else:
814 folded = util.fspath(normed, self._root)
818 folded = util.fspath(normed, self._root)
815 storemap[normed] = folded
819 storemap[normed] = folded
816
820
817 return folded
821 return folded
818
822
819 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
823 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
820 normed = util.normcase(path)
824 normed = util.normcase(path)
821 folded = self._map.filefoldmap.get(normed, None)
825 folded = self._map.filefoldmap.get(normed, None)
822 if folded is None:
826 if folded is None:
823 if isknown:
827 if isknown:
824 folded = path
828 folded = path
825 else:
829 else:
826 folded = self._discoverpath(
830 folded = self._discoverpath(
827 path, normed, ignoremissing, exists, self._map.filefoldmap
831 path, normed, ignoremissing, exists, self._map.filefoldmap
828 )
832 )
829 return folded
833 return folded
830
834
831 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
835 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
832 normed = util.normcase(path)
836 normed = util.normcase(path)
833 folded = self._map.filefoldmap.get(normed, None)
837 folded = self._map.filefoldmap.get(normed, None)
834 if folded is None:
838 if folded is None:
835 folded = self._map.dirfoldmap.get(normed, None)
839 folded = self._map.dirfoldmap.get(normed, None)
836 if folded is None:
840 if folded is None:
837 if isknown:
841 if isknown:
838 folded = path
842 folded = path
839 else:
843 else:
840 # store discovered result in dirfoldmap so that future
844 # store discovered result in dirfoldmap so that future
841 # normalizefile calls don't start matching directories
845 # normalizefile calls don't start matching directories
842 folded = self._discoverpath(
846 folded = self._discoverpath(
843 path, normed, ignoremissing, exists, self._map.dirfoldmap
847 path, normed, ignoremissing, exists, self._map.dirfoldmap
844 )
848 )
845 return folded
849 return folded
846
850
847 def normalize(self, path, isknown=False, ignoremissing=False):
851 def normalize(self, path, isknown=False, ignoremissing=False):
848 """
852 """
849 normalize the case of a pathname when on a casefolding filesystem
853 normalize the case of a pathname when on a casefolding filesystem
850
854
851 isknown specifies whether the filename came from walking the
855 isknown specifies whether the filename came from walking the
852 disk, to avoid extra filesystem access.
856 disk, to avoid extra filesystem access.
853
857
854 If ignoremissing is True, missing path are returned
858 If ignoremissing is True, missing path are returned
855 unchanged. Otherwise, we try harder to normalize possibly
859 unchanged. Otherwise, we try harder to normalize possibly
856 existing path components.
860 existing path components.
857
861
858 The normalized case is determined based on the following precedence:
862 The normalized case is determined based on the following precedence:
859
863
860 - version of name already stored in the dirstate
864 - version of name already stored in the dirstate
861 - version of name stored on disk
865 - version of name stored on disk
862 - version provided via command arguments
866 - version provided via command arguments
863 """
867 """
864
868
865 if self._checkcase:
869 if self._checkcase:
866 return self._normalize(path, isknown, ignoremissing)
870 return self._normalize(path, isknown, ignoremissing)
867 return path
871 return path
868
872
869 # XXX this method is barely used, as a result:
873 # XXX this method is barely used, as a result:
870 # - its semantic is unclear
874 # - its semantic is unclear
871 # - do we really needs it ?
875 # - do we really needs it ?
872 @requires_changing_parents
876 @requires_changing_parents
873 def clear(self):
877 def clear(self):
874 self._map.clear()
878 self._map.clear()
875 self._dirty = True
879 self._dirty = True
876
880
877 @requires_changing_parents
881 @requires_changing_parents
878 def rebuild(self, parent, allfiles, changedfiles=None):
882 def rebuild(self, parent, allfiles, changedfiles=None):
879 matcher = self._sparsematcher
883 matcher = self._sparsematcher
880 if matcher is not None and not matcher.always():
884 if matcher is not None and not matcher.always():
881 # should not add non-matching files
885 # should not add non-matching files
882 allfiles = [f for f in allfiles if matcher(f)]
886 allfiles = [f for f in allfiles if matcher(f)]
883 if changedfiles:
887 if changedfiles:
884 changedfiles = [f for f in changedfiles if matcher(f)]
888 changedfiles = [f for f in changedfiles if matcher(f)]
885
889
886 if changedfiles is not None:
890 if changedfiles is not None:
887 # these files will be deleted from the dirstate when they are
891 # these files will be deleted from the dirstate when they are
888 # not found to be in allfiles
892 # not found to be in allfiles
889 dirstatefilestoremove = {f for f in self if not matcher(f)}
893 dirstatefilestoremove = {f for f in self if not matcher(f)}
890 changedfiles = dirstatefilestoremove.union(changedfiles)
894 changedfiles = dirstatefilestoremove.union(changedfiles)
891
895
892 if changedfiles is None:
896 if changedfiles is None:
893 # Rebuild entire dirstate
897 # Rebuild entire dirstate
894 to_lookup = allfiles
898 to_lookup = allfiles
895 to_drop = []
899 to_drop = []
896 self.clear()
900 self.clear()
897 elif len(changedfiles) < 10:
901 elif len(changedfiles) < 10:
898 # Avoid turning allfiles into a set, which can be expensive if it's
902 # Avoid turning allfiles into a set, which can be expensive if it's
899 # large.
903 # large.
900 to_lookup = []
904 to_lookup = []
901 to_drop = []
905 to_drop = []
902 for f in changedfiles:
906 for f in changedfiles:
903 if f in allfiles:
907 if f in allfiles:
904 to_lookup.append(f)
908 to_lookup.append(f)
905 else:
909 else:
906 to_drop.append(f)
910 to_drop.append(f)
907 else:
911 else:
908 changedfilesset = set(changedfiles)
912 changedfilesset = set(changedfiles)
909 to_lookup = changedfilesset & set(allfiles)
913 to_lookup = changedfilesset & set(allfiles)
910 to_drop = changedfilesset - to_lookup
914 to_drop = changedfilesset - to_lookup
911
915
912 if self._origpl is None:
916 if self._origpl is None:
913 self._origpl = self._pl
917 self._origpl = self._pl
914 self._map.setparents(parent, self._nodeconstants.nullid)
918 self._map.setparents(parent, self._nodeconstants.nullid)
915
919
916 for f in to_lookup:
920 for f in to_lookup:
917 if self.in_merge:
921 if self.in_merge:
918 self.set_tracked(f)
922 self.set_tracked(f)
919 else:
923 else:
920 self._map.reset_state(
924 self._map.reset_state(
921 f,
925 f,
922 wc_tracked=True,
926 wc_tracked=True,
923 p1_tracked=True,
927 p1_tracked=True,
924 )
928 )
925 for f in to_drop:
929 for f in to_drop:
926 self._map.reset_state(f)
930 self._map.reset_state(f)
927
931
928 self._dirty = True
932 self._dirty = True
929
933
930 def identity(self):
934 def identity(self):
931 """Return identity of dirstate itself to detect changing in storage
935 """Return identity of dirstate itself to detect changing in storage
932
936
933 If identity of previous dirstate is equal to this, writing
937 If identity of previous dirstate is equal to this, writing
934 changes based on the former dirstate out can keep consistency.
938 changes based on the former dirstate out can keep consistency.
935 """
939 """
936 return self._map.identity
940 return self._map.identity
937
941
938 def write(self, tr):
942 def write(self, tr):
939 if not self._dirty:
943 if not self._dirty:
940 return
944 return
941 # make sure we don't request a write of invalidated content
945 # make sure we don't request a write of invalidated content
942 # XXX move before the dirty check once `unlock` stop calling `write`
946 # XXX move before the dirty check once `unlock` stop calling `write`
943 assert not self._invalidated_context
947 assert not self._invalidated_context
944
948
945 write_key = self._use_tracked_hint and self._dirty_tracked_set
949 write_key = self._use_tracked_hint and self._dirty_tracked_set
946 if tr:
950 if tr:
947
951
948 def on_abort(tr):
952 def on_abort(tr):
949 self._attached_to_a_transaction = False
953 self._attached_to_a_transaction = False
950 self.invalidate()
954 self.invalidate()
951
955
952 # make sure we invalidate the current change on abort
956 # make sure we invalidate the current change on abort
953 if tr is not None:
957 if tr is not None:
954 tr.addabort(b'dirstate-invalidate', on_abort)
958 tr.addabort(
959 b'dirstate-invalidate%s' % self._tr_key_suffix,
960 on_abort,
961 )
955
962
956 self._attached_to_a_transaction = True
963 self._attached_to_a_transaction = True
957
964
958 def on_success(f):
965 def on_success(f):
959 self._attached_to_a_transaction = False
966 self._attached_to_a_transaction = False
960 self._writedirstate(tr, f),
967 self._writedirstate(tr, f),
961
968
962 # delay writing in-memory changes out
969 # delay writing in-memory changes out
963 tr.addfilegenerator(
970 tr.addfilegenerator(
964 b'dirstate-1-main',
971 b'dirstate-1-main%s' % self._tr_key_suffix,
965 (self._filename,),
972 (self._filename,),
966 on_success,
973 on_success,
967 location=b'plain',
974 location=b'plain',
968 post_finalize=True,
975 post_finalize=True,
969 )
976 )
970 if write_key:
977 if write_key:
971 tr.addfilegenerator(
978 tr.addfilegenerator(
972 b'dirstate-2-key-post',
979 b'dirstate-2-key-post%s' % self._tr_key_suffix,
973 (self._filename_th,),
980 (self._filename_th,),
974 lambda f: self._write_tracked_hint(tr, f),
981 lambda f: self._write_tracked_hint(tr, f),
975 location=b'plain',
982 location=b'plain',
976 post_finalize=True,
983 post_finalize=True,
977 )
984 )
978 return
985 return
979
986
980 file = lambda f: self._opener(f, b"w", atomictemp=True, checkambig=True)
987 file = lambda f: self._opener(f, b"w", atomictemp=True, checkambig=True)
981 with file(self._filename) as f:
988 with file(self._filename) as f:
982 self._writedirstate(tr, f)
989 self._writedirstate(tr, f)
983 if write_key:
990 if write_key:
984 # we update the key-file after writing to make sure reader have a
991 # we update the key-file after writing to make sure reader have a
985 # key that match the newly written content
992 # key that match the newly written content
986 with file(self._filename_th) as f:
993 with file(self._filename_th) as f:
987 self._write_tracked_hint(tr, f)
994 self._write_tracked_hint(tr, f)
988
995
989 def delete_tracked_hint(self):
996 def delete_tracked_hint(self):
990 """remove the tracked_hint file
997 """remove the tracked_hint file
991
998
992 To be used by format downgrades operation"""
999 To be used by format downgrades operation"""
993 self._opener.unlink(self._filename_th)
1000 self._opener.unlink(self._filename_th)
994 self._use_tracked_hint = False
1001 self._use_tracked_hint = False
995
1002
996 def addparentchangecallback(self, category, callback):
1003 def addparentchangecallback(self, category, callback):
997 """add a callback to be called when the wd parents are changed
1004 """add a callback to be called when the wd parents are changed
998
1005
999 Callback will be called with the following arguments:
1006 Callback will be called with the following arguments:
1000 dirstate, (oldp1, oldp2), (newp1, newp2)
1007 dirstate, (oldp1, oldp2), (newp1, newp2)
1001
1008
1002 Category is a unique identifier to allow overwriting an old callback
1009 Category is a unique identifier to allow overwriting an old callback
1003 with a newer callback.
1010 with a newer callback.
1004 """
1011 """
1005 self._plchangecallbacks[category] = callback
1012 self._plchangecallbacks[category] = callback
1006
1013
1007 def _writedirstate(self, tr, st):
1014 def _writedirstate(self, tr, st):
1008 # make sure we don't write invalidated content
1015 # make sure we don't write invalidated content
1009 assert not self._invalidated_context
1016 assert not self._invalidated_context
1010 # notify callbacks about parents change
1017 # notify callbacks about parents change
1011 if self._origpl is not None and self._origpl != self._pl:
1018 if self._origpl is not None and self._origpl != self._pl:
1012 for c, callback in sorted(self._plchangecallbacks.items()):
1019 for c, callback in sorted(self._plchangecallbacks.items()):
1013 callback(self, self._origpl, self._pl)
1020 callback(self, self._origpl, self._pl)
1014 self._origpl = None
1021 self._origpl = None
1015 self._map.write(tr, st)
1022 self._map.write(tr, st)
1016 self._dirty = False
1023 self._dirty = False
1017 self._dirty_tracked_set = False
1024 self._dirty_tracked_set = False
1018
1025
1019 def _write_tracked_hint(self, tr, f):
1026 def _write_tracked_hint(self, tr, f):
1020 key = node.hex(uuid.uuid4().bytes)
1027 key = node.hex(uuid.uuid4().bytes)
1021 f.write(b"1\n%s\n" % key) # 1 is the format version
1028 f.write(b"1\n%s\n" % key) # 1 is the format version
1022
1029
1023 def _dirignore(self, f):
1030 def _dirignore(self, f):
1024 if self._ignore(f):
1031 if self._ignore(f):
1025 return True
1032 return True
1026 for p in pathutil.finddirs(f):
1033 for p in pathutil.finddirs(f):
1027 if self._ignore(p):
1034 if self._ignore(p):
1028 return True
1035 return True
1029 return False
1036 return False
1030
1037
1031 def _ignorefiles(self):
1038 def _ignorefiles(self):
1032 files = []
1039 files = []
1033 if os.path.exists(self._join(b'.hgignore')):
1040 if os.path.exists(self._join(b'.hgignore')):
1034 files.append(self._join(b'.hgignore'))
1041 files.append(self._join(b'.hgignore'))
1035 for name, path in self._ui.configitems(b"ui"):
1042 for name, path in self._ui.configitems(b"ui"):
1036 if name == b'ignore' or name.startswith(b'ignore.'):
1043 if name == b'ignore' or name.startswith(b'ignore.'):
1037 # we need to use os.path.join here rather than self._join
1044 # we need to use os.path.join here rather than self._join
1038 # because path is arbitrary and user-specified
1045 # because path is arbitrary and user-specified
1039 files.append(os.path.join(self._rootdir, util.expandpath(path)))
1046 files.append(os.path.join(self._rootdir, util.expandpath(path)))
1040 return files
1047 return files
1041
1048
1042 def _ignorefileandline(self, f):
1049 def _ignorefileandline(self, f):
1043 files = collections.deque(self._ignorefiles())
1050 files = collections.deque(self._ignorefiles())
1044 visited = set()
1051 visited = set()
1045 while files:
1052 while files:
1046 i = files.popleft()
1053 i = files.popleft()
1047 patterns = matchmod.readpatternfile(
1054 patterns = matchmod.readpatternfile(
1048 i, self._ui.warn, sourceinfo=True
1055 i, self._ui.warn, sourceinfo=True
1049 )
1056 )
1050 for pattern, lineno, line in patterns:
1057 for pattern, lineno, line in patterns:
1051 kind, p = matchmod._patsplit(pattern, b'glob')
1058 kind, p = matchmod._patsplit(pattern, b'glob')
1052 if kind == b"subinclude":
1059 if kind == b"subinclude":
1053 if p not in visited:
1060 if p not in visited:
1054 files.append(p)
1061 files.append(p)
1055 continue
1062 continue
1056 m = matchmod.match(
1063 m = matchmod.match(
1057 self._root, b'', [], [pattern], warn=self._ui.warn
1064 self._root, b'', [], [pattern], warn=self._ui.warn
1058 )
1065 )
1059 if m(f):
1066 if m(f):
1060 return (i, lineno, line)
1067 return (i, lineno, line)
1061 visited.add(i)
1068 visited.add(i)
1062 return (None, -1, b"")
1069 return (None, -1, b"")
1063
1070
1064 def _walkexplicit(self, match, subrepos):
1071 def _walkexplicit(self, match, subrepos):
1065 """Get stat data about the files explicitly specified by match.
1072 """Get stat data about the files explicitly specified by match.
1066
1073
1067 Return a triple (results, dirsfound, dirsnotfound).
1074 Return a triple (results, dirsfound, dirsnotfound).
1068 - results is a mapping from filename to stat result. It also contains
1075 - results is a mapping from filename to stat result. It also contains
1069 listings mapping subrepos and .hg to None.
1076 listings mapping subrepos and .hg to None.
1070 - dirsfound is a list of files found to be directories.
1077 - dirsfound is a list of files found to be directories.
1071 - dirsnotfound is a list of files that the dirstate thinks are
1078 - dirsnotfound is a list of files that the dirstate thinks are
1072 directories and that were not found."""
1079 directories and that were not found."""
1073
1080
1074 def badtype(mode):
1081 def badtype(mode):
1075 kind = _(b'unknown')
1082 kind = _(b'unknown')
1076 if stat.S_ISCHR(mode):
1083 if stat.S_ISCHR(mode):
1077 kind = _(b'character device')
1084 kind = _(b'character device')
1078 elif stat.S_ISBLK(mode):
1085 elif stat.S_ISBLK(mode):
1079 kind = _(b'block device')
1086 kind = _(b'block device')
1080 elif stat.S_ISFIFO(mode):
1087 elif stat.S_ISFIFO(mode):
1081 kind = _(b'fifo')
1088 kind = _(b'fifo')
1082 elif stat.S_ISSOCK(mode):
1089 elif stat.S_ISSOCK(mode):
1083 kind = _(b'socket')
1090 kind = _(b'socket')
1084 elif stat.S_ISDIR(mode):
1091 elif stat.S_ISDIR(mode):
1085 kind = _(b'directory')
1092 kind = _(b'directory')
1086 return _(b'unsupported file type (type is %s)') % kind
1093 return _(b'unsupported file type (type is %s)') % kind
1087
1094
1088 badfn = match.bad
1095 badfn = match.bad
1089 dmap = self._map
1096 dmap = self._map
1090 lstat = os.lstat
1097 lstat = os.lstat
1091 getkind = stat.S_IFMT
1098 getkind = stat.S_IFMT
1092 dirkind = stat.S_IFDIR
1099 dirkind = stat.S_IFDIR
1093 regkind = stat.S_IFREG
1100 regkind = stat.S_IFREG
1094 lnkkind = stat.S_IFLNK
1101 lnkkind = stat.S_IFLNK
1095 join = self._join
1102 join = self._join
1096 dirsfound = []
1103 dirsfound = []
1097 foundadd = dirsfound.append
1104 foundadd = dirsfound.append
1098 dirsnotfound = []
1105 dirsnotfound = []
1099 notfoundadd = dirsnotfound.append
1106 notfoundadd = dirsnotfound.append
1100
1107
1101 if not match.isexact() and self._checkcase:
1108 if not match.isexact() and self._checkcase:
1102 normalize = self._normalize
1109 normalize = self._normalize
1103 else:
1110 else:
1104 normalize = None
1111 normalize = None
1105
1112
1106 files = sorted(match.files())
1113 files = sorted(match.files())
1107 subrepos.sort()
1114 subrepos.sort()
1108 i, j = 0, 0
1115 i, j = 0, 0
1109 while i < len(files) and j < len(subrepos):
1116 while i < len(files) and j < len(subrepos):
1110 subpath = subrepos[j] + b"/"
1117 subpath = subrepos[j] + b"/"
1111 if files[i] < subpath:
1118 if files[i] < subpath:
1112 i += 1
1119 i += 1
1113 continue
1120 continue
1114 while i < len(files) and files[i].startswith(subpath):
1121 while i < len(files) and files[i].startswith(subpath):
1115 del files[i]
1122 del files[i]
1116 j += 1
1123 j += 1
1117
1124
1118 if not files or b'' in files:
1125 if not files or b'' in files:
1119 files = [b'']
1126 files = [b'']
1120 # constructing the foldmap is expensive, so don't do it for the
1127 # constructing the foldmap is expensive, so don't do it for the
1121 # common case where files is ['']
1128 # common case where files is ['']
1122 normalize = None
1129 normalize = None
1123 results = dict.fromkeys(subrepos)
1130 results = dict.fromkeys(subrepos)
1124 results[b'.hg'] = None
1131 results[b'.hg'] = None
1125
1132
1126 for ff in files:
1133 for ff in files:
1127 if normalize:
1134 if normalize:
1128 nf = normalize(ff, False, True)
1135 nf = normalize(ff, False, True)
1129 else:
1136 else:
1130 nf = ff
1137 nf = ff
1131 if nf in results:
1138 if nf in results:
1132 continue
1139 continue
1133
1140
1134 try:
1141 try:
1135 st = lstat(join(nf))
1142 st = lstat(join(nf))
1136 kind = getkind(st.st_mode)
1143 kind = getkind(st.st_mode)
1137 if kind == dirkind:
1144 if kind == dirkind:
1138 if nf in dmap:
1145 if nf in dmap:
1139 # file replaced by dir on disk but still in dirstate
1146 # file replaced by dir on disk but still in dirstate
1140 results[nf] = None
1147 results[nf] = None
1141 foundadd((nf, ff))
1148 foundadd((nf, ff))
1142 elif kind == regkind or kind == lnkkind:
1149 elif kind == regkind or kind == lnkkind:
1143 results[nf] = st
1150 results[nf] = st
1144 else:
1151 else:
1145 badfn(ff, badtype(kind))
1152 badfn(ff, badtype(kind))
1146 if nf in dmap:
1153 if nf in dmap:
1147 results[nf] = None
1154 results[nf] = None
1148 except (OSError) as inst:
1155 except (OSError) as inst:
1149 # nf not found on disk - it is dirstate only
1156 # nf not found on disk - it is dirstate only
1150 if nf in dmap: # does it exactly match a missing file?
1157 if nf in dmap: # does it exactly match a missing file?
1151 results[nf] = None
1158 results[nf] = None
1152 else: # does it match a missing directory?
1159 else: # does it match a missing directory?
1153 if self._map.hasdir(nf):
1160 if self._map.hasdir(nf):
1154 notfoundadd(nf)
1161 notfoundadd(nf)
1155 else:
1162 else:
1156 badfn(ff, encoding.strtolocal(inst.strerror))
1163 badfn(ff, encoding.strtolocal(inst.strerror))
1157
1164
1158 # match.files() may contain explicitly-specified paths that shouldn't
1165 # match.files() may contain explicitly-specified paths that shouldn't
1159 # be taken; drop them from the list of files found. dirsfound/notfound
1166 # be taken; drop them from the list of files found. dirsfound/notfound
1160 # aren't filtered here because they will be tested later.
1167 # aren't filtered here because they will be tested later.
1161 if match.anypats():
1168 if match.anypats():
1162 for f in list(results):
1169 for f in list(results):
1163 if f == b'.hg' or f in subrepos:
1170 if f == b'.hg' or f in subrepos:
1164 # keep sentinel to disable further out-of-repo walks
1171 # keep sentinel to disable further out-of-repo walks
1165 continue
1172 continue
1166 if not match(f):
1173 if not match(f):
1167 del results[f]
1174 del results[f]
1168
1175
1169 # Case insensitive filesystems cannot rely on lstat() failing to detect
1176 # Case insensitive filesystems cannot rely on lstat() failing to detect
1170 # a case-only rename. Prune the stat object for any file that does not
1177 # a case-only rename. Prune the stat object for any file that does not
1171 # match the case in the filesystem, if there are multiple files that
1178 # match the case in the filesystem, if there are multiple files that
1172 # normalize to the same path.
1179 # normalize to the same path.
1173 if match.isexact() and self._checkcase:
1180 if match.isexact() and self._checkcase:
1174 normed = {}
1181 normed = {}
1175
1182
1176 for f, st in results.items():
1183 for f, st in results.items():
1177 if st is None:
1184 if st is None:
1178 continue
1185 continue
1179
1186
1180 nc = util.normcase(f)
1187 nc = util.normcase(f)
1181 paths = normed.get(nc)
1188 paths = normed.get(nc)
1182
1189
1183 if paths is None:
1190 if paths is None:
1184 paths = set()
1191 paths = set()
1185 normed[nc] = paths
1192 normed[nc] = paths
1186
1193
1187 paths.add(f)
1194 paths.add(f)
1188
1195
1189 for norm, paths in normed.items():
1196 for norm, paths in normed.items():
1190 if len(paths) > 1:
1197 if len(paths) > 1:
1191 for path in paths:
1198 for path in paths:
1192 folded = self._discoverpath(
1199 folded = self._discoverpath(
1193 path, norm, True, None, self._map.dirfoldmap
1200 path, norm, True, None, self._map.dirfoldmap
1194 )
1201 )
1195 if path != folded:
1202 if path != folded:
1196 results[path] = None
1203 results[path] = None
1197
1204
1198 return results, dirsfound, dirsnotfound
1205 return results, dirsfound, dirsnotfound
1199
1206
1200 def walk(self, match, subrepos, unknown, ignored, full=True):
1207 def walk(self, match, subrepos, unknown, ignored, full=True):
1201 """
1208 """
1202 Walk recursively through the directory tree, finding all files
1209 Walk recursively through the directory tree, finding all files
1203 matched by match.
1210 matched by match.
1204
1211
1205 If full is False, maybe skip some known-clean files.
1212 If full is False, maybe skip some known-clean files.
1206
1213
1207 Return a dict mapping filename to stat-like object (either
1214 Return a dict mapping filename to stat-like object (either
1208 mercurial.osutil.stat instance or return value of os.stat()).
1215 mercurial.osutil.stat instance or return value of os.stat()).
1209
1216
1210 """
1217 """
1211 # full is a flag that extensions that hook into walk can use -- this
1218 # full is a flag that extensions that hook into walk can use -- this
1212 # implementation doesn't use it at all. This satisfies the contract
1219 # implementation doesn't use it at all. This satisfies the contract
1213 # because we only guarantee a "maybe".
1220 # because we only guarantee a "maybe".
1214
1221
1215 if ignored:
1222 if ignored:
1216 ignore = util.never
1223 ignore = util.never
1217 dirignore = util.never
1224 dirignore = util.never
1218 elif unknown:
1225 elif unknown:
1219 ignore = self._ignore
1226 ignore = self._ignore
1220 dirignore = self._dirignore
1227 dirignore = self._dirignore
1221 else:
1228 else:
1222 # if not unknown and not ignored, drop dir recursion and step 2
1229 # if not unknown and not ignored, drop dir recursion and step 2
1223 ignore = util.always
1230 ignore = util.always
1224 dirignore = util.always
1231 dirignore = util.always
1225
1232
1226 if self._sparsematchfn is not None:
1233 if self._sparsematchfn is not None:
1227 em = matchmod.exact(match.files())
1234 em = matchmod.exact(match.files())
1228 sm = matchmod.unionmatcher([self._sparsematcher, em])
1235 sm = matchmod.unionmatcher([self._sparsematcher, em])
1229 match = matchmod.intersectmatchers(match, sm)
1236 match = matchmod.intersectmatchers(match, sm)
1230
1237
1231 matchfn = match.matchfn
1238 matchfn = match.matchfn
1232 matchalways = match.always()
1239 matchalways = match.always()
1233 matchtdir = match.traversedir
1240 matchtdir = match.traversedir
1234 dmap = self._map
1241 dmap = self._map
1235 listdir = util.listdir
1242 listdir = util.listdir
1236 lstat = os.lstat
1243 lstat = os.lstat
1237 dirkind = stat.S_IFDIR
1244 dirkind = stat.S_IFDIR
1238 regkind = stat.S_IFREG
1245 regkind = stat.S_IFREG
1239 lnkkind = stat.S_IFLNK
1246 lnkkind = stat.S_IFLNK
1240 join = self._join
1247 join = self._join
1241
1248
1242 exact = skipstep3 = False
1249 exact = skipstep3 = False
1243 if match.isexact(): # match.exact
1250 if match.isexact(): # match.exact
1244 exact = True
1251 exact = True
1245 dirignore = util.always # skip step 2
1252 dirignore = util.always # skip step 2
1246 elif match.prefix(): # match.match, no patterns
1253 elif match.prefix(): # match.match, no patterns
1247 skipstep3 = True
1254 skipstep3 = True
1248
1255
1249 if not exact and self._checkcase:
1256 if not exact and self._checkcase:
1250 normalize = self._normalize
1257 normalize = self._normalize
1251 normalizefile = self._normalizefile
1258 normalizefile = self._normalizefile
1252 skipstep3 = False
1259 skipstep3 = False
1253 else:
1260 else:
1254 normalize = self._normalize
1261 normalize = self._normalize
1255 normalizefile = None
1262 normalizefile = None
1256
1263
1257 # step 1: find all explicit files
1264 # step 1: find all explicit files
1258 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1265 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1259 if matchtdir:
1266 if matchtdir:
1260 for d in work:
1267 for d in work:
1261 matchtdir(d[0])
1268 matchtdir(d[0])
1262 for d in dirsnotfound:
1269 for d in dirsnotfound:
1263 matchtdir(d)
1270 matchtdir(d)
1264
1271
1265 skipstep3 = skipstep3 and not (work or dirsnotfound)
1272 skipstep3 = skipstep3 and not (work or dirsnotfound)
1266 work = [d for d in work if not dirignore(d[0])]
1273 work = [d for d in work if not dirignore(d[0])]
1267
1274
1268 # step 2: visit subdirectories
1275 # step 2: visit subdirectories
1269 def traverse(work, alreadynormed):
1276 def traverse(work, alreadynormed):
1270 wadd = work.append
1277 wadd = work.append
1271 while work:
1278 while work:
1272 tracing.counter('dirstate.walk work', len(work))
1279 tracing.counter('dirstate.walk work', len(work))
1273 nd = work.pop()
1280 nd = work.pop()
1274 visitentries = match.visitchildrenset(nd)
1281 visitentries = match.visitchildrenset(nd)
1275 if not visitentries:
1282 if not visitentries:
1276 continue
1283 continue
1277 if visitentries == b'this' or visitentries == b'all':
1284 if visitentries == b'this' or visitentries == b'all':
1278 visitentries = None
1285 visitentries = None
1279 skip = None
1286 skip = None
1280 if nd != b'':
1287 if nd != b'':
1281 skip = b'.hg'
1288 skip = b'.hg'
1282 try:
1289 try:
1283 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1290 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1284 entries = listdir(join(nd), stat=True, skip=skip)
1291 entries = listdir(join(nd), stat=True, skip=skip)
1285 except (PermissionError, FileNotFoundError) as inst:
1292 except (PermissionError, FileNotFoundError) as inst:
1286 match.bad(
1293 match.bad(
1287 self.pathto(nd), encoding.strtolocal(inst.strerror)
1294 self.pathto(nd), encoding.strtolocal(inst.strerror)
1288 )
1295 )
1289 continue
1296 continue
1290 for f, kind, st in entries:
1297 for f, kind, st in entries:
1291 # Some matchers may return files in the visitentries set,
1298 # Some matchers may return files in the visitentries set,
1292 # instead of 'this', if the matcher explicitly mentions them
1299 # instead of 'this', if the matcher explicitly mentions them
1293 # and is not an exactmatcher. This is acceptable; we do not
1300 # and is not an exactmatcher. This is acceptable; we do not
1294 # make any hard assumptions about file-or-directory below
1301 # make any hard assumptions about file-or-directory below
1295 # based on the presence of `f` in visitentries. If
1302 # based on the presence of `f` in visitentries. If
1296 # visitchildrenset returned a set, we can always skip the
1303 # visitchildrenset returned a set, we can always skip the
1297 # entries *not* in the set it provided regardless of whether
1304 # entries *not* in the set it provided regardless of whether
1298 # they're actually a file or a directory.
1305 # they're actually a file or a directory.
1299 if visitentries and f not in visitentries:
1306 if visitentries and f not in visitentries:
1300 continue
1307 continue
1301 if normalizefile:
1308 if normalizefile:
1302 # even though f might be a directory, we're only
1309 # even though f might be a directory, we're only
1303 # interested in comparing it to files currently in the
1310 # interested in comparing it to files currently in the
1304 # dmap -- therefore normalizefile is enough
1311 # dmap -- therefore normalizefile is enough
1305 nf = normalizefile(
1312 nf = normalizefile(
1306 nd and (nd + b"/" + f) or f, True, True
1313 nd and (nd + b"/" + f) or f, True, True
1307 )
1314 )
1308 else:
1315 else:
1309 nf = nd and (nd + b"/" + f) or f
1316 nf = nd and (nd + b"/" + f) or f
1310 if nf not in results:
1317 if nf not in results:
1311 if kind == dirkind:
1318 if kind == dirkind:
1312 if not ignore(nf):
1319 if not ignore(nf):
1313 if matchtdir:
1320 if matchtdir:
1314 matchtdir(nf)
1321 matchtdir(nf)
1315 wadd(nf)
1322 wadd(nf)
1316 if nf in dmap and (matchalways or matchfn(nf)):
1323 if nf in dmap and (matchalways or matchfn(nf)):
1317 results[nf] = None
1324 results[nf] = None
1318 elif kind == regkind or kind == lnkkind:
1325 elif kind == regkind or kind == lnkkind:
1319 if nf in dmap:
1326 if nf in dmap:
1320 if matchalways or matchfn(nf):
1327 if matchalways or matchfn(nf):
1321 results[nf] = st
1328 results[nf] = st
1322 elif (matchalways or matchfn(nf)) and not ignore(
1329 elif (matchalways or matchfn(nf)) and not ignore(
1323 nf
1330 nf
1324 ):
1331 ):
1325 # unknown file -- normalize if necessary
1332 # unknown file -- normalize if necessary
1326 if not alreadynormed:
1333 if not alreadynormed:
1327 nf = normalize(nf, False, True)
1334 nf = normalize(nf, False, True)
1328 results[nf] = st
1335 results[nf] = st
1329 elif nf in dmap and (matchalways or matchfn(nf)):
1336 elif nf in dmap and (matchalways or matchfn(nf)):
1330 results[nf] = None
1337 results[nf] = None
1331
1338
1332 for nd, d in work:
1339 for nd, d in work:
1333 # alreadynormed means that processwork doesn't have to do any
1340 # alreadynormed means that processwork doesn't have to do any
1334 # expensive directory normalization
1341 # expensive directory normalization
1335 alreadynormed = not normalize or nd == d
1342 alreadynormed = not normalize or nd == d
1336 traverse([d], alreadynormed)
1343 traverse([d], alreadynormed)
1337
1344
1338 for s in subrepos:
1345 for s in subrepos:
1339 del results[s]
1346 del results[s]
1340 del results[b'.hg']
1347 del results[b'.hg']
1341
1348
1342 # step 3: visit remaining files from dmap
1349 # step 3: visit remaining files from dmap
1343 if not skipstep3 and not exact:
1350 if not skipstep3 and not exact:
1344 # If a dmap file is not in results yet, it was either
1351 # If a dmap file is not in results yet, it was either
1345 # a) not matching matchfn b) ignored, c) missing, or d) under a
1352 # a) not matching matchfn b) ignored, c) missing, or d) under a
1346 # symlink directory.
1353 # symlink directory.
1347 if not results and matchalways:
1354 if not results and matchalways:
1348 visit = [f for f in dmap]
1355 visit = [f for f in dmap]
1349 else:
1356 else:
1350 visit = [f for f in dmap if f not in results and matchfn(f)]
1357 visit = [f for f in dmap if f not in results and matchfn(f)]
1351 visit.sort()
1358 visit.sort()
1352
1359
1353 if unknown:
1360 if unknown:
1354 # unknown == True means we walked all dirs under the roots
1361 # unknown == True means we walked all dirs under the roots
1355 # that wasn't ignored, and everything that matched was stat'ed
1362 # that wasn't ignored, and everything that matched was stat'ed
1356 # and is already in results.
1363 # and is already in results.
1357 # The rest must thus be ignored or under a symlink.
1364 # The rest must thus be ignored or under a symlink.
1358 audit_path = pathutil.pathauditor(self._root, cached=True)
1365 audit_path = pathutil.pathauditor(self._root, cached=True)
1359
1366
1360 for nf in iter(visit):
1367 for nf in iter(visit):
1361 # If a stat for the same file was already added with a
1368 # If a stat for the same file was already added with a
1362 # different case, don't add one for this, since that would
1369 # different case, don't add one for this, since that would
1363 # make it appear as if the file exists under both names
1370 # make it appear as if the file exists under both names
1364 # on disk.
1371 # on disk.
1365 if (
1372 if (
1366 normalizefile
1373 normalizefile
1367 and normalizefile(nf, True, True) in results
1374 and normalizefile(nf, True, True) in results
1368 ):
1375 ):
1369 results[nf] = None
1376 results[nf] = None
1370 # Report ignored items in the dmap as long as they are not
1377 # Report ignored items in the dmap as long as they are not
1371 # under a symlink directory.
1378 # under a symlink directory.
1372 elif audit_path.check(nf):
1379 elif audit_path.check(nf):
1373 try:
1380 try:
1374 results[nf] = lstat(join(nf))
1381 results[nf] = lstat(join(nf))
1375 # file was just ignored, no links, and exists
1382 # file was just ignored, no links, and exists
1376 except OSError:
1383 except OSError:
1377 # file doesn't exist
1384 # file doesn't exist
1378 results[nf] = None
1385 results[nf] = None
1379 else:
1386 else:
1380 # It's either missing or under a symlink directory
1387 # It's either missing or under a symlink directory
1381 # which we in this case report as missing
1388 # which we in this case report as missing
1382 results[nf] = None
1389 results[nf] = None
1383 else:
1390 else:
1384 # We may not have walked the full directory tree above,
1391 # We may not have walked the full directory tree above,
1385 # so stat and check everything we missed.
1392 # so stat and check everything we missed.
1386 iv = iter(visit)
1393 iv = iter(visit)
1387 for st in util.statfiles([join(i) for i in visit]):
1394 for st in util.statfiles([join(i) for i in visit]):
1388 results[next(iv)] = st
1395 results[next(iv)] = st
1389 return results
1396 return results
1390
1397
1391 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1398 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1392 if self._sparsematchfn is not None:
1399 if self._sparsematchfn is not None:
1393 em = matchmod.exact(matcher.files())
1400 em = matchmod.exact(matcher.files())
1394 sm = matchmod.unionmatcher([self._sparsematcher, em])
1401 sm = matchmod.unionmatcher([self._sparsematcher, em])
1395 matcher = matchmod.intersectmatchers(matcher, sm)
1402 matcher = matchmod.intersectmatchers(matcher, sm)
1396 # Force Rayon (Rust parallelism library) to respect the number of
1403 # Force Rayon (Rust parallelism library) to respect the number of
1397 # workers. This is a temporary workaround until Rust code knows
1404 # workers. This is a temporary workaround until Rust code knows
1398 # how to read the config file.
1405 # how to read the config file.
1399 numcpus = self._ui.configint(b"worker", b"numcpus")
1406 numcpus = self._ui.configint(b"worker", b"numcpus")
1400 if numcpus is not None:
1407 if numcpus is not None:
1401 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1408 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1402
1409
1403 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1410 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1404 if not workers_enabled:
1411 if not workers_enabled:
1405 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1412 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1406
1413
1407 (
1414 (
1408 lookup,
1415 lookup,
1409 modified,
1416 modified,
1410 added,
1417 added,
1411 removed,
1418 removed,
1412 deleted,
1419 deleted,
1413 clean,
1420 clean,
1414 ignored,
1421 ignored,
1415 unknown,
1422 unknown,
1416 warnings,
1423 warnings,
1417 bad,
1424 bad,
1418 traversed,
1425 traversed,
1419 dirty,
1426 dirty,
1420 ) = rustmod.status(
1427 ) = rustmod.status(
1421 self._map._map,
1428 self._map._map,
1422 matcher,
1429 matcher,
1423 self._rootdir,
1430 self._rootdir,
1424 self._ignorefiles(),
1431 self._ignorefiles(),
1425 self._checkexec,
1432 self._checkexec,
1426 bool(list_clean),
1433 bool(list_clean),
1427 bool(list_ignored),
1434 bool(list_ignored),
1428 bool(list_unknown),
1435 bool(list_unknown),
1429 bool(matcher.traversedir),
1436 bool(matcher.traversedir),
1430 )
1437 )
1431
1438
1432 self._dirty |= dirty
1439 self._dirty |= dirty
1433
1440
1434 if matcher.traversedir:
1441 if matcher.traversedir:
1435 for dir in traversed:
1442 for dir in traversed:
1436 matcher.traversedir(dir)
1443 matcher.traversedir(dir)
1437
1444
1438 if self._ui.warn:
1445 if self._ui.warn:
1439 for item in warnings:
1446 for item in warnings:
1440 if isinstance(item, tuple):
1447 if isinstance(item, tuple):
1441 file_path, syntax = item
1448 file_path, syntax = item
1442 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1449 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1443 file_path,
1450 file_path,
1444 syntax,
1451 syntax,
1445 )
1452 )
1446 self._ui.warn(msg)
1453 self._ui.warn(msg)
1447 else:
1454 else:
1448 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1455 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1449 self._ui.warn(
1456 self._ui.warn(
1450 msg
1457 msg
1451 % (
1458 % (
1452 pathutil.canonpath(
1459 pathutil.canonpath(
1453 self._rootdir, self._rootdir, item
1460 self._rootdir, self._rootdir, item
1454 ),
1461 ),
1455 b"No such file or directory",
1462 b"No such file or directory",
1456 )
1463 )
1457 )
1464 )
1458
1465
1459 for fn, message in bad:
1466 for fn, message in bad:
1460 matcher.bad(fn, encoding.strtolocal(message))
1467 matcher.bad(fn, encoding.strtolocal(message))
1461
1468
1462 status = scmutil.status(
1469 status = scmutil.status(
1463 modified=modified,
1470 modified=modified,
1464 added=added,
1471 added=added,
1465 removed=removed,
1472 removed=removed,
1466 deleted=deleted,
1473 deleted=deleted,
1467 unknown=unknown,
1474 unknown=unknown,
1468 ignored=ignored,
1475 ignored=ignored,
1469 clean=clean,
1476 clean=clean,
1470 )
1477 )
1471 return (lookup, status)
1478 return (lookup, status)
1472
1479
1473 # XXX since this can make the dirstate dirty (through rust), we should
1480 # XXX since this can make the dirstate dirty (through rust), we should
1474 # enforce that it is done withing an appropriate change-context that scope
1481 # enforce that it is done withing an appropriate change-context that scope
1475 # the change and ensure it eventually get written on disk (or rolled back)
1482 # the change and ensure it eventually get written on disk (or rolled back)
1476 def status(self, match, subrepos, ignored, clean, unknown):
1483 def status(self, match, subrepos, ignored, clean, unknown):
1477 """Determine the status of the working copy relative to the
1484 """Determine the status of the working copy relative to the
1478 dirstate and return a pair of (unsure, status), where status is of type
1485 dirstate and return a pair of (unsure, status), where status is of type
1479 scmutil.status and:
1486 scmutil.status and:
1480
1487
1481 unsure:
1488 unsure:
1482 files that might have been modified since the dirstate was
1489 files that might have been modified since the dirstate was
1483 written, but need to be read to be sure (size is the same
1490 written, but need to be read to be sure (size is the same
1484 but mtime differs)
1491 but mtime differs)
1485 status.modified:
1492 status.modified:
1486 files that have definitely been modified since the dirstate
1493 files that have definitely been modified since the dirstate
1487 was written (different size or mode)
1494 was written (different size or mode)
1488 status.clean:
1495 status.clean:
1489 files that have definitely not been modified since the
1496 files that have definitely not been modified since the
1490 dirstate was written
1497 dirstate was written
1491 """
1498 """
1492 listignored, listclean, listunknown = ignored, clean, unknown
1499 listignored, listclean, listunknown = ignored, clean, unknown
1493 lookup, modified, added, unknown, ignored = [], [], [], [], []
1500 lookup, modified, added, unknown, ignored = [], [], [], [], []
1494 removed, deleted, clean = [], [], []
1501 removed, deleted, clean = [], [], []
1495
1502
1496 dmap = self._map
1503 dmap = self._map
1497 dmap.preload()
1504 dmap.preload()
1498
1505
1499 use_rust = True
1506 use_rust = True
1500
1507
1501 allowed_matchers = (
1508 allowed_matchers = (
1502 matchmod.alwaysmatcher,
1509 matchmod.alwaysmatcher,
1503 matchmod.differencematcher,
1510 matchmod.differencematcher,
1504 matchmod.exactmatcher,
1511 matchmod.exactmatcher,
1505 matchmod.includematcher,
1512 matchmod.includematcher,
1506 matchmod.intersectionmatcher,
1513 matchmod.intersectionmatcher,
1507 matchmod.nevermatcher,
1514 matchmod.nevermatcher,
1508 matchmod.unionmatcher,
1515 matchmod.unionmatcher,
1509 )
1516 )
1510
1517
1511 if rustmod is None:
1518 if rustmod is None:
1512 use_rust = False
1519 use_rust = False
1513 elif self._checkcase:
1520 elif self._checkcase:
1514 # Case-insensitive filesystems are not handled yet
1521 # Case-insensitive filesystems are not handled yet
1515 use_rust = False
1522 use_rust = False
1516 elif subrepos:
1523 elif subrepos:
1517 use_rust = False
1524 use_rust = False
1518 elif not isinstance(match, allowed_matchers):
1525 elif not isinstance(match, allowed_matchers):
1519 # Some matchers have yet to be implemented
1526 # Some matchers have yet to be implemented
1520 use_rust = False
1527 use_rust = False
1521
1528
1522 # Get the time from the filesystem so we can disambiguate files that
1529 # Get the time from the filesystem so we can disambiguate files that
1523 # appear modified in the present or future.
1530 # appear modified in the present or future.
1524 try:
1531 try:
1525 mtime_boundary = timestamp.get_fs_now(self._opener)
1532 mtime_boundary = timestamp.get_fs_now(self._opener)
1526 except OSError:
1533 except OSError:
1527 # In largefiles or readonly context
1534 # In largefiles or readonly context
1528 mtime_boundary = None
1535 mtime_boundary = None
1529
1536
1530 if use_rust:
1537 if use_rust:
1531 try:
1538 try:
1532 res = self._rust_status(
1539 res = self._rust_status(
1533 match, listclean, listignored, listunknown
1540 match, listclean, listignored, listunknown
1534 )
1541 )
1535 return res + (mtime_boundary,)
1542 return res + (mtime_boundary,)
1536 except rustmod.FallbackError:
1543 except rustmod.FallbackError:
1537 pass
1544 pass
1538
1545
1539 def noop(f):
1546 def noop(f):
1540 pass
1547 pass
1541
1548
1542 dcontains = dmap.__contains__
1549 dcontains = dmap.__contains__
1543 dget = dmap.__getitem__
1550 dget = dmap.__getitem__
1544 ladd = lookup.append # aka "unsure"
1551 ladd = lookup.append # aka "unsure"
1545 madd = modified.append
1552 madd = modified.append
1546 aadd = added.append
1553 aadd = added.append
1547 uadd = unknown.append if listunknown else noop
1554 uadd = unknown.append if listunknown else noop
1548 iadd = ignored.append if listignored else noop
1555 iadd = ignored.append if listignored else noop
1549 radd = removed.append
1556 radd = removed.append
1550 dadd = deleted.append
1557 dadd = deleted.append
1551 cadd = clean.append if listclean else noop
1558 cadd = clean.append if listclean else noop
1552 mexact = match.exact
1559 mexact = match.exact
1553 dirignore = self._dirignore
1560 dirignore = self._dirignore
1554 checkexec = self._checkexec
1561 checkexec = self._checkexec
1555 checklink = self._checklink
1562 checklink = self._checklink
1556 copymap = self._map.copymap
1563 copymap = self._map.copymap
1557
1564
1558 # We need to do full walks when either
1565 # We need to do full walks when either
1559 # - we're listing all clean files, or
1566 # - we're listing all clean files, or
1560 # - match.traversedir does something, because match.traversedir should
1567 # - match.traversedir does something, because match.traversedir should
1561 # be called for every dir in the working dir
1568 # be called for every dir in the working dir
1562 full = listclean or match.traversedir is not None
1569 full = listclean or match.traversedir is not None
1563 for fn, st in self.walk(
1570 for fn, st in self.walk(
1564 match, subrepos, listunknown, listignored, full=full
1571 match, subrepos, listunknown, listignored, full=full
1565 ).items():
1572 ).items():
1566 if not dcontains(fn):
1573 if not dcontains(fn):
1567 if (listignored or mexact(fn)) and dirignore(fn):
1574 if (listignored or mexact(fn)) and dirignore(fn):
1568 if listignored:
1575 if listignored:
1569 iadd(fn)
1576 iadd(fn)
1570 else:
1577 else:
1571 uadd(fn)
1578 uadd(fn)
1572 continue
1579 continue
1573
1580
1574 t = dget(fn)
1581 t = dget(fn)
1575 mode = t.mode
1582 mode = t.mode
1576 size = t.size
1583 size = t.size
1577
1584
1578 if not st and t.tracked:
1585 if not st and t.tracked:
1579 dadd(fn)
1586 dadd(fn)
1580 elif t.p2_info:
1587 elif t.p2_info:
1581 madd(fn)
1588 madd(fn)
1582 elif t.added:
1589 elif t.added:
1583 aadd(fn)
1590 aadd(fn)
1584 elif t.removed:
1591 elif t.removed:
1585 radd(fn)
1592 radd(fn)
1586 elif t.tracked:
1593 elif t.tracked:
1587 if not checklink and t.has_fallback_symlink:
1594 if not checklink and t.has_fallback_symlink:
1588 # If the file system does not support symlink, the mode
1595 # If the file system does not support symlink, the mode
1589 # might not be correctly stored in the dirstate, so do not
1596 # might not be correctly stored in the dirstate, so do not
1590 # trust it.
1597 # trust it.
1591 ladd(fn)
1598 ladd(fn)
1592 elif not checkexec and t.has_fallback_exec:
1599 elif not checkexec and t.has_fallback_exec:
1593 # If the file system does not support exec bits, the mode
1600 # If the file system does not support exec bits, the mode
1594 # might not be correctly stored in the dirstate, so do not
1601 # might not be correctly stored in the dirstate, so do not
1595 # trust it.
1602 # trust it.
1596 ladd(fn)
1603 ladd(fn)
1597 elif (
1604 elif (
1598 size >= 0
1605 size >= 0
1599 and (
1606 and (
1600 (size != st.st_size and size != st.st_size & _rangemask)
1607 (size != st.st_size and size != st.st_size & _rangemask)
1601 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1608 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1602 )
1609 )
1603 or fn in copymap
1610 or fn in copymap
1604 ):
1611 ):
1605 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1612 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1606 # issue6456: Size returned may be longer due to
1613 # issue6456: Size returned may be longer due to
1607 # encryption on EXT-4 fscrypt, undecided.
1614 # encryption on EXT-4 fscrypt, undecided.
1608 ladd(fn)
1615 ladd(fn)
1609 else:
1616 else:
1610 madd(fn)
1617 madd(fn)
1611 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1618 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1612 # There might be a change in the future if for example the
1619 # There might be a change in the future if for example the
1613 # internal clock is off, but this is a case where the issues
1620 # internal clock is off, but this is a case where the issues
1614 # the user would face would be a lot worse and there is
1621 # the user would face would be a lot worse and there is
1615 # nothing we can really do.
1622 # nothing we can really do.
1616 ladd(fn)
1623 ladd(fn)
1617 elif listclean:
1624 elif listclean:
1618 cadd(fn)
1625 cadd(fn)
1619 status = scmutil.status(
1626 status = scmutil.status(
1620 modified, added, removed, deleted, unknown, ignored, clean
1627 modified, added, removed, deleted, unknown, ignored, clean
1621 )
1628 )
1622 return (lookup, status, mtime_boundary)
1629 return (lookup, status, mtime_boundary)
1623
1630
1624 def matches(self, match):
1631 def matches(self, match):
1625 """
1632 """
1626 return files in the dirstate (in whatever state) filtered by match
1633 return files in the dirstate (in whatever state) filtered by match
1627 """
1634 """
1628 dmap = self._map
1635 dmap = self._map
1629 if rustmod is not None:
1636 if rustmod is not None:
1630 dmap = self._map._map
1637 dmap = self._map._map
1631
1638
1632 if match.always():
1639 if match.always():
1633 return dmap.keys()
1640 return dmap.keys()
1634 files = match.files()
1641 files = match.files()
1635 if match.isexact():
1642 if match.isexact():
1636 # fast path -- filter the other way around, since typically files is
1643 # fast path -- filter the other way around, since typically files is
1637 # much smaller than dmap
1644 # much smaller than dmap
1638 return [f for f in files if f in dmap]
1645 return [f for f in files if f in dmap]
1639 if match.prefix() and all(fn in dmap for fn in files):
1646 if match.prefix() and all(fn in dmap for fn in files):
1640 # fast path -- all the values are known to be files, so just return
1647 # fast path -- all the values are known to be files, so just return
1641 # that
1648 # that
1642 return list(files)
1649 return list(files)
1643 return [f for f in dmap if match(f)]
1650 return [f for f in dmap if match(f)]
1644
1651
1645 def _actualfilename(self, tr):
1652 def _actualfilename(self, tr):
1646 if tr:
1653 if tr:
1647 return self._pendingfilename
1654 return self._pendingfilename
1648 else:
1655 else:
1649 return self._filename
1656 return self._filename
1650
1657
1651 def all_file_names(self):
1658 def all_file_names(self):
1652 """list all filename currently used by this dirstate
1659 """list all filename currently used by this dirstate
1653
1660
1654 This is only used to do `hg rollback` related backup in the transaction
1661 This is only used to do `hg rollback` related backup in the transaction
1655 """
1662 """
1656 if not self._opener.exists(self._filename):
1663 if not self._opener.exists(self._filename):
1657 # no data every written to disk yet
1664 # no data every written to disk yet
1658 return ()
1665 return ()
1659 elif self._use_dirstate_v2:
1666 elif self._use_dirstate_v2:
1660 return (
1667 return (
1661 self._filename,
1668 self._filename,
1662 self._map.docket.data_filename(),
1669 self._map.docket.data_filename(),
1663 )
1670 )
1664 else:
1671 else:
1665 return (self._filename,)
1672 return (self._filename,)
1666
1673
1667 def verify(self, m1, m2, p1, narrow_matcher=None):
1674 def verify(self, m1, m2, p1, narrow_matcher=None):
1668 """
1675 """
1669 check the dirstate contents against the parent manifest and yield errors
1676 check the dirstate contents against the parent manifest and yield errors
1670 """
1677 """
1671 missing_from_p1 = _(
1678 missing_from_p1 = _(
1672 b"%s marked as tracked in p1 (%s) but not in manifest1\n"
1679 b"%s marked as tracked in p1 (%s) but not in manifest1\n"
1673 )
1680 )
1674 unexpected_in_p1 = _(b"%s marked as added, but also in manifest1\n")
1681 unexpected_in_p1 = _(b"%s marked as added, but also in manifest1\n")
1675 missing_from_ps = _(
1682 missing_from_ps = _(
1676 b"%s marked as modified, but not in either manifest\n"
1683 b"%s marked as modified, but not in either manifest\n"
1677 )
1684 )
1678 missing_from_ds = _(
1685 missing_from_ds = _(
1679 b"%s in manifest1, but not marked as tracked in p1 (%s)\n"
1686 b"%s in manifest1, but not marked as tracked in p1 (%s)\n"
1680 )
1687 )
1681 for f, entry in self.items():
1688 for f, entry in self.items():
1682 if entry.p1_tracked:
1689 if entry.p1_tracked:
1683 if entry.modified and f not in m1 and f not in m2:
1690 if entry.modified and f not in m1 and f not in m2:
1684 yield missing_from_ps % f
1691 yield missing_from_ps % f
1685 elif f not in m1:
1692 elif f not in m1:
1686 yield missing_from_p1 % (f, node.short(p1))
1693 yield missing_from_p1 % (f, node.short(p1))
1687 if entry.added and f in m1:
1694 if entry.added and f in m1:
1688 yield unexpected_in_p1 % f
1695 yield unexpected_in_p1 % f
1689 for f in m1:
1696 for f in m1:
1690 if narrow_matcher is not None and not narrow_matcher(f):
1697 if narrow_matcher is not None and not narrow_matcher(f):
1691 continue
1698 continue
1692 entry = self.get_entry(f)
1699 entry = self.get_entry(f)
1693 if not entry.p1_tracked:
1700 if not entry.p1_tracked:
1694 yield missing_from_ds % (f, node.short(p1))
1701 yield missing_from_ds % (f, node.short(p1))
General Comments 0
You need to be logged in to leave comments. Login now