##// END OF EJS Templates
dirstate: make it mandatory to provide parentfiledata in `set_clean`...
marmoute -
r49208:080151f1 default
parent child Browse files
Show More
@@ -1,369 +1,369 b''
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2
2
3 import contextlib
3 import contextlib
4 import errno
4 import errno
5 import os
5 import os
6
6
7 from mercurial.node import sha1nodeconstants
7 from mercurial.node import sha1nodeconstants
8 from mercurial import (
8 from mercurial import (
9 error,
9 error,
10 extensions,
10 extensions,
11 match as matchmod,
11 match as matchmod,
12 pycompat,
12 pycompat,
13 scmutil,
13 scmutil,
14 util,
14 util,
15 )
15 )
16 from mercurial.interfaces import (
16 from mercurial.interfaces import (
17 dirstate as intdirstate,
17 dirstate as intdirstate,
18 util as interfaceutil,
18 util as interfaceutil,
19 )
19 )
20
20
21 from . import gitutil
21 from . import gitutil
22
22
23 pygit2 = gitutil.get_pygit2()
23 pygit2 = gitutil.get_pygit2()
24
24
25
25
26 def readpatternfile(orig, filepath, warn, sourceinfo=False):
26 def readpatternfile(orig, filepath, warn, sourceinfo=False):
27 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
27 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
28 return orig(filepath, warn, sourceinfo=False)
28 return orig(filepath, warn, sourceinfo=False)
29 result = []
29 result = []
30 warnings = []
30 warnings = []
31 with open(filepath, b'rb') as fp:
31 with open(filepath, b'rb') as fp:
32 for l in fp:
32 for l in fp:
33 l = l.strip()
33 l = l.strip()
34 if not l or l.startswith(b'#'):
34 if not l or l.startswith(b'#'):
35 continue
35 continue
36 if l.startswith(b'!'):
36 if l.startswith(b'!'):
37 warnings.append(b'unsupported ignore pattern %s' % l)
37 warnings.append(b'unsupported ignore pattern %s' % l)
38 continue
38 continue
39 if l.startswith(b'/'):
39 if l.startswith(b'/'):
40 result.append(b'rootglob:' + l[1:])
40 result.append(b'rootglob:' + l[1:])
41 else:
41 else:
42 result.append(b'relglob:' + l)
42 result.append(b'relglob:' + l)
43 return result, warnings
43 return result, warnings
44
44
45
45
46 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
46 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
47
47
48
48
49 _STATUS_MAP = {}
49 _STATUS_MAP = {}
50 if pygit2:
50 if pygit2:
51 _STATUS_MAP = {
51 _STATUS_MAP = {
52 pygit2.GIT_STATUS_CONFLICTED: b'm',
52 pygit2.GIT_STATUS_CONFLICTED: b'm',
53 pygit2.GIT_STATUS_CURRENT: b'n',
53 pygit2.GIT_STATUS_CURRENT: b'n',
54 pygit2.GIT_STATUS_IGNORED: b'?',
54 pygit2.GIT_STATUS_IGNORED: b'?',
55 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
55 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
56 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
56 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
57 pygit2.GIT_STATUS_INDEX_NEW: b'a',
57 pygit2.GIT_STATUS_INDEX_NEW: b'a',
58 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
58 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
59 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
59 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
60 pygit2.GIT_STATUS_WT_DELETED: b'r',
60 pygit2.GIT_STATUS_WT_DELETED: b'r',
61 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
61 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
62 pygit2.GIT_STATUS_WT_NEW: b'?',
62 pygit2.GIT_STATUS_WT_NEW: b'?',
63 pygit2.GIT_STATUS_WT_RENAMED: b'a',
63 pygit2.GIT_STATUS_WT_RENAMED: b'a',
64 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
64 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
65 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
65 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
66 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: b'm',
66 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: b'm',
67 }
67 }
68
68
69
69
70 @interfaceutil.implementer(intdirstate.idirstate)
70 @interfaceutil.implementer(intdirstate.idirstate)
71 class gitdirstate(object):
71 class gitdirstate(object):
72 def __init__(self, ui, root, gitrepo):
72 def __init__(self, ui, root, gitrepo):
73 self._ui = ui
73 self._ui = ui
74 self._root = os.path.dirname(root)
74 self._root = os.path.dirname(root)
75 self.git = gitrepo
75 self.git = gitrepo
76 self._plchangecallbacks = {}
76 self._plchangecallbacks = {}
77 # TODO: context.poststatusfixup is bad and uses this attribute
77 # TODO: context.poststatusfixup is bad and uses this attribute
78 self._dirty = False
78 self._dirty = False
79
79
80 def p1(self):
80 def p1(self):
81 try:
81 try:
82 return self.git.head.peel().id.raw
82 return self.git.head.peel().id.raw
83 except pygit2.GitError:
83 except pygit2.GitError:
84 # Typically happens when peeling HEAD fails, as in an
84 # Typically happens when peeling HEAD fails, as in an
85 # empty repository.
85 # empty repository.
86 return sha1nodeconstants.nullid
86 return sha1nodeconstants.nullid
87
87
88 def p2(self):
88 def p2(self):
89 # TODO: MERGE_HEAD? something like that, right?
89 # TODO: MERGE_HEAD? something like that, right?
90 return sha1nodeconstants.nullid
90 return sha1nodeconstants.nullid
91
91
92 def setparents(self, p1, p2=None):
92 def setparents(self, p1, p2=None):
93 if p2 is None:
93 if p2 is None:
94 p2 = sha1nodeconstants.nullid
94 p2 = sha1nodeconstants.nullid
95 assert p2 == sha1nodeconstants.nullid, b'TODO merging support'
95 assert p2 == sha1nodeconstants.nullid, b'TODO merging support'
96 self.git.head.set_target(gitutil.togitnode(p1))
96 self.git.head.set_target(gitutil.togitnode(p1))
97
97
98 @util.propertycache
98 @util.propertycache
99 def identity(self):
99 def identity(self):
100 return util.filestat.frompath(
100 return util.filestat.frompath(
101 os.path.join(self._root, b'.git', b'index')
101 os.path.join(self._root, b'.git', b'index')
102 )
102 )
103
103
104 def branch(self):
104 def branch(self):
105 return b'default'
105 return b'default'
106
106
107 def parents(self):
107 def parents(self):
108 # TODO how on earth do we find p2 if a merge is in flight?
108 # TODO how on earth do we find p2 if a merge is in flight?
109 return self.p1(), sha1nodeconstants.nullid
109 return self.p1(), sha1nodeconstants.nullid
110
110
111 def __iter__(self):
111 def __iter__(self):
112 return (pycompat.fsencode(f.path) for f in self.git.index)
112 return (pycompat.fsencode(f.path) for f in self.git.index)
113
113
114 def items(self):
114 def items(self):
115 for ie in self.git.index:
115 for ie in self.git.index:
116 yield ie.path, None # value should be a DirstateItem
116 yield ie.path, None # value should be a DirstateItem
117
117
118 # py2,3 compat forward
118 # py2,3 compat forward
119 iteritems = items
119 iteritems = items
120
120
121 def __getitem__(self, filename):
121 def __getitem__(self, filename):
122 try:
122 try:
123 gs = self.git.status_file(filename)
123 gs = self.git.status_file(filename)
124 except KeyError:
124 except KeyError:
125 return b'?'
125 return b'?'
126 return _STATUS_MAP[gs]
126 return _STATUS_MAP[gs]
127
127
128 def __contains__(self, filename):
128 def __contains__(self, filename):
129 try:
129 try:
130 gs = self.git.status_file(filename)
130 gs = self.git.status_file(filename)
131 return _STATUS_MAP[gs] != b'?'
131 return _STATUS_MAP[gs] != b'?'
132 except KeyError:
132 except KeyError:
133 return False
133 return False
134
134
135 def status(self, match, subrepos, ignored, clean, unknown):
135 def status(self, match, subrepos, ignored, clean, unknown):
136 listclean = clean
136 listclean = clean
137 # TODO handling of clean files - can we get that from git.status()?
137 # TODO handling of clean files - can we get that from git.status()?
138 modified, added, removed, deleted, unknown, ignored, clean = (
138 modified, added, removed, deleted, unknown, ignored, clean = (
139 [],
139 [],
140 [],
140 [],
141 [],
141 [],
142 [],
142 [],
143 [],
143 [],
144 [],
144 [],
145 [],
145 [],
146 )
146 )
147 gstatus = self.git.status()
147 gstatus = self.git.status()
148 for path, status in gstatus.items():
148 for path, status in gstatus.items():
149 path = pycompat.fsencode(path)
149 path = pycompat.fsencode(path)
150 if not match(path):
150 if not match(path):
151 continue
151 continue
152 if status == pygit2.GIT_STATUS_IGNORED:
152 if status == pygit2.GIT_STATUS_IGNORED:
153 if path.endswith(b'/'):
153 if path.endswith(b'/'):
154 continue
154 continue
155 ignored.append(path)
155 ignored.append(path)
156 elif status in (
156 elif status in (
157 pygit2.GIT_STATUS_WT_MODIFIED,
157 pygit2.GIT_STATUS_WT_MODIFIED,
158 pygit2.GIT_STATUS_INDEX_MODIFIED,
158 pygit2.GIT_STATUS_INDEX_MODIFIED,
159 pygit2.GIT_STATUS_WT_MODIFIED
159 pygit2.GIT_STATUS_WT_MODIFIED
160 | pygit2.GIT_STATUS_INDEX_MODIFIED,
160 | pygit2.GIT_STATUS_INDEX_MODIFIED,
161 ):
161 ):
162 modified.append(path)
162 modified.append(path)
163 elif status == pygit2.GIT_STATUS_INDEX_NEW:
163 elif status == pygit2.GIT_STATUS_INDEX_NEW:
164 added.append(path)
164 added.append(path)
165 elif status == pygit2.GIT_STATUS_WT_NEW:
165 elif status == pygit2.GIT_STATUS_WT_NEW:
166 unknown.append(path)
166 unknown.append(path)
167 elif status == pygit2.GIT_STATUS_WT_DELETED:
167 elif status == pygit2.GIT_STATUS_WT_DELETED:
168 deleted.append(path)
168 deleted.append(path)
169 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
169 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
170 removed.append(path)
170 removed.append(path)
171 else:
171 else:
172 raise error.Abort(
172 raise error.Abort(
173 b'unhandled case: status for %r is %r' % (path, status)
173 b'unhandled case: status for %r is %r' % (path, status)
174 )
174 )
175
175
176 if listclean:
176 if listclean:
177 observed = set(
177 observed = set(
178 modified + added + removed + deleted + unknown + ignored
178 modified + added + removed + deleted + unknown + ignored
179 )
179 )
180 index = self.git.index
180 index = self.git.index
181 index.read()
181 index.read()
182 for entry in index:
182 for entry in index:
183 path = pycompat.fsencode(entry.path)
183 path = pycompat.fsencode(entry.path)
184 if not match(path):
184 if not match(path):
185 continue
185 continue
186 if path in observed:
186 if path in observed:
187 continue # already in some other set
187 continue # already in some other set
188 if path[-1] == b'/':
188 if path[-1] == b'/':
189 continue # directory
189 continue # directory
190 clean.append(path)
190 clean.append(path)
191
191
192 # TODO are we really always sure of status here?
192 # TODO are we really always sure of status here?
193 return (
193 return (
194 False,
194 False,
195 scmutil.status(
195 scmutil.status(
196 modified, added, removed, deleted, unknown, ignored, clean
196 modified, added, removed, deleted, unknown, ignored, clean
197 ),
197 ),
198 )
198 )
199
199
200 def flagfunc(self, buildfallback):
200 def flagfunc(self, buildfallback):
201 # TODO we can do better
201 # TODO we can do better
202 return buildfallback()
202 return buildfallback()
203
203
204 def getcwd(self):
204 def getcwd(self):
205 # TODO is this a good way to do this?
205 # TODO is this a good way to do this?
206 return os.path.dirname(
206 return os.path.dirname(
207 os.path.dirname(pycompat.fsencode(self.git.path))
207 os.path.dirname(pycompat.fsencode(self.git.path))
208 )
208 )
209
209
210 def normalize(self, path):
210 def normalize(self, path):
211 normed = util.normcase(path)
211 normed = util.normcase(path)
212 assert normed == path, b"TODO handling of case folding: %s != %s" % (
212 assert normed == path, b"TODO handling of case folding: %s != %s" % (
213 normed,
213 normed,
214 path,
214 path,
215 )
215 )
216 return path
216 return path
217
217
218 @property
218 @property
219 def _checklink(self):
219 def _checklink(self):
220 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
220 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
221
221
222 def copies(self):
222 def copies(self):
223 # TODO support copies?
223 # TODO support copies?
224 return {}
224 return {}
225
225
226 # # TODO what the heck is this
226 # # TODO what the heck is this
227 _filecache = set()
227 _filecache = set()
228
228
229 def pendingparentchange(self):
229 def pendingparentchange(self):
230 # TODO: we need to implement the context manager bits and
230 # TODO: we need to implement the context manager bits and
231 # correctly stage/revert index edits.
231 # correctly stage/revert index edits.
232 return False
232 return False
233
233
234 def write(self, tr):
234 def write(self, tr):
235 # TODO: call parent change callbacks
235 # TODO: call parent change callbacks
236
236
237 if tr:
237 if tr:
238
238
239 def writeinner(category):
239 def writeinner(category):
240 self.git.index.write()
240 self.git.index.write()
241
241
242 tr.addpending(b'gitdirstate', writeinner)
242 tr.addpending(b'gitdirstate', writeinner)
243 else:
243 else:
244 self.git.index.write()
244 self.git.index.write()
245
245
246 def pathto(self, f, cwd=None):
246 def pathto(self, f, cwd=None):
247 if cwd is None:
247 if cwd is None:
248 cwd = self.getcwd()
248 cwd = self.getcwd()
249 # TODO core dirstate does something about slashes here
249 # TODO core dirstate does something about slashes here
250 assert isinstance(f, bytes)
250 assert isinstance(f, bytes)
251 r = util.pathto(self._root, cwd, f)
251 r = util.pathto(self._root, cwd, f)
252 return r
252 return r
253
253
254 def matches(self, match):
254 def matches(self, match):
255 for x in self.git.index:
255 for x in self.git.index:
256 p = pycompat.fsencode(x.path)
256 p = pycompat.fsencode(x.path)
257 if match(p):
257 if match(p):
258 yield p
258 yield p
259
259
260 def set_clean(self, f, parentfiledata=None):
260 def set_clean(self, f, parentfiledata):
261 """Mark a file normal and clean."""
261 """Mark a file normal and clean."""
262 # TODO: for now we just let libgit2 re-stat the file. We can
262 # TODO: for now we just let libgit2 re-stat the file. We can
263 # clearly do better.
263 # clearly do better.
264
264
265 def set_possibly_dirty(self, f):
265 def set_possibly_dirty(self, f):
266 """Mark a file normal, but possibly dirty."""
266 """Mark a file normal, but possibly dirty."""
267 # TODO: for now we just let libgit2 re-stat the file. We can
267 # TODO: for now we just let libgit2 re-stat the file. We can
268 # clearly do better.
268 # clearly do better.
269
269
270 def walk(self, match, subrepos, unknown, ignored, full=True):
270 def walk(self, match, subrepos, unknown, ignored, full=True):
271 # TODO: we need to use .status() and not iterate the index,
271 # TODO: we need to use .status() and not iterate the index,
272 # because the index doesn't force a re-walk and so `hg add` of
272 # because the index doesn't force a re-walk and so `hg add` of
273 # a new file without an intervening call to status will
273 # a new file without an intervening call to status will
274 # silently do nothing.
274 # silently do nothing.
275 r = {}
275 r = {}
276 cwd = self.getcwd()
276 cwd = self.getcwd()
277 for path, status in self.git.status().items():
277 for path, status in self.git.status().items():
278 if path.startswith('.hg/'):
278 if path.startswith('.hg/'):
279 continue
279 continue
280 path = pycompat.fsencode(path)
280 path = pycompat.fsencode(path)
281 if not match(path):
281 if not match(path):
282 continue
282 continue
283 # TODO construct the stat info from the status object?
283 # TODO construct the stat info from the status object?
284 try:
284 try:
285 s = os.stat(os.path.join(cwd, path))
285 s = os.stat(os.path.join(cwd, path))
286 except OSError as e:
286 except OSError as e:
287 if e.errno != errno.ENOENT:
287 if e.errno != errno.ENOENT:
288 raise
288 raise
289 continue
289 continue
290 r[path] = s
290 r[path] = s
291 return r
291 return r
292
292
293 def savebackup(self, tr, backupname):
293 def savebackup(self, tr, backupname):
294 # TODO: figure out a strategy for saving index backups.
294 # TODO: figure out a strategy for saving index backups.
295 pass
295 pass
296
296
297 def restorebackup(self, tr, backupname):
297 def restorebackup(self, tr, backupname):
298 # TODO: figure out a strategy for saving index backups.
298 # TODO: figure out a strategy for saving index backups.
299 pass
299 pass
300
300
301 def set_tracked(self, f):
301 def set_tracked(self, f):
302 uf = pycompat.fsdecode(f)
302 uf = pycompat.fsdecode(f)
303 if uf in self.git.index:
303 if uf in self.git.index:
304 return False
304 return False
305 index = self.git.index
305 index = self.git.index
306 index.read()
306 index.read()
307 index.add(uf)
307 index.add(uf)
308 index.write()
308 index.write()
309 return True
309 return True
310
310
311 def add(self, f):
311 def add(self, f):
312 index = self.git.index
312 index = self.git.index
313 index.read()
313 index.read()
314 index.add(pycompat.fsdecode(f))
314 index.add(pycompat.fsdecode(f))
315 index.write()
315 index.write()
316
316
317 def drop(self, f):
317 def drop(self, f):
318 index = self.git.index
318 index = self.git.index
319 index.read()
319 index.read()
320 fs = pycompat.fsdecode(f)
320 fs = pycompat.fsdecode(f)
321 if fs in index:
321 if fs in index:
322 index.remove(fs)
322 index.remove(fs)
323 index.write()
323 index.write()
324
324
325 def set_untracked(self, f):
325 def set_untracked(self, f):
326 index = self.git.index
326 index = self.git.index
327 index.read()
327 index.read()
328 fs = pycompat.fsdecode(f)
328 fs = pycompat.fsdecode(f)
329 if fs in index:
329 if fs in index:
330 index.remove(fs)
330 index.remove(fs)
331 index.write()
331 index.write()
332 return True
332 return True
333 return False
333 return False
334
334
335 def remove(self, f):
335 def remove(self, f):
336 index = self.git.index
336 index = self.git.index
337 index.read()
337 index.read()
338 index.remove(pycompat.fsdecode(f))
338 index.remove(pycompat.fsdecode(f))
339 index.write()
339 index.write()
340
340
341 def copied(self, path):
341 def copied(self, path):
342 # TODO: track copies?
342 # TODO: track copies?
343 return None
343 return None
344
344
345 def prefetch_parents(self):
345 def prefetch_parents(self):
346 # TODO
346 # TODO
347 pass
347 pass
348
348
349 def update_file(self, *args, **kwargs):
349 def update_file(self, *args, **kwargs):
350 # TODO
350 # TODO
351 pass
351 pass
352
352
353 @contextlib.contextmanager
353 @contextlib.contextmanager
354 def parentchange(self):
354 def parentchange(self):
355 # TODO: track this maybe?
355 # TODO: track this maybe?
356 yield
356 yield
357
357
358 def addparentchangecallback(self, category, callback):
358 def addparentchangecallback(self, category, callback):
359 # TODO: should this be added to the dirstate interface?
359 # TODO: should this be added to the dirstate interface?
360 self._plchangecallbacks[category] = callback
360 self._plchangecallbacks[category] = callback
361
361
362 def clearbackup(self, tr, backupname):
362 def clearbackup(self, tr, backupname):
363 # TODO
363 # TODO
364 pass
364 pass
365
365
366 def setbranch(self, branch):
366 def setbranch(self, branch):
367 raise error.Abort(
367 raise error.Abort(
368 b'git repos do not support branches. try using bookmarks'
368 b'git repos do not support branches. try using bookmarks'
369 )
369 )
@@ -1,1529 +1,1526 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import contextlib
11 import contextlib
12 import errno
12 import errno
13 import os
13 import os
14 import stat
14 import stat
15
15
16 from .i18n import _
16 from .i18n import _
17 from .pycompat import delattr
17 from .pycompat import delattr
18
18
19 from hgdemandimport import tracing
19 from hgdemandimport import tracing
20
20
21 from . import (
21 from . import (
22 dirstatemap,
22 dirstatemap,
23 encoding,
23 encoding,
24 error,
24 error,
25 match as matchmod,
25 match as matchmod,
26 pathutil,
26 pathutil,
27 policy,
27 policy,
28 pycompat,
28 pycompat,
29 scmutil,
29 scmutil,
30 sparse,
30 sparse,
31 util,
31 util,
32 )
32 )
33
33
34 from .dirstateutils import (
34 from .dirstateutils import (
35 timestamp,
35 timestamp,
36 )
36 )
37
37
38 from .interfaces import (
38 from .interfaces import (
39 dirstate as intdirstate,
39 dirstate as intdirstate,
40 util as interfaceutil,
40 util as interfaceutil,
41 )
41 )
42
42
43 parsers = policy.importmod('parsers')
43 parsers = policy.importmod('parsers')
44 rustmod = policy.importrust('dirstate')
44 rustmod = policy.importrust('dirstate')
45
45
46 HAS_FAST_DIRSTATE_V2 = rustmod is not None
46 HAS_FAST_DIRSTATE_V2 = rustmod is not None
47
47
48 propertycache = util.propertycache
48 propertycache = util.propertycache
49 filecache = scmutil.filecache
49 filecache = scmutil.filecache
50 _rangemask = dirstatemap.rangemask
50 _rangemask = dirstatemap.rangemask
51
51
52 DirstateItem = dirstatemap.DirstateItem
52 DirstateItem = dirstatemap.DirstateItem
53
53
54
54
55 class repocache(filecache):
55 class repocache(filecache):
56 """filecache for files in .hg/"""
56 """filecache for files in .hg/"""
57
57
58 def join(self, obj, fname):
58 def join(self, obj, fname):
59 return obj._opener.join(fname)
59 return obj._opener.join(fname)
60
60
61
61
62 class rootcache(filecache):
62 class rootcache(filecache):
63 """filecache for files in the repository root"""
63 """filecache for files in the repository root"""
64
64
65 def join(self, obj, fname):
65 def join(self, obj, fname):
66 return obj._join(fname)
66 return obj._join(fname)
67
67
68
68
69 def requires_parents_change(func):
69 def requires_parents_change(func):
70 def wrap(self, *args, **kwargs):
70 def wrap(self, *args, **kwargs):
71 if not self.pendingparentchange():
71 if not self.pendingparentchange():
72 msg = 'calling `%s` outside of a parentchange context'
72 msg = 'calling `%s` outside of a parentchange context'
73 msg %= func.__name__
73 msg %= func.__name__
74 raise error.ProgrammingError(msg)
74 raise error.ProgrammingError(msg)
75 return func(self, *args, **kwargs)
75 return func(self, *args, **kwargs)
76
76
77 return wrap
77 return wrap
78
78
79
79
80 def requires_no_parents_change(func):
80 def requires_no_parents_change(func):
81 def wrap(self, *args, **kwargs):
81 def wrap(self, *args, **kwargs):
82 if self.pendingparentchange():
82 if self.pendingparentchange():
83 msg = 'calling `%s` inside of a parentchange context'
83 msg = 'calling `%s` inside of a parentchange context'
84 msg %= func.__name__
84 msg %= func.__name__
85 raise error.ProgrammingError(msg)
85 raise error.ProgrammingError(msg)
86 return func(self, *args, **kwargs)
86 return func(self, *args, **kwargs)
87
87
88 return wrap
88 return wrap
89
89
90
90
91 @interfaceutil.implementer(intdirstate.idirstate)
91 @interfaceutil.implementer(intdirstate.idirstate)
92 class dirstate(object):
92 class dirstate(object):
93 def __init__(
93 def __init__(
94 self,
94 self,
95 opener,
95 opener,
96 ui,
96 ui,
97 root,
97 root,
98 validate,
98 validate,
99 sparsematchfn,
99 sparsematchfn,
100 nodeconstants,
100 nodeconstants,
101 use_dirstate_v2,
101 use_dirstate_v2,
102 ):
102 ):
103 """Create a new dirstate object.
103 """Create a new dirstate object.
104
104
105 opener is an open()-like callable that can be used to open the
105 opener is an open()-like callable that can be used to open the
106 dirstate file; root is the root of the directory tracked by
106 dirstate file; root is the root of the directory tracked by
107 the dirstate.
107 the dirstate.
108 """
108 """
109 self._use_dirstate_v2 = use_dirstate_v2
109 self._use_dirstate_v2 = use_dirstate_v2
110 self._nodeconstants = nodeconstants
110 self._nodeconstants = nodeconstants
111 self._opener = opener
111 self._opener = opener
112 self._validate = validate
112 self._validate = validate
113 self._root = root
113 self._root = root
114 self._sparsematchfn = sparsematchfn
114 self._sparsematchfn = sparsematchfn
115 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
115 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
116 # UNC path pointing to root share (issue4557)
116 # UNC path pointing to root share (issue4557)
117 self._rootdir = pathutil.normasprefix(root)
117 self._rootdir = pathutil.normasprefix(root)
118 self._dirty = False
118 self._dirty = False
119 self._lastnormaltime = timestamp.zero()
119 self._lastnormaltime = timestamp.zero()
120 self._ui = ui
120 self._ui = ui
121 self._filecache = {}
121 self._filecache = {}
122 self._parentwriters = 0
122 self._parentwriters = 0
123 self._filename = b'dirstate'
123 self._filename = b'dirstate'
124 self._pendingfilename = b'%s.pending' % self._filename
124 self._pendingfilename = b'%s.pending' % self._filename
125 self._plchangecallbacks = {}
125 self._plchangecallbacks = {}
126 self._origpl = None
126 self._origpl = None
127 self._mapcls = dirstatemap.dirstatemap
127 self._mapcls = dirstatemap.dirstatemap
128 # Access and cache cwd early, so we don't access it for the first time
128 # Access and cache cwd early, so we don't access it for the first time
129 # after a working-copy update caused it to not exist (accessing it then
129 # after a working-copy update caused it to not exist (accessing it then
130 # raises an exception).
130 # raises an exception).
131 self._cwd
131 self._cwd
132
132
133 def prefetch_parents(self):
133 def prefetch_parents(self):
134 """make sure the parents are loaded
134 """make sure the parents are loaded
135
135
136 Used to avoid a race condition.
136 Used to avoid a race condition.
137 """
137 """
138 self._pl
138 self._pl
139
139
140 @contextlib.contextmanager
140 @contextlib.contextmanager
141 def parentchange(self):
141 def parentchange(self):
142 """Context manager for handling dirstate parents.
142 """Context manager for handling dirstate parents.
143
143
144 If an exception occurs in the scope of the context manager,
144 If an exception occurs in the scope of the context manager,
145 the incoherent dirstate won't be written when wlock is
145 the incoherent dirstate won't be written when wlock is
146 released.
146 released.
147 """
147 """
148 self._parentwriters += 1
148 self._parentwriters += 1
149 yield
149 yield
150 # Typically we want the "undo" step of a context manager in a
150 # Typically we want the "undo" step of a context manager in a
151 # finally block so it happens even when an exception
151 # finally block so it happens even when an exception
152 # occurs. In this case, however, we only want to decrement
152 # occurs. In this case, however, we only want to decrement
153 # parentwriters if the code in the with statement exits
153 # parentwriters if the code in the with statement exits
154 # normally, so we don't have a try/finally here on purpose.
154 # normally, so we don't have a try/finally here on purpose.
155 self._parentwriters -= 1
155 self._parentwriters -= 1
156
156
157 def pendingparentchange(self):
157 def pendingparentchange(self):
158 """Returns true if the dirstate is in the middle of a set of changes
158 """Returns true if the dirstate is in the middle of a set of changes
159 that modify the dirstate parent.
159 that modify the dirstate parent.
160 """
160 """
161 return self._parentwriters > 0
161 return self._parentwriters > 0
162
162
163 @propertycache
163 @propertycache
164 def _map(self):
164 def _map(self):
165 """Return the dirstate contents (see documentation for dirstatemap)."""
165 """Return the dirstate contents (see documentation for dirstatemap)."""
166 self._map = self._mapcls(
166 self._map = self._mapcls(
167 self._ui,
167 self._ui,
168 self._opener,
168 self._opener,
169 self._root,
169 self._root,
170 self._nodeconstants,
170 self._nodeconstants,
171 self._use_dirstate_v2,
171 self._use_dirstate_v2,
172 )
172 )
173 return self._map
173 return self._map
174
174
175 @property
175 @property
176 def _sparsematcher(self):
176 def _sparsematcher(self):
177 """The matcher for the sparse checkout.
177 """The matcher for the sparse checkout.
178
178
179 The working directory may not include every file from a manifest. The
179 The working directory may not include every file from a manifest. The
180 matcher obtained by this property will match a path if it is to be
180 matcher obtained by this property will match a path if it is to be
181 included in the working directory.
181 included in the working directory.
182 """
182 """
183 # TODO there is potential to cache this property. For now, the matcher
183 # TODO there is potential to cache this property. For now, the matcher
184 # is resolved on every access. (But the called function does use a
184 # is resolved on every access. (But the called function does use a
185 # cache to keep the lookup fast.)
185 # cache to keep the lookup fast.)
186 return self._sparsematchfn()
186 return self._sparsematchfn()
187
187
188 @repocache(b'branch')
188 @repocache(b'branch')
189 def _branch(self):
189 def _branch(self):
190 try:
190 try:
191 return self._opener.read(b"branch").strip() or b"default"
191 return self._opener.read(b"branch").strip() or b"default"
192 except IOError as inst:
192 except IOError as inst:
193 if inst.errno != errno.ENOENT:
193 if inst.errno != errno.ENOENT:
194 raise
194 raise
195 return b"default"
195 return b"default"
196
196
197 @property
197 @property
198 def _pl(self):
198 def _pl(self):
199 return self._map.parents()
199 return self._map.parents()
200
200
201 def hasdir(self, d):
201 def hasdir(self, d):
202 return self._map.hastrackeddir(d)
202 return self._map.hastrackeddir(d)
203
203
204 @rootcache(b'.hgignore')
204 @rootcache(b'.hgignore')
205 def _ignore(self):
205 def _ignore(self):
206 files = self._ignorefiles()
206 files = self._ignorefiles()
207 if not files:
207 if not files:
208 return matchmod.never()
208 return matchmod.never()
209
209
210 pats = [b'include:%s' % f for f in files]
210 pats = [b'include:%s' % f for f in files]
211 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
211 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
212
212
213 @propertycache
213 @propertycache
214 def _slash(self):
214 def _slash(self):
215 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
215 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
216
216
217 @propertycache
217 @propertycache
218 def _checklink(self):
218 def _checklink(self):
219 return util.checklink(self._root)
219 return util.checklink(self._root)
220
220
221 @propertycache
221 @propertycache
222 def _checkexec(self):
222 def _checkexec(self):
223 return bool(util.checkexec(self._root))
223 return bool(util.checkexec(self._root))
224
224
225 @propertycache
225 @propertycache
226 def _checkcase(self):
226 def _checkcase(self):
227 return not util.fscasesensitive(self._join(b'.hg'))
227 return not util.fscasesensitive(self._join(b'.hg'))
228
228
229 def _join(self, f):
229 def _join(self, f):
230 # much faster than os.path.join()
230 # much faster than os.path.join()
231 # it's safe because f is always a relative path
231 # it's safe because f is always a relative path
232 return self._rootdir + f
232 return self._rootdir + f
233
233
234 def flagfunc(self, buildfallback):
234 def flagfunc(self, buildfallback):
235 """build a callable that returns flags associated with a filename
235 """build a callable that returns flags associated with a filename
236
236
237 The information is extracted from three possible layers:
237 The information is extracted from three possible layers:
238 1. the file system if it supports the information
238 1. the file system if it supports the information
239 2. the "fallback" information stored in the dirstate if any
239 2. the "fallback" information stored in the dirstate if any
240 3. a more expensive mechanism inferring the flags from the parents.
240 3. a more expensive mechanism inferring the flags from the parents.
241 """
241 """
242
242
243 # small hack to cache the result of buildfallback()
243 # small hack to cache the result of buildfallback()
244 fallback_func = []
244 fallback_func = []
245
245
246 def get_flags(x):
246 def get_flags(x):
247 entry = None
247 entry = None
248 fallback_value = None
248 fallback_value = None
249 try:
249 try:
250 st = os.lstat(self._join(x))
250 st = os.lstat(self._join(x))
251 except OSError:
251 except OSError:
252 return b''
252 return b''
253
253
254 if self._checklink:
254 if self._checklink:
255 if util.statislink(st):
255 if util.statislink(st):
256 return b'l'
256 return b'l'
257 else:
257 else:
258 entry = self.get_entry(x)
258 entry = self.get_entry(x)
259 if entry.has_fallback_symlink:
259 if entry.has_fallback_symlink:
260 if entry.fallback_symlink:
260 if entry.fallback_symlink:
261 return b'l'
261 return b'l'
262 else:
262 else:
263 if not fallback_func:
263 if not fallback_func:
264 fallback_func.append(buildfallback())
264 fallback_func.append(buildfallback())
265 fallback_value = fallback_func[0](x)
265 fallback_value = fallback_func[0](x)
266 if b'l' in fallback_value:
266 if b'l' in fallback_value:
267 return b'l'
267 return b'l'
268
268
269 if self._checkexec:
269 if self._checkexec:
270 if util.statisexec(st):
270 if util.statisexec(st):
271 return b'x'
271 return b'x'
272 else:
272 else:
273 if entry is None:
273 if entry is None:
274 entry = self.get_entry(x)
274 entry = self.get_entry(x)
275 if entry.has_fallback_exec:
275 if entry.has_fallback_exec:
276 if entry.fallback_exec:
276 if entry.fallback_exec:
277 return b'x'
277 return b'x'
278 else:
278 else:
279 if fallback_value is None:
279 if fallback_value is None:
280 if not fallback_func:
280 if not fallback_func:
281 fallback_func.append(buildfallback())
281 fallback_func.append(buildfallback())
282 fallback_value = fallback_func[0](x)
282 fallback_value = fallback_func[0](x)
283 if b'x' in fallback_value:
283 if b'x' in fallback_value:
284 return b'x'
284 return b'x'
285 return b''
285 return b''
286
286
287 return get_flags
287 return get_flags
288
288
289 @propertycache
289 @propertycache
290 def _cwd(self):
290 def _cwd(self):
291 # internal config: ui.forcecwd
291 # internal config: ui.forcecwd
292 forcecwd = self._ui.config(b'ui', b'forcecwd')
292 forcecwd = self._ui.config(b'ui', b'forcecwd')
293 if forcecwd:
293 if forcecwd:
294 return forcecwd
294 return forcecwd
295 return encoding.getcwd()
295 return encoding.getcwd()
296
296
297 def getcwd(self):
297 def getcwd(self):
298 """Return the path from which a canonical path is calculated.
298 """Return the path from which a canonical path is calculated.
299
299
300 This path should be used to resolve file patterns or to convert
300 This path should be used to resolve file patterns or to convert
301 canonical paths back to file paths for display. It shouldn't be
301 canonical paths back to file paths for display. It shouldn't be
302 used to get real file paths. Use vfs functions instead.
302 used to get real file paths. Use vfs functions instead.
303 """
303 """
304 cwd = self._cwd
304 cwd = self._cwd
305 if cwd == self._root:
305 if cwd == self._root:
306 return b''
306 return b''
307 # self._root ends with a path separator if self._root is '/' or 'C:\'
307 # self._root ends with a path separator if self._root is '/' or 'C:\'
308 rootsep = self._root
308 rootsep = self._root
309 if not util.endswithsep(rootsep):
309 if not util.endswithsep(rootsep):
310 rootsep += pycompat.ossep
310 rootsep += pycompat.ossep
311 if cwd.startswith(rootsep):
311 if cwd.startswith(rootsep):
312 return cwd[len(rootsep) :]
312 return cwd[len(rootsep) :]
313 else:
313 else:
314 # we're outside the repo. return an absolute path.
314 # we're outside the repo. return an absolute path.
315 return cwd
315 return cwd
316
316
317 def pathto(self, f, cwd=None):
317 def pathto(self, f, cwd=None):
318 if cwd is None:
318 if cwd is None:
319 cwd = self.getcwd()
319 cwd = self.getcwd()
320 path = util.pathto(self._root, cwd, f)
320 path = util.pathto(self._root, cwd, f)
321 if self._slash:
321 if self._slash:
322 return util.pconvert(path)
322 return util.pconvert(path)
323 return path
323 return path
324
324
325 def __getitem__(self, key):
325 def __getitem__(self, key):
326 """Return the current state of key (a filename) in the dirstate.
326 """Return the current state of key (a filename) in the dirstate.
327
327
328 States are:
328 States are:
329 n normal
329 n normal
330 m needs merging
330 m needs merging
331 r marked for removal
331 r marked for removal
332 a marked for addition
332 a marked for addition
333 ? not tracked
333 ? not tracked
334
334
335 XXX The "state" is a bit obscure to be in the "public" API. we should
335 XXX The "state" is a bit obscure to be in the "public" API. we should
336 consider migrating all user of this to going through the dirstate entry
336 consider migrating all user of this to going through the dirstate entry
337 instead.
337 instead.
338 """
338 """
339 msg = b"don't use dirstate[file], use dirstate.get_entry(file)"
339 msg = b"don't use dirstate[file], use dirstate.get_entry(file)"
340 util.nouideprecwarn(msg, b'6.1', stacklevel=2)
340 util.nouideprecwarn(msg, b'6.1', stacklevel=2)
341 entry = self._map.get(key)
341 entry = self._map.get(key)
342 if entry is not None:
342 if entry is not None:
343 return entry.state
343 return entry.state
344 return b'?'
344 return b'?'
345
345
346 def get_entry(self, path):
346 def get_entry(self, path):
347 """return a DirstateItem for the associated path"""
347 """return a DirstateItem for the associated path"""
348 entry = self._map.get(path)
348 entry = self._map.get(path)
349 if entry is None:
349 if entry is None:
350 return DirstateItem()
350 return DirstateItem()
351 return entry
351 return entry
352
352
353 def __contains__(self, key):
353 def __contains__(self, key):
354 return key in self._map
354 return key in self._map
355
355
356 def __iter__(self):
356 def __iter__(self):
357 return iter(sorted(self._map))
357 return iter(sorted(self._map))
358
358
359 def items(self):
359 def items(self):
360 return pycompat.iteritems(self._map)
360 return pycompat.iteritems(self._map)
361
361
362 iteritems = items
362 iteritems = items
363
363
364 def parents(self):
364 def parents(self):
365 return [self._validate(p) for p in self._pl]
365 return [self._validate(p) for p in self._pl]
366
366
367 def p1(self):
367 def p1(self):
368 return self._validate(self._pl[0])
368 return self._validate(self._pl[0])
369
369
370 def p2(self):
370 def p2(self):
371 return self._validate(self._pl[1])
371 return self._validate(self._pl[1])
372
372
373 @property
373 @property
374 def in_merge(self):
374 def in_merge(self):
375 """True if a merge is in progress"""
375 """True if a merge is in progress"""
376 return self._pl[1] != self._nodeconstants.nullid
376 return self._pl[1] != self._nodeconstants.nullid
377
377
378 def branch(self):
378 def branch(self):
379 return encoding.tolocal(self._branch)
379 return encoding.tolocal(self._branch)
380
380
381 def setparents(self, p1, p2=None):
381 def setparents(self, p1, p2=None):
382 """Set dirstate parents to p1 and p2.
382 """Set dirstate parents to p1 and p2.
383
383
384 When moving from two parents to one, "merged" entries a
384 When moving from two parents to one, "merged" entries a
385 adjusted to normal and previous copy records discarded and
385 adjusted to normal and previous copy records discarded and
386 returned by the call.
386 returned by the call.
387
387
388 See localrepo.setparents()
388 See localrepo.setparents()
389 """
389 """
390 if p2 is None:
390 if p2 is None:
391 p2 = self._nodeconstants.nullid
391 p2 = self._nodeconstants.nullid
392 if self._parentwriters == 0:
392 if self._parentwriters == 0:
393 raise ValueError(
393 raise ValueError(
394 b"cannot set dirstate parent outside of "
394 b"cannot set dirstate parent outside of "
395 b"dirstate.parentchange context manager"
395 b"dirstate.parentchange context manager"
396 )
396 )
397
397
398 self._dirty = True
398 self._dirty = True
399 oldp2 = self._pl[1]
399 oldp2 = self._pl[1]
400 if self._origpl is None:
400 if self._origpl is None:
401 self._origpl = self._pl
401 self._origpl = self._pl
402 nullid = self._nodeconstants.nullid
402 nullid = self._nodeconstants.nullid
403 # True if we need to fold p2 related state back to a linear case
403 # True if we need to fold p2 related state back to a linear case
404 fold_p2 = oldp2 != nullid and p2 == nullid
404 fold_p2 = oldp2 != nullid and p2 == nullid
405 return self._map.setparents(p1, p2, fold_p2=fold_p2)
405 return self._map.setparents(p1, p2, fold_p2=fold_p2)
406
406
407 def setbranch(self, branch):
407 def setbranch(self, branch):
408 self.__class__._branch.set(self, encoding.fromlocal(branch))
408 self.__class__._branch.set(self, encoding.fromlocal(branch))
409 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
409 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
410 try:
410 try:
411 f.write(self._branch + b'\n')
411 f.write(self._branch + b'\n')
412 f.close()
412 f.close()
413
413
414 # make sure filecache has the correct stat info for _branch after
414 # make sure filecache has the correct stat info for _branch after
415 # replacing the underlying file
415 # replacing the underlying file
416 ce = self._filecache[b'_branch']
416 ce = self._filecache[b'_branch']
417 if ce:
417 if ce:
418 ce.refresh()
418 ce.refresh()
419 except: # re-raises
419 except: # re-raises
420 f.discard()
420 f.discard()
421 raise
421 raise
422
422
423 def invalidate(self):
423 def invalidate(self):
424 """Causes the next access to reread the dirstate.
424 """Causes the next access to reread the dirstate.
425
425
426 This is different from localrepo.invalidatedirstate() because it always
426 This is different from localrepo.invalidatedirstate() because it always
427 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
427 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
428 check whether the dirstate has changed before rereading it."""
428 check whether the dirstate has changed before rereading it."""
429
429
430 for a in ("_map", "_branch", "_ignore"):
430 for a in ("_map", "_branch", "_ignore"):
431 if a in self.__dict__:
431 if a in self.__dict__:
432 delattr(self, a)
432 delattr(self, a)
433 self._lastnormaltime = timestamp.zero()
433 self._lastnormaltime = timestamp.zero()
434 self._dirty = False
434 self._dirty = False
435 self._parentwriters = 0
435 self._parentwriters = 0
436 self._origpl = None
436 self._origpl = None
437
437
438 def copy(self, source, dest):
438 def copy(self, source, dest):
439 """Mark dest as a copy of source. Unmark dest if source is None."""
439 """Mark dest as a copy of source. Unmark dest if source is None."""
440 if source == dest:
440 if source == dest:
441 return
441 return
442 self._dirty = True
442 self._dirty = True
443 if source is not None:
443 if source is not None:
444 self._map.copymap[dest] = source
444 self._map.copymap[dest] = source
445 else:
445 else:
446 self._map.copymap.pop(dest, None)
446 self._map.copymap.pop(dest, None)
447
447
448 def copied(self, file):
448 def copied(self, file):
449 return self._map.copymap.get(file, None)
449 return self._map.copymap.get(file, None)
450
450
451 def copies(self):
451 def copies(self):
452 return self._map.copymap
452 return self._map.copymap
453
453
454 @requires_no_parents_change
454 @requires_no_parents_change
455 def set_tracked(self, filename, reset_copy=False):
455 def set_tracked(self, filename, reset_copy=False):
456 """a "public" method for generic code to mark a file as tracked
456 """a "public" method for generic code to mark a file as tracked
457
457
458 This function is to be called outside of "update/merge" case. For
458 This function is to be called outside of "update/merge" case. For
459 example by a command like `hg add X`.
459 example by a command like `hg add X`.
460
460
461 if reset_copy is set, any existing copy information will be dropped.
461 if reset_copy is set, any existing copy information will be dropped.
462
462
463 return True the file was previously untracked, False otherwise.
463 return True the file was previously untracked, False otherwise.
464 """
464 """
465 self._dirty = True
465 self._dirty = True
466 entry = self._map.get(filename)
466 entry = self._map.get(filename)
467 if entry is None or not entry.tracked:
467 if entry is None or not entry.tracked:
468 self._check_new_tracked_filename(filename)
468 self._check_new_tracked_filename(filename)
469 pre_tracked = self._map.set_tracked(filename)
469 pre_tracked = self._map.set_tracked(filename)
470 if reset_copy:
470 if reset_copy:
471 self._map.copymap.pop(filename, None)
471 self._map.copymap.pop(filename, None)
472 return pre_tracked
472 return pre_tracked
473
473
474 @requires_no_parents_change
474 @requires_no_parents_change
475 def set_untracked(self, filename):
475 def set_untracked(self, filename):
476 """a "public" method for generic code to mark a file as untracked
476 """a "public" method for generic code to mark a file as untracked
477
477
478 This function is to be called outside of "update/merge" case. For
478 This function is to be called outside of "update/merge" case. For
479 example by a command like `hg remove X`.
479 example by a command like `hg remove X`.
480
480
481 return True the file was previously tracked, False otherwise.
481 return True the file was previously tracked, False otherwise.
482 """
482 """
483 ret = self._map.set_untracked(filename)
483 ret = self._map.set_untracked(filename)
484 if ret:
484 if ret:
485 self._dirty = True
485 self._dirty = True
486 return ret
486 return ret
487
487
488 @requires_no_parents_change
488 @requires_no_parents_change
489 def set_clean(self, filename, parentfiledata=None):
489 def set_clean(self, filename, parentfiledata):
490 """record that the current state of the file on disk is known to be clean"""
490 """record that the current state of the file on disk is known to be clean"""
491 self._dirty = True
491 self._dirty = True
492 if parentfiledata:
493 (mode, size, mtime) = parentfiledata
494 else:
495 (mode, size, mtime) = self._get_filedata(filename)
496 if not self._map[filename].tracked:
492 if not self._map[filename].tracked:
497 self._check_new_tracked_filename(filename)
493 self._check_new_tracked_filename(filename)
494 (mode, size, mtime) = parentfiledata
498 self._map.set_clean(filename, mode, size, mtime)
495 self._map.set_clean(filename, mode, size, mtime)
499 if mtime > self._lastnormaltime:
496 if mtime > self._lastnormaltime:
500 # Remember the most recent modification timeslot for status(),
497 # Remember the most recent modification timeslot for status(),
501 # to make sure we won't miss future size-preserving file content
498 # to make sure we won't miss future size-preserving file content
502 # modifications that happen within the same timeslot.
499 # modifications that happen within the same timeslot.
503 self._lastnormaltime = mtime
500 self._lastnormaltime = mtime
504
501
505 @requires_no_parents_change
502 @requires_no_parents_change
506 def set_possibly_dirty(self, filename):
503 def set_possibly_dirty(self, filename):
507 """record that the current state of the file on disk is unknown"""
504 """record that the current state of the file on disk is unknown"""
508 self._dirty = True
505 self._dirty = True
509 self._map.set_possibly_dirty(filename)
506 self._map.set_possibly_dirty(filename)
510
507
511 @requires_parents_change
508 @requires_parents_change
512 def update_file_p1(
509 def update_file_p1(
513 self,
510 self,
514 filename,
511 filename,
515 p1_tracked,
512 p1_tracked,
516 ):
513 ):
517 """Set a file as tracked in the parent (or not)
514 """Set a file as tracked in the parent (or not)
518
515
519 This is to be called when adjust the dirstate to a new parent after an history
516 This is to be called when adjust the dirstate to a new parent after an history
520 rewriting operation.
517 rewriting operation.
521
518
522 It should not be called during a merge (p2 != nullid) and only within
519 It should not be called during a merge (p2 != nullid) and only within
523 a `with dirstate.parentchange():` context.
520 a `with dirstate.parentchange():` context.
524 """
521 """
525 if self.in_merge:
522 if self.in_merge:
526 msg = b'update_file_reference should not be called when merging'
523 msg = b'update_file_reference should not be called when merging'
527 raise error.ProgrammingError(msg)
524 raise error.ProgrammingError(msg)
528 entry = self._map.get(filename)
525 entry = self._map.get(filename)
529 if entry is None:
526 if entry is None:
530 wc_tracked = False
527 wc_tracked = False
531 else:
528 else:
532 wc_tracked = entry.tracked
529 wc_tracked = entry.tracked
533 if not (p1_tracked or wc_tracked):
530 if not (p1_tracked or wc_tracked):
534 # the file is no longer relevant to anyone
531 # the file is no longer relevant to anyone
535 if self._map.get(filename) is not None:
532 if self._map.get(filename) is not None:
536 self._map.reset_state(filename)
533 self._map.reset_state(filename)
537 self._dirty = True
534 self._dirty = True
538 elif (not p1_tracked) and wc_tracked:
535 elif (not p1_tracked) and wc_tracked:
539 if entry is not None and entry.added:
536 if entry is not None and entry.added:
540 return # avoid dropping copy information (maybe?)
537 return # avoid dropping copy information (maybe?)
541
538
542 parentfiledata = None
539 parentfiledata = None
543 if wc_tracked and p1_tracked:
540 if wc_tracked and p1_tracked:
544 parentfiledata = self._get_filedata(filename)
541 parentfiledata = self._get_filedata(filename)
545
542
546 self._map.reset_state(
543 self._map.reset_state(
547 filename,
544 filename,
548 wc_tracked,
545 wc_tracked,
549 p1_tracked,
546 p1_tracked,
550 # the underlying reference might have changed, we will have to
547 # the underlying reference might have changed, we will have to
551 # check it.
548 # check it.
552 has_meaningful_mtime=False,
549 has_meaningful_mtime=False,
553 parentfiledata=parentfiledata,
550 parentfiledata=parentfiledata,
554 )
551 )
555 if (
552 if (
556 parentfiledata is not None
553 parentfiledata is not None
557 and parentfiledata[2] > self._lastnormaltime
554 and parentfiledata[2] > self._lastnormaltime
558 ):
555 ):
559 # Remember the most recent modification timeslot for status(),
556 # Remember the most recent modification timeslot for status(),
560 # to make sure we won't miss future size-preserving file content
557 # to make sure we won't miss future size-preserving file content
561 # modifications that happen within the same timeslot.
558 # modifications that happen within the same timeslot.
562 self._lastnormaltime = parentfiledata[2]
559 self._lastnormaltime = parentfiledata[2]
563
560
564 @requires_parents_change
561 @requires_parents_change
565 def update_file(
562 def update_file(
566 self,
563 self,
567 filename,
564 filename,
568 wc_tracked,
565 wc_tracked,
569 p1_tracked,
566 p1_tracked,
570 p2_info=False,
567 p2_info=False,
571 possibly_dirty=False,
568 possibly_dirty=False,
572 parentfiledata=None,
569 parentfiledata=None,
573 ):
570 ):
574 """update the information about a file in the dirstate
571 """update the information about a file in the dirstate
575
572
576 This is to be called when the direstates parent changes to keep track
573 This is to be called when the direstates parent changes to keep track
577 of what is the file situation in regards to the working copy and its parent.
574 of what is the file situation in regards to the working copy and its parent.
578
575
579 This function must be called within a `dirstate.parentchange` context.
576 This function must be called within a `dirstate.parentchange` context.
580
577
581 note: the API is at an early stage and we might need to adjust it
578 note: the API is at an early stage and we might need to adjust it
582 depending of what information ends up being relevant and useful to
579 depending of what information ends up being relevant and useful to
583 other processing.
580 other processing.
584 """
581 """
585
582
586 # note: I do not think we need to double check name clash here since we
583 # note: I do not think we need to double check name clash here since we
587 # are in a update/merge case that should already have taken care of
584 # are in a update/merge case that should already have taken care of
588 # this. The test agrees
585 # this. The test agrees
589
586
590 self._dirty = True
587 self._dirty = True
591
588
592 need_parent_file_data = (
589 need_parent_file_data = (
593 not possibly_dirty and not p2_info and wc_tracked and p1_tracked
590 not possibly_dirty and not p2_info and wc_tracked and p1_tracked
594 )
591 )
595
592
596 if need_parent_file_data and parentfiledata is None:
593 if need_parent_file_data and parentfiledata is None:
597 parentfiledata = self._get_filedata(filename)
594 parentfiledata = self._get_filedata(filename)
598
595
599 self._map.reset_state(
596 self._map.reset_state(
600 filename,
597 filename,
601 wc_tracked,
598 wc_tracked,
602 p1_tracked,
599 p1_tracked,
603 p2_info=p2_info,
600 p2_info=p2_info,
604 has_meaningful_mtime=not possibly_dirty,
601 has_meaningful_mtime=not possibly_dirty,
605 parentfiledata=parentfiledata,
602 parentfiledata=parentfiledata,
606 )
603 )
607 if (
604 if (
608 parentfiledata is not None
605 parentfiledata is not None
609 and parentfiledata[2] is not None
606 and parentfiledata[2] is not None
610 and parentfiledata[2] > self._lastnormaltime
607 and parentfiledata[2] > self._lastnormaltime
611 ):
608 ):
612 # Remember the most recent modification timeslot for status(),
609 # Remember the most recent modification timeslot for status(),
613 # to make sure we won't miss future size-preserving file content
610 # to make sure we won't miss future size-preserving file content
614 # modifications that happen within the same timeslot.
611 # modifications that happen within the same timeslot.
615 self._lastnormaltime = parentfiledata[2]
612 self._lastnormaltime = parentfiledata[2]
616
613
617 def _check_new_tracked_filename(self, filename):
614 def _check_new_tracked_filename(self, filename):
618 scmutil.checkfilename(filename)
615 scmutil.checkfilename(filename)
619 if self._map.hastrackeddir(filename):
616 if self._map.hastrackeddir(filename):
620 msg = _(b'directory %r already in dirstate')
617 msg = _(b'directory %r already in dirstate')
621 msg %= pycompat.bytestr(filename)
618 msg %= pycompat.bytestr(filename)
622 raise error.Abort(msg)
619 raise error.Abort(msg)
623 # shadows
620 # shadows
624 for d in pathutil.finddirs(filename):
621 for d in pathutil.finddirs(filename):
625 if self._map.hastrackeddir(d):
622 if self._map.hastrackeddir(d):
626 break
623 break
627 entry = self._map.get(d)
624 entry = self._map.get(d)
628 if entry is not None and not entry.removed:
625 if entry is not None and not entry.removed:
629 msg = _(b'file %r in dirstate clashes with %r')
626 msg = _(b'file %r in dirstate clashes with %r')
630 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
627 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
631 raise error.Abort(msg)
628 raise error.Abort(msg)
632
629
633 def _get_filedata(self, filename):
630 def _get_filedata(self, filename):
634 """returns"""
631 """returns"""
635 s = os.lstat(self._join(filename))
632 s = os.lstat(self._join(filename))
636 mode = s.st_mode
633 mode = s.st_mode
637 size = s.st_size
634 size = s.st_size
638 mtime = timestamp.mtime_of(s)
635 mtime = timestamp.mtime_of(s)
639 return (mode, size, mtime)
636 return (mode, size, mtime)
640
637
641 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
638 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
642 if exists is None:
639 if exists is None:
643 exists = os.path.lexists(os.path.join(self._root, path))
640 exists = os.path.lexists(os.path.join(self._root, path))
644 if not exists:
641 if not exists:
645 # Maybe a path component exists
642 # Maybe a path component exists
646 if not ignoremissing and b'/' in path:
643 if not ignoremissing and b'/' in path:
647 d, f = path.rsplit(b'/', 1)
644 d, f = path.rsplit(b'/', 1)
648 d = self._normalize(d, False, ignoremissing, None)
645 d = self._normalize(d, False, ignoremissing, None)
649 folded = d + b"/" + f
646 folded = d + b"/" + f
650 else:
647 else:
651 # No path components, preserve original case
648 # No path components, preserve original case
652 folded = path
649 folded = path
653 else:
650 else:
654 # recursively normalize leading directory components
651 # recursively normalize leading directory components
655 # against dirstate
652 # against dirstate
656 if b'/' in normed:
653 if b'/' in normed:
657 d, f = normed.rsplit(b'/', 1)
654 d, f = normed.rsplit(b'/', 1)
658 d = self._normalize(d, False, ignoremissing, True)
655 d = self._normalize(d, False, ignoremissing, True)
659 r = self._root + b"/" + d
656 r = self._root + b"/" + d
660 folded = d + b"/" + util.fspath(f, r)
657 folded = d + b"/" + util.fspath(f, r)
661 else:
658 else:
662 folded = util.fspath(normed, self._root)
659 folded = util.fspath(normed, self._root)
663 storemap[normed] = folded
660 storemap[normed] = folded
664
661
665 return folded
662 return folded
666
663
667 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
664 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
668 normed = util.normcase(path)
665 normed = util.normcase(path)
669 folded = self._map.filefoldmap.get(normed, None)
666 folded = self._map.filefoldmap.get(normed, None)
670 if folded is None:
667 if folded is None:
671 if isknown:
668 if isknown:
672 folded = path
669 folded = path
673 else:
670 else:
674 folded = self._discoverpath(
671 folded = self._discoverpath(
675 path, normed, ignoremissing, exists, self._map.filefoldmap
672 path, normed, ignoremissing, exists, self._map.filefoldmap
676 )
673 )
677 return folded
674 return folded
678
675
679 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
676 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
680 normed = util.normcase(path)
677 normed = util.normcase(path)
681 folded = self._map.filefoldmap.get(normed, None)
678 folded = self._map.filefoldmap.get(normed, None)
682 if folded is None:
679 if folded is None:
683 folded = self._map.dirfoldmap.get(normed, None)
680 folded = self._map.dirfoldmap.get(normed, None)
684 if folded is None:
681 if folded is None:
685 if isknown:
682 if isknown:
686 folded = path
683 folded = path
687 else:
684 else:
688 # store discovered result in dirfoldmap so that future
685 # store discovered result in dirfoldmap so that future
689 # normalizefile calls don't start matching directories
686 # normalizefile calls don't start matching directories
690 folded = self._discoverpath(
687 folded = self._discoverpath(
691 path, normed, ignoremissing, exists, self._map.dirfoldmap
688 path, normed, ignoremissing, exists, self._map.dirfoldmap
692 )
689 )
693 return folded
690 return folded
694
691
695 def normalize(self, path, isknown=False, ignoremissing=False):
692 def normalize(self, path, isknown=False, ignoremissing=False):
696 """
693 """
697 normalize the case of a pathname when on a casefolding filesystem
694 normalize the case of a pathname when on a casefolding filesystem
698
695
699 isknown specifies whether the filename came from walking the
696 isknown specifies whether the filename came from walking the
700 disk, to avoid extra filesystem access.
697 disk, to avoid extra filesystem access.
701
698
702 If ignoremissing is True, missing path are returned
699 If ignoremissing is True, missing path are returned
703 unchanged. Otherwise, we try harder to normalize possibly
700 unchanged. Otherwise, we try harder to normalize possibly
704 existing path components.
701 existing path components.
705
702
706 The normalized case is determined based on the following precedence:
703 The normalized case is determined based on the following precedence:
707
704
708 - version of name already stored in the dirstate
705 - version of name already stored in the dirstate
709 - version of name stored on disk
706 - version of name stored on disk
710 - version provided via command arguments
707 - version provided via command arguments
711 """
708 """
712
709
713 if self._checkcase:
710 if self._checkcase:
714 return self._normalize(path, isknown, ignoremissing)
711 return self._normalize(path, isknown, ignoremissing)
715 return path
712 return path
716
713
717 def clear(self):
714 def clear(self):
718 self._map.clear()
715 self._map.clear()
719 self._lastnormaltime = timestamp.zero()
716 self._lastnormaltime = timestamp.zero()
720 self._dirty = True
717 self._dirty = True
721
718
722 def rebuild(self, parent, allfiles, changedfiles=None):
719 def rebuild(self, parent, allfiles, changedfiles=None):
723 if changedfiles is None:
720 if changedfiles is None:
724 # Rebuild entire dirstate
721 # Rebuild entire dirstate
725 to_lookup = allfiles
722 to_lookup = allfiles
726 to_drop = []
723 to_drop = []
727 lastnormaltime = self._lastnormaltime
724 lastnormaltime = self._lastnormaltime
728 self.clear()
725 self.clear()
729 self._lastnormaltime = lastnormaltime
726 self._lastnormaltime = lastnormaltime
730 elif len(changedfiles) < 10:
727 elif len(changedfiles) < 10:
731 # Avoid turning allfiles into a set, which can be expensive if it's
728 # Avoid turning allfiles into a set, which can be expensive if it's
732 # large.
729 # large.
733 to_lookup = []
730 to_lookup = []
734 to_drop = []
731 to_drop = []
735 for f in changedfiles:
732 for f in changedfiles:
736 if f in allfiles:
733 if f in allfiles:
737 to_lookup.append(f)
734 to_lookup.append(f)
738 else:
735 else:
739 to_drop.append(f)
736 to_drop.append(f)
740 else:
737 else:
741 changedfilesset = set(changedfiles)
738 changedfilesset = set(changedfiles)
742 to_lookup = changedfilesset & set(allfiles)
739 to_lookup = changedfilesset & set(allfiles)
743 to_drop = changedfilesset - to_lookup
740 to_drop = changedfilesset - to_lookup
744
741
745 if self._origpl is None:
742 if self._origpl is None:
746 self._origpl = self._pl
743 self._origpl = self._pl
747 self._map.setparents(parent, self._nodeconstants.nullid)
744 self._map.setparents(parent, self._nodeconstants.nullid)
748
745
749 for f in to_lookup:
746 for f in to_lookup:
750
747
751 if self.in_merge:
748 if self.in_merge:
752 self.set_tracked(f)
749 self.set_tracked(f)
753 else:
750 else:
754 self._map.reset_state(
751 self._map.reset_state(
755 f,
752 f,
756 wc_tracked=True,
753 wc_tracked=True,
757 p1_tracked=True,
754 p1_tracked=True,
758 )
755 )
759 for f in to_drop:
756 for f in to_drop:
760 self._map.reset_state(f)
757 self._map.reset_state(f)
761
758
762 self._dirty = True
759 self._dirty = True
763
760
764 def identity(self):
761 def identity(self):
765 """Return identity of dirstate itself to detect changing in storage
762 """Return identity of dirstate itself to detect changing in storage
766
763
767 If identity of previous dirstate is equal to this, writing
764 If identity of previous dirstate is equal to this, writing
768 changes based on the former dirstate out can keep consistency.
765 changes based on the former dirstate out can keep consistency.
769 """
766 """
770 return self._map.identity
767 return self._map.identity
771
768
772 def write(self, tr):
769 def write(self, tr):
773 if not self._dirty:
770 if not self._dirty:
774 return
771 return
775
772
776 filename = self._filename
773 filename = self._filename
777 if tr:
774 if tr:
778 # 'dirstate.write()' is not only for writing in-memory
775 # 'dirstate.write()' is not only for writing in-memory
779 # changes out, but also for dropping ambiguous timestamp.
776 # changes out, but also for dropping ambiguous timestamp.
780 # delayed writing re-raise "ambiguous timestamp issue".
777 # delayed writing re-raise "ambiguous timestamp issue".
781 # See also the wiki page below for detail:
778 # See also the wiki page below for detail:
782 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
779 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
783
780
784 # record when mtime start to be ambiguous
781 # record when mtime start to be ambiguous
785 now = timestamp.get_fs_now(self._opener)
782 now = timestamp.get_fs_now(self._opener)
786
783
787 # delay writing in-memory changes out
784 # delay writing in-memory changes out
788 tr.addfilegenerator(
785 tr.addfilegenerator(
789 b'dirstate',
786 b'dirstate',
790 (self._filename,),
787 (self._filename,),
791 lambda f: self._writedirstate(tr, f, now=now),
788 lambda f: self._writedirstate(tr, f, now=now),
792 location=b'plain',
789 location=b'plain',
793 )
790 )
794 return
791 return
795
792
796 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
793 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
797 self._writedirstate(tr, st)
794 self._writedirstate(tr, st)
798
795
799 def addparentchangecallback(self, category, callback):
796 def addparentchangecallback(self, category, callback):
800 """add a callback to be called when the wd parents are changed
797 """add a callback to be called when the wd parents are changed
801
798
802 Callback will be called with the following arguments:
799 Callback will be called with the following arguments:
803 dirstate, (oldp1, oldp2), (newp1, newp2)
800 dirstate, (oldp1, oldp2), (newp1, newp2)
804
801
805 Category is a unique identifier to allow overwriting an old callback
802 Category is a unique identifier to allow overwriting an old callback
806 with a newer callback.
803 with a newer callback.
807 """
804 """
808 self._plchangecallbacks[category] = callback
805 self._plchangecallbacks[category] = callback
809
806
810 def _writedirstate(self, tr, st, now=None):
807 def _writedirstate(self, tr, st, now=None):
811 # notify callbacks about parents change
808 # notify callbacks about parents change
812 if self._origpl is not None and self._origpl != self._pl:
809 if self._origpl is not None and self._origpl != self._pl:
813 for c, callback in sorted(
810 for c, callback in sorted(
814 pycompat.iteritems(self._plchangecallbacks)
811 pycompat.iteritems(self._plchangecallbacks)
815 ):
812 ):
816 callback(self, self._origpl, self._pl)
813 callback(self, self._origpl, self._pl)
817 self._origpl = None
814 self._origpl = None
818
815
819 if now is None:
816 if now is None:
820 # use the modification time of the newly created temporary file as the
817 # use the modification time of the newly created temporary file as the
821 # filesystem's notion of 'now'
818 # filesystem's notion of 'now'
822 now = timestamp.mtime_of(util.fstat(st))
819 now = timestamp.mtime_of(util.fstat(st))
823
820
824 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
821 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
825 # timestamp of each entries in dirstate, because of 'now > mtime'
822 # timestamp of each entries in dirstate, because of 'now > mtime'
826 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
823 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
827 if delaywrite > 0:
824 if delaywrite > 0:
828 # do we have any files to delay for?
825 # do we have any files to delay for?
829 for f, e in pycompat.iteritems(self._map):
826 for f, e in pycompat.iteritems(self._map):
830 if e.need_delay(now):
827 if e.need_delay(now):
831 import time # to avoid useless import
828 import time # to avoid useless import
832
829
833 # rather than sleep n seconds, sleep until the next
830 # rather than sleep n seconds, sleep until the next
834 # multiple of n seconds
831 # multiple of n seconds
835 clock = time.time()
832 clock = time.time()
836 start = int(clock) - (int(clock) % delaywrite)
833 start = int(clock) - (int(clock) % delaywrite)
837 end = start + delaywrite
834 end = start + delaywrite
838 time.sleep(end - clock)
835 time.sleep(end - clock)
839 # trust our estimate that the end is near now
836 # trust our estimate that the end is near now
840 now = timestamp.timestamp((end, 0))
837 now = timestamp.timestamp((end, 0))
841 break
838 break
842
839
843 self._map.write(tr, st, now)
840 self._map.write(tr, st, now)
844 self._lastnormaltime = timestamp.zero()
841 self._lastnormaltime = timestamp.zero()
845 self._dirty = False
842 self._dirty = False
846
843
847 def _dirignore(self, f):
844 def _dirignore(self, f):
848 if self._ignore(f):
845 if self._ignore(f):
849 return True
846 return True
850 for p in pathutil.finddirs(f):
847 for p in pathutil.finddirs(f):
851 if self._ignore(p):
848 if self._ignore(p):
852 return True
849 return True
853 return False
850 return False
854
851
855 def _ignorefiles(self):
852 def _ignorefiles(self):
856 files = []
853 files = []
857 if os.path.exists(self._join(b'.hgignore')):
854 if os.path.exists(self._join(b'.hgignore')):
858 files.append(self._join(b'.hgignore'))
855 files.append(self._join(b'.hgignore'))
859 for name, path in self._ui.configitems(b"ui"):
856 for name, path in self._ui.configitems(b"ui"):
860 if name == b'ignore' or name.startswith(b'ignore.'):
857 if name == b'ignore' or name.startswith(b'ignore.'):
861 # we need to use os.path.join here rather than self._join
858 # we need to use os.path.join here rather than self._join
862 # because path is arbitrary and user-specified
859 # because path is arbitrary and user-specified
863 files.append(os.path.join(self._rootdir, util.expandpath(path)))
860 files.append(os.path.join(self._rootdir, util.expandpath(path)))
864 return files
861 return files
865
862
866 def _ignorefileandline(self, f):
863 def _ignorefileandline(self, f):
867 files = collections.deque(self._ignorefiles())
864 files = collections.deque(self._ignorefiles())
868 visited = set()
865 visited = set()
869 while files:
866 while files:
870 i = files.popleft()
867 i = files.popleft()
871 patterns = matchmod.readpatternfile(
868 patterns = matchmod.readpatternfile(
872 i, self._ui.warn, sourceinfo=True
869 i, self._ui.warn, sourceinfo=True
873 )
870 )
874 for pattern, lineno, line in patterns:
871 for pattern, lineno, line in patterns:
875 kind, p = matchmod._patsplit(pattern, b'glob')
872 kind, p = matchmod._patsplit(pattern, b'glob')
876 if kind == b"subinclude":
873 if kind == b"subinclude":
877 if p not in visited:
874 if p not in visited:
878 files.append(p)
875 files.append(p)
879 continue
876 continue
880 m = matchmod.match(
877 m = matchmod.match(
881 self._root, b'', [], [pattern], warn=self._ui.warn
878 self._root, b'', [], [pattern], warn=self._ui.warn
882 )
879 )
883 if m(f):
880 if m(f):
884 return (i, lineno, line)
881 return (i, lineno, line)
885 visited.add(i)
882 visited.add(i)
886 return (None, -1, b"")
883 return (None, -1, b"")
887
884
888 def _walkexplicit(self, match, subrepos):
885 def _walkexplicit(self, match, subrepos):
889 """Get stat data about the files explicitly specified by match.
886 """Get stat data about the files explicitly specified by match.
890
887
891 Return a triple (results, dirsfound, dirsnotfound).
888 Return a triple (results, dirsfound, dirsnotfound).
892 - results is a mapping from filename to stat result. It also contains
889 - results is a mapping from filename to stat result. It also contains
893 listings mapping subrepos and .hg to None.
890 listings mapping subrepos and .hg to None.
894 - dirsfound is a list of files found to be directories.
891 - dirsfound is a list of files found to be directories.
895 - dirsnotfound is a list of files that the dirstate thinks are
892 - dirsnotfound is a list of files that the dirstate thinks are
896 directories and that were not found."""
893 directories and that were not found."""
897
894
898 def badtype(mode):
895 def badtype(mode):
899 kind = _(b'unknown')
896 kind = _(b'unknown')
900 if stat.S_ISCHR(mode):
897 if stat.S_ISCHR(mode):
901 kind = _(b'character device')
898 kind = _(b'character device')
902 elif stat.S_ISBLK(mode):
899 elif stat.S_ISBLK(mode):
903 kind = _(b'block device')
900 kind = _(b'block device')
904 elif stat.S_ISFIFO(mode):
901 elif stat.S_ISFIFO(mode):
905 kind = _(b'fifo')
902 kind = _(b'fifo')
906 elif stat.S_ISSOCK(mode):
903 elif stat.S_ISSOCK(mode):
907 kind = _(b'socket')
904 kind = _(b'socket')
908 elif stat.S_ISDIR(mode):
905 elif stat.S_ISDIR(mode):
909 kind = _(b'directory')
906 kind = _(b'directory')
910 return _(b'unsupported file type (type is %s)') % kind
907 return _(b'unsupported file type (type is %s)') % kind
911
908
912 badfn = match.bad
909 badfn = match.bad
913 dmap = self._map
910 dmap = self._map
914 lstat = os.lstat
911 lstat = os.lstat
915 getkind = stat.S_IFMT
912 getkind = stat.S_IFMT
916 dirkind = stat.S_IFDIR
913 dirkind = stat.S_IFDIR
917 regkind = stat.S_IFREG
914 regkind = stat.S_IFREG
918 lnkkind = stat.S_IFLNK
915 lnkkind = stat.S_IFLNK
919 join = self._join
916 join = self._join
920 dirsfound = []
917 dirsfound = []
921 foundadd = dirsfound.append
918 foundadd = dirsfound.append
922 dirsnotfound = []
919 dirsnotfound = []
923 notfoundadd = dirsnotfound.append
920 notfoundadd = dirsnotfound.append
924
921
925 if not match.isexact() and self._checkcase:
922 if not match.isexact() and self._checkcase:
926 normalize = self._normalize
923 normalize = self._normalize
927 else:
924 else:
928 normalize = None
925 normalize = None
929
926
930 files = sorted(match.files())
927 files = sorted(match.files())
931 subrepos.sort()
928 subrepos.sort()
932 i, j = 0, 0
929 i, j = 0, 0
933 while i < len(files) and j < len(subrepos):
930 while i < len(files) and j < len(subrepos):
934 subpath = subrepos[j] + b"/"
931 subpath = subrepos[j] + b"/"
935 if files[i] < subpath:
932 if files[i] < subpath:
936 i += 1
933 i += 1
937 continue
934 continue
938 while i < len(files) and files[i].startswith(subpath):
935 while i < len(files) and files[i].startswith(subpath):
939 del files[i]
936 del files[i]
940 j += 1
937 j += 1
941
938
942 if not files or b'' in files:
939 if not files or b'' in files:
943 files = [b'']
940 files = [b'']
944 # constructing the foldmap is expensive, so don't do it for the
941 # constructing the foldmap is expensive, so don't do it for the
945 # common case where files is ['']
942 # common case where files is ['']
946 normalize = None
943 normalize = None
947 results = dict.fromkeys(subrepos)
944 results = dict.fromkeys(subrepos)
948 results[b'.hg'] = None
945 results[b'.hg'] = None
949
946
950 for ff in files:
947 for ff in files:
951 if normalize:
948 if normalize:
952 nf = normalize(ff, False, True)
949 nf = normalize(ff, False, True)
953 else:
950 else:
954 nf = ff
951 nf = ff
955 if nf in results:
952 if nf in results:
956 continue
953 continue
957
954
958 try:
955 try:
959 st = lstat(join(nf))
956 st = lstat(join(nf))
960 kind = getkind(st.st_mode)
957 kind = getkind(st.st_mode)
961 if kind == dirkind:
958 if kind == dirkind:
962 if nf in dmap:
959 if nf in dmap:
963 # file replaced by dir on disk but still in dirstate
960 # file replaced by dir on disk but still in dirstate
964 results[nf] = None
961 results[nf] = None
965 foundadd((nf, ff))
962 foundadd((nf, ff))
966 elif kind == regkind or kind == lnkkind:
963 elif kind == regkind or kind == lnkkind:
967 results[nf] = st
964 results[nf] = st
968 else:
965 else:
969 badfn(ff, badtype(kind))
966 badfn(ff, badtype(kind))
970 if nf in dmap:
967 if nf in dmap:
971 results[nf] = None
968 results[nf] = None
972 except OSError as inst: # nf not found on disk - it is dirstate only
969 except OSError as inst: # nf not found on disk - it is dirstate only
973 if nf in dmap: # does it exactly match a missing file?
970 if nf in dmap: # does it exactly match a missing file?
974 results[nf] = None
971 results[nf] = None
975 else: # does it match a missing directory?
972 else: # does it match a missing directory?
976 if self._map.hasdir(nf):
973 if self._map.hasdir(nf):
977 notfoundadd(nf)
974 notfoundadd(nf)
978 else:
975 else:
979 badfn(ff, encoding.strtolocal(inst.strerror))
976 badfn(ff, encoding.strtolocal(inst.strerror))
980
977
981 # match.files() may contain explicitly-specified paths that shouldn't
978 # match.files() may contain explicitly-specified paths that shouldn't
982 # be taken; drop them from the list of files found. dirsfound/notfound
979 # be taken; drop them from the list of files found. dirsfound/notfound
983 # aren't filtered here because they will be tested later.
980 # aren't filtered here because they will be tested later.
984 if match.anypats():
981 if match.anypats():
985 for f in list(results):
982 for f in list(results):
986 if f == b'.hg' or f in subrepos:
983 if f == b'.hg' or f in subrepos:
987 # keep sentinel to disable further out-of-repo walks
984 # keep sentinel to disable further out-of-repo walks
988 continue
985 continue
989 if not match(f):
986 if not match(f):
990 del results[f]
987 del results[f]
991
988
992 # Case insensitive filesystems cannot rely on lstat() failing to detect
989 # Case insensitive filesystems cannot rely on lstat() failing to detect
993 # a case-only rename. Prune the stat object for any file that does not
990 # a case-only rename. Prune the stat object for any file that does not
994 # match the case in the filesystem, if there are multiple files that
991 # match the case in the filesystem, if there are multiple files that
995 # normalize to the same path.
992 # normalize to the same path.
996 if match.isexact() and self._checkcase:
993 if match.isexact() and self._checkcase:
997 normed = {}
994 normed = {}
998
995
999 for f, st in pycompat.iteritems(results):
996 for f, st in pycompat.iteritems(results):
1000 if st is None:
997 if st is None:
1001 continue
998 continue
1002
999
1003 nc = util.normcase(f)
1000 nc = util.normcase(f)
1004 paths = normed.get(nc)
1001 paths = normed.get(nc)
1005
1002
1006 if paths is None:
1003 if paths is None:
1007 paths = set()
1004 paths = set()
1008 normed[nc] = paths
1005 normed[nc] = paths
1009
1006
1010 paths.add(f)
1007 paths.add(f)
1011
1008
1012 for norm, paths in pycompat.iteritems(normed):
1009 for norm, paths in pycompat.iteritems(normed):
1013 if len(paths) > 1:
1010 if len(paths) > 1:
1014 for path in paths:
1011 for path in paths:
1015 folded = self._discoverpath(
1012 folded = self._discoverpath(
1016 path, norm, True, None, self._map.dirfoldmap
1013 path, norm, True, None, self._map.dirfoldmap
1017 )
1014 )
1018 if path != folded:
1015 if path != folded:
1019 results[path] = None
1016 results[path] = None
1020
1017
1021 return results, dirsfound, dirsnotfound
1018 return results, dirsfound, dirsnotfound
1022
1019
1023 def walk(self, match, subrepos, unknown, ignored, full=True):
1020 def walk(self, match, subrepos, unknown, ignored, full=True):
1024 """
1021 """
1025 Walk recursively through the directory tree, finding all files
1022 Walk recursively through the directory tree, finding all files
1026 matched by match.
1023 matched by match.
1027
1024
1028 If full is False, maybe skip some known-clean files.
1025 If full is False, maybe skip some known-clean files.
1029
1026
1030 Return a dict mapping filename to stat-like object (either
1027 Return a dict mapping filename to stat-like object (either
1031 mercurial.osutil.stat instance or return value of os.stat()).
1028 mercurial.osutil.stat instance or return value of os.stat()).
1032
1029
1033 """
1030 """
1034 # full is a flag that extensions that hook into walk can use -- this
1031 # full is a flag that extensions that hook into walk can use -- this
1035 # implementation doesn't use it at all. This satisfies the contract
1032 # implementation doesn't use it at all. This satisfies the contract
1036 # because we only guarantee a "maybe".
1033 # because we only guarantee a "maybe".
1037
1034
1038 if ignored:
1035 if ignored:
1039 ignore = util.never
1036 ignore = util.never
1040 dirignore = util.never
1037 dirignore = util.never
1041 elif unknown:
1038 elif unknown:
1042 ignore = self._ignore
1039 ignore = self._ignore
1043 dirignore = self._dirignore
1040 dirignore = self._dirignore
1044 else:
1041 else:
1045 # if not unknown and not ignored, drop dir recursion and step 2
1042 # if not unknown and not ignored, drop dir recursion and step 2
1046 ignore = util.always
1043 ignore = util.always
1047 dirignore = util.always
1044 dirignore = util.always
1048
1045
1049 matchfn = match.matchfn
1046 matchfn = match.matchfn
1050 matchalways = match.always()
1047 matchalways = match.always()
1051 matchtdir = match.traversedir
1048 matchtdir = match.traversedir
1052 dmap = self._map
1049 dmap = self._map
1053 listdir = util.listdir
1050 listdir = util.listdir
1054 lstat = os.lstat
1051 lstat = os.lstat
1055 dirkind = stat.S_IFDIR
1052 dirkind = stat.S_IFDIR
1056 regkind = stat.S_IFREG
1053 regkind = stat.S_IFREG
1057 lnkkind = stat.S_IFLNK
1054 lnkkind = stat.S_IFLNK
1058 join = self._join
1055 join = self._join
1059
1056
1060 exact = skipstep3 = False
1057 exact = skipstep3 = False
1061 if match.isexact(): # match.exact
1058 if match.isexact(): # match.exact
1062 exact = True
1059 exact = True
1063 dirignore = util.always # skip step 2
1060 dirignore = util.always # skip step 2
1064 elif match.prefix(): # match.match, no patterns
1061 elif match.prefix(): # match.match, no patterns
1065 skipstep3 = True
1062 skipstep3 = True
1066
1063
1067 if not exact and self._checkcase:
1064 if not exact and self._checkcase:
1068 normalize = self._normalize
1065 normalize = self._normalize
1069 normalizefile = self._normalizefile
1066 normalizefile = self._normalizefile
1070 skipstep3 = False
1067 skipstep3 = False
1071 else:
1068 else:
1072 normalize = self._normalize
1069 normalize = self._normalize
1073 normalizefile = None
1070 normalizefile = None
1074
1071
1075 # step 1: find all explicit files
1072 # step 1: find all explicit files
1076 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1073 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1077 if matchtdir:
1074 if matchtdir:
1078 for d in work:
1075 for d in work:
1079 matchtdir(d[0])
1076 matchtdir(d[0])
1080 for d in dirsnotfound:
1077 for d in dirsnotfound:
1081 matchtdir(d)
1078 matchtdir(d)
1082
1079
1083 skipstep3 = skipstep3 and not (work or dirsnotfound)
1080 skipstep3 = skipstep3 and not (work or dirsnotfound)
1084 work = [d for d in work if not dirignore(d[0])]
1081 work = [d for d in work if not dirignore(d[0])]
1085
1082
1086 # step 2: visit subdirectories
1083 # step 2: visit subdirectories
1087 def traverse(work, alreadynormed):
1084 def traverse(work, alreadynormed):
1088 wadd = work.append
1085 wadd = work.append
1089 while work:
1086 while work:
1090 tracing.counter('dirstate.walk work', len(work))
1087 tracing.counter('dirstate.walk work', len(work))
1091 nd = work.pop()
1088 nd = work.pop()
1092 visitentries = match.visitchildrenset(nd)
1089 visitentries = match.visitchildrenset(nd)
1093 if not visitentries:
1090 if not visitentries:
1094 continue
1091 continue
1095 if visitentries == b'this' or visitentries == b'all':
1092 if visitentries == b'this' or visitentries == b'all':
1096 visitentries = None
1093 visitentries = None
1097 skip = None
1094 skip = None
1098 if nd != b'':
1095 if nd != b'':
1099 skip = b'.hg'
1096 skip = b'.hg'
1100 try:
1097 try:
1101 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1098 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1102 entries = listdir(join(nd), stat=True, skip=skip)
1099 entries = listdir(join(nd), stat=True, skip=skip)
1103 except OSError as inst:
1100 except OSError as inst:
1104 if inst.errno in (errno.EACCES, errno.ENOENT):
1101 if inst.errno in (errno.EACCES, errno.ENOENT):
1105 match.bad(
1102 match.bad(
1106 self.pathto(nd), encoding.strtolocal(inst.strerror)
1103 self.pathto(nd), encoding.strtolocal(inst.strerror)
1107 )
1104 )
1108 continue
1105 continue
1109 raise
1106 raise
1110 for f, kind, st in entries:
1107 for f, kind, st in entries:
1111 # Some matchers may return files in the visitentries set,
1108 # Some matchers may return files in the visitentries set,
1112 # instead of 'this', if the matcher explicitly mentions them
1109 # instead of 'this', if the matcher explicitly mentions them
1113 # and is not an exactmatcher. This is acceptable; we do not
1110 # and is not an exactmatcher. This is acceptable; we do not
1114 # make any hard assumptions about file-or-directory below
1111 # make any hard assumptions about file-or-directory below
1115 # based on the presence of `f` in visitentries. If
1112 # based on the presence of `f` in visitentries. If
1116 # visitchildrenset returned a set, we can always skip the
1113 # visitchildrenset returned a set, we can always skip the
1117 # entries *not* in the set it provided regardless of whether
1114 # entries *not* in the set it provided regardless of whether
1118 # they're actually a file or a directory.
1115 # they're actually a file or a directory.
1119 if visitentries and f not in visitentries:
1116 if visitentries and f not in visitentries:
1120 continue
1117 continue
1121 if normalizefile:
1118 if normalizefile:
1122 # even though f might be a directory, we're only
1119 # even though f might be a directory, we're only
1123 # interested in comparing it to files currently in the
1120 # interested in comparing it to files currently in the
1124 # dmap -- therefore normalizefile is enough
1121 # dmap -- therefore normalizefile is enough
1125 nf = normalizefile(
1122 nf = normalizefile(
1126 nd and (nd + b"/" + f) or f, True, True
1123 nd and (nd + b"/" + f) or f, True, True
1127 )
1124 )
1128 else:
1125 else:
1129 nf = nd and (nd + b"/" + f) or f
1126 nf = nd and (nd + b"/" + f) or f
1130 if nf not in results:
1127 if nf not in results:
1131 if kind == dirkind:
1128 if kind == dirkind:
1132 if not ignore(nf):
1129 if not ignore(nf):
1133 if matchtdir:
1130 if matchtdir:
1134 matchtdir(nf)
1131 matchtdir(nf)
1135 wadd(nf)
1132 wadd(nf)
1136 if nf in dmap and (matchalways or matchfn(nf)):
1133 if nf in dmap and (matchalways or matchfn(nf)):
1137 results[nf] = None
1134 results[nf] = None
1138 elif kind == regkind or kind == lnkkind:
1135 elif kind == regkind or kind == lnkkind:
1139 if nf in dmap:
1136 if nf in dmap:
1140 if matchalways or matchfn(nf):
1137 if matchalways or matchfn(nf):
1141 results[nf] = st
1138 results[nf] = st
1142 elif (matchalways or matchfn(nf)) and not ignore(
1139 elif (matchalways or matchfn(nf)) and not ignore(
1143 nf
1140 nf
1144 ):
1141 ):
1145 # unknown file -- normalize if necessary
1142 # unknown file -- normalize if necessary
1146 if not alreadynormed:
1143 if not alreadynormed:
1147 nf = normalize(nf, False, True)
1144 nf = normalize(nf, False, True)
1148 results[nf] = st
1145 results[nf] = st
1149 elif nf in dmap and (matchalways or matchfn(nf)):
1146 elif nf in dmap and (matchalways or matchfn(nf)):
1150 results[nf] = None
1147 results[nf] = None
1151
1148
1152 for nd, d in work:
1149 for nd, d in work:
1153 # alreadynormed means that processwork doesn't have to do any
1150 # alreadynormed means that processwork doesn't have to do any
1154 # expensive directory normalization
1151 # expensive directory normalization
1155 alreadynormed = not normalize or nd == d
1152 alreadynormed = not normalize or nd == d
1156 traverse([d], alreadynormed)
1153 traverse([d], alreadynormed)
1157
1154
1158 for s in subrepos:
1155 for s in subrepos:
1159 del results[s]
1156 del results[s]
1160 del results[b'.hg']
1157 del results[b'.hg']
1161
1158
1162 # step 3: visit remaining files from dmap
1159 # step 3: visit remaining files from dmap
1163 if not skipstep3 and not exact:
1160 if not skipstep3 and not exact:
1164 # If a dmap file is not in results yet, it was either
1161 # If a dmap file is not in results yet, it was either
1165 # a) not matching matchfn b) ignored, c) missing, or d) under a
1162 # a) not matching matchfn b) ignored, c) missing, or d) under a
1166 # symlink directory.
1163 # symlink directory.
1167 if not results and matchalways:
1164 if not results and matchalways:
1168 visit = [f for f in dmap]
1165 visit = [f for f in dmap]
1169 else:
1166 else:
1170 visit = [f for f in dmap if f not in results and matchfn(f)]
1167 visit = [f for f in dmap if f not in results and matchfn(f)]
1171 visit.sort()
1168 visit.sort()
1172
1169
1173 if unknown:
1170 if unknown:
1174 # unknown == True means we walked all dirs under the roots
1171 # unknown == True means we walked all dirs under the roots
1175 # that wasn't ignored, and everything that matched was stat'ed
1172 # that wasn't ignored, and everything that matched was stat'ed
1176 # and is already in results.
1173 # and is already in results.
1177 # The rest must thus be ignored or under a symlink.
1174 # The rest must thus be ignored or under a symlink.
1178 audit_path = pathutil.pathauditor(self._root, cached=True)
1175 audit_path = pathutil.pathauditor(self._root, cached=True)
1179
1176
1180 for nf in iter(visit):
1177 for nf in iter(visit):
1181 # If a stat for the same file was already added with a
1178 # If a stat for the same file was already added with a
1182 # different case, don't add one for this, since that would
1179 # different case, don't add one for this, since that would
1183 # make it appear as if the file exists under both names
1180 # make it appear as if the file exists under both names
1184 # on disk.
1181 # on disk.
1185 if (
1182 if (
1186 normalizefile
1183 normalizefile
1187 and normalizefile(nf, True, True) in results
1184 and normalizefile(nf, True, True) in results
1188 ):
1185 ):
1189 results[nf] = None
1186 results[nf] = None
1190 # Report ignored items in the dmap as long as they are not
1187 # Report ignored items in the dmap as long as they are not
1191 # under a symlink directory.
1188 # under a symlink directory.
1192 elif audit_path.check(nf):
1189 elif audit_path.check(nf):
1193 try:
1190 try:
1194 results[nf] = lstat(join(nf))
1191 results[nf] = lstat(join(nf))
1195 # file was just ignored, no links, and exists
1192 # file was just ignored, no links, and exists
1196 except OSError:
1193 except OSError:
1197 # file doesn't exist
1194 # file doesn't exist
1198 results[nf] = None
1195 results[nf] = None
1199 else:
1196 else:
1200 # It's either missing or under a symlink directory
1197 # It's either missing or under a symlink directory
1201 # which we in this case report as missing
1198 # which we in this case report as missing
1202 results[nf] = None
1199 results[nf] = None
1203 else:
1200 else:
1204 # We may not have walked the full directory tree above,
1201 # We may not have walked the full directory tree above,
1205 # so stat and check everything we missed.
1202 # so stat and check everything we missed.
1206 iv = iter(visit)
1203 iv = iter(visit)
1207 for st in util.statfiles([join(i) for i in visit]):
1204 for st in util.statfiles([join(i) for i in visit]):
1208 results[next(iv)] = st
1205 results[next(iv)] = st
1209 return results
1206 return results
1210
1207
1211 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1208 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1212 # Force Rayon (Rust parallelism library) to respect the number of
1209 # Force Rayon (Rust parallelism library) to respect the number of
1213 # workers. This is a temporary workaround until Rust code knows
1210 # workers. This is a temporary workaround until Rust code knows
1214 # how to read the config file.
1211 # how to read the config file.
1215 numcpus = self._ui.configint(b"worker", b"numcpus")
1212 numcpus = self._ui.configint(b"worker", b"numcpus")
1216 if numcpus is not None:
1213 if numcpus is not None:
1217 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1214 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1218
1215
1219 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1216 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1220 if not workers_enabled:
1217 if not workers_enabled:
1221 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1218 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1222
1219
1223 (
1220 (
1224 lookup,
1221 lookup,
1225 modified,
1222 modified,
1226 added,
1223 added,
1227 removed,
1224 removed,
1228 deleted,
1225 deleted,
1229 clean,
1226 clean,
1230 ignored,
1227 ignored,
1231 unknown,
1228 unknown,
1232 warnings,
1229 warnings,
1233 bad,
1230 bad,
1234 traversed,
1231 traversed,
1235 dirty,
1232 dirty,
1236 ) = rustmod.status(
1233 ) = rustmod.status(
1237 self._map._map,
1234 self._map._map,
1238 matcher,
1235 matcher,
1239 self._rootdir,
1236 self._rootdir,
1240 self._ignorefiles(),
1237 self._ignorefiles(),
1241 self._checkexec,
1238 self._checkexec,
1242 self._lastnormaltime,
1239 self._lastnormaltime,
1243 bool(list_clean),
1240 bool(list_clean),
1244 bool(list_ignored),
1241 bool(list_ignored),
1245 bool(list_unknown),
1242 bool(list_unknown),
1246 bool(matcher.traversedir),
1243 bool(matcher.traversedir),
1247 )
1244 )
1248
1245
1249 self._dirty |= dirty
1246 self._dirty |= dirty
1250
1247
1251 if matcher.traversedir:
1248 if matcher.traversedir:
1252 for dir in traversed:
1249 for dir in traversed:
1253 matcher.traversedir(dir)
1250 matcher.traversedir(dir)
1254
1251
1255 if self._ui.warn:
1252 if self._ui.warn:
1256 for item in warnings:
1253 for item in warnings:
1257 if isinstance(item, tuple):
1254 if isinstance(item, tuple):
1258 file_path, syntax = item
1255 file_path, syntax = item
1259 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1256 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1260 file_path,
1257 file_path,
1261 syntax,
1258 syntax,
1262 )
1259 )
1263 self._ui.warn(msg)
1260 self._ui.warn(msg)
1264 else:
1261 else:
1265 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1262 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1266 self._ui.warn(
1263 self._ui.warn(
1267 msg
1264 msg
1268 % (
1265 % (
1269 pathutil.canonpath(
1266 pathutil.canonpath(
1270 self._rootdir, self._rootdir, item
1267 self._rootdir, self._rootdir, item
1271 ),
1268 ),
1272 b"No such file or directory",
1269 b"No such file or directory",
1273 )
1270 )
1274 )
1271 )
1275
1272
1276 for (fn, message) in bad:
1273 for (fn, message) in bad:
1277 matcher.bad(fn, encoding.strtolocal(message))
1274 matcher.bad(fn, encoding.strtolocal(message))
1278
1275
1279 status = scmutil.status(
1276 status = scmutil.status(
1280 modified=modified,
1277 modified=modified,
1281 added=added,
1278 added=added,
1282 removed=removed,
1279 removed=removed,
1283 deleted=deleted,
1280 deleted=deleted,
1284 unknown=unknown,
1281 unknown=unknown,
1285 ignored=ignored,
1282 ignored=ignored,
1286 clean=clean,
1283 clean=clean,
1287 )
1284 )
1288 return (lookup, status)
1285 return (lookup, status)
1289
1286
1290 def status(self, match, subrepos, ignored, clean, unknown):
1287 def status(self, match, subrepos, ignored, clean, unknown):
1291 """Determine the status of the working copy relative to the
1288 """Determine the status of the working copy relative to the
1292 dirstate and return a pair of (unsure, status), where status is of type
1289 dirstate and return a pair of (unsure, status), where status is of type
1293 scmutil.status and:
1290 scmutil.status and:
1294
1291
1295 unsure:
1292 unsure:
1296 files that might have been modified since the dirstate was
1293 files that might have been modified since the dirstate was
1297 written, but need to be read to be sure (size is the same
1294 written, but need to be read to be sure (size is the same
1298 but mtime differs)
1295 but mtime differs)
1299 status.modified:
1296 status.modified:
1300 files that have definitely been modified since the dirstate
1297 files that have definitely been modified since the dirstate
1301 was written (different size or mode)
1298 was written (different size or mode)
1302 status.clean:
1299 status.clean:
1303 files that have definitely not been modified since the
1300 files that have definitely not been modified since the
1304 dirstate was written
1301 dirstate was written
1305 """
1302 """
1306 listignored, listclean, listunknown = ignored, clean, unknown
1303 listignored, listclean, listunknown = ignored, clean, unknown
1307 lookup, modified, added, unknown, ignored = [], [], [], [], []
1304 lookup, modified, added, unknown, ignored = [], [], [], [], []
1308 removed, deleted, clean = [], [], []
1305 removed, deleted, clean = [], [], []
1309
1306
1310 dmap = self._map
1307 dmap = self._map
1311 dmap.preload()
1308 dmap.preload()
1312
1309
1313 use_rust = True
1310 use_rust = True
1314
1311
1315 allowed_matchers = (
1312 allowed_matchers = (
1316 matchmod.alwaysmatcher,
1313 matchmod.alwaysmatcher,
1317 matchmod.exactmatcher,
1314 matchmod.exactmatcher,
1318 matchmod.includematcher,
1315 matchmod.includematcher,
1319 )
1316 )
1320
1317
1321 if rustmod is None:
1318 if rustmod is None:
1322 use_rust = False
1319 use_rust = False
1323 elif self._checkcase:
1320 elif self._checkcase:
1324 # Case-insensitive filesystems are not handled yet
1321 # Case-insensitive filesystems are not handled yet
1325 use_rust = False
1322 use_rust = False
1326 elif subrepos:
1323 elif subrepos:
1327 use_rust = False
1324 use_rust = False
1328 elif sparse.enabled:
1325 elif sparse.enabled:
1329 use_rust = False
1326 use_rust = False
1330 elif not isinstance(match, allowed_matchers):
1327 elif not isinstance(match, allowed_matchers):
1331 # Some matchers have yet to be implemented
1328 # Some matchers have yet to be implemented
1332 use_rust = False
1329 use_rust = False
1333
1330
1334 if use_rust:
1331 if use_rust:
1335 try:
1332 try:
1336 return self._rust_status(
1333 return self._rust_status(
1337 match, listclean, listignored, listunknown
1334 match, listclean, listignored, listunknown
1338 )
1335 )
1339 except rustmod.FallbackError:
1336 except rustmod.FallbackError:
1340 pass
1337 pass
1341
1338
1342 def noop(f):
1339 def noop(f):
1343 pass
1340 pass
1344
1341
1345 dcontains = dmap.__contains__
1342 dcontains = dmap.__contains__
1346 dget = dmap.__getitem__
1343 dget = dmap.__getitem__
1347 ladd = lookup.append # aka "unsure"
1344 ladd = lookup.append # aka "unsure"
1348 madd = modified.append
1345 madd = modified.append
1349 aadd = added.append
1346 aadd = added.append
1350 uadd = unknown.append if listunknown else noop
1347 uadd = unknown.append if listunknown else noop
1351 iadd = ignored.append if listignored else noop
1348 iadd = ignored.append if listignored else noop
1352 radd = removed.append
1349 radd = removed.append
1353 dadd = deleted.append
1350 dadd = deleted.append
1354 cadd = clean.append if listclean else noop
1351 cadd = clean.append if listclean else noop
1355 mexact = match.exact
1352 mexact = match.exact
1356 dirignore = self._dirignore
1353 dirignore = self._dirignore
1357 checkexec = self._checkexec
1354 checkexec = self._checkexec
1358 checklink = self._checklink
1355 checklink = self._checklink
1359 copymap = self._map.copymap
1356 copymap = self._map.copymap
1360 lastnormaltime = self._lastnormaltime
1357 lastnormaltime = self._lastnormaltime
1361
1358
1362 # We need to do full walks when either
1359 # We need to do full walks when either
1363 # - we're listing all clean files, or
1360 # - we're listing all clean files, or
1364 # - match.traversedir does something, because match.traversedir should
1361 # - match.traversedir does something, because match.traversedir should
1365 # be called for every dir in the working dir
1362 # be called for every dir in the working dir
1366 full = listclean or match.traversedir is not None
1363 full = listclean or match.traversedir is not None
1367 for fn, st in pycompat.iteritems(
1364 for fn, st in pycompat.iteritems(
1368 self.walk(match, subrepos, listunknown, listignored, full=full)
1365 self.walk(match, subrepos, listunknown, listignored, full=full)
1369 ):
1366 ):
1370 if not dcontains(fn):
1367 if not dcontains(fn):
1371 if (listignored or mexact(fn)) and dirignore(fn):
1368 if (listignored or mexact(fn)) and dirignore(fn):
1372 if listignored:
1369 if listignored:
1373 iadd(fn)
1370 iadd(fn)
1374 else:
1371 else:
1375 uadd(fn)
1372 uadd(fn)
1376 continue
1373 continue
1377
1374
1378 t = dget(fn)
1375 t = dget(fn)
1379 mode = t.mode
1376 mode = t.mode
1380 size = t.size
1377 size = t.size
1381
1378
1382 if not st and t.tracked:
1379 if not st and t.tracked:
1383 dadd(fn)
1380 dadd(fn)
1384 elif t.p2_info:
1381 elif t.p2_info:
1385 madd(fn)
1382 madd(fn)
1386 elif t.added:
1383 elif t.added:
1387 aadd(fn)
1384 aadd(fn)
1388 elif t.removed:
1385 elif t.removed:
1389 radd(fn)
1386 radd(fn)
1390 elif t.tracked:
1387 elif t.tracked:
1391 if not checklink and t.has_fallback_symlink:
1388 if not checklink and t.has_fallback_symlink:
1392 # If the file system does not support symlink, the mode
1389 # If the file system does not support symlink, the mode
1393 # might not be correctly stored in the dirstate, so do not
1390 # might not be correctly stored in the dirstate, so do not
1394 # trust it.
1391 # trust it.
1395 ladd(fn)
1392 ladd(fn)
1396 elif not checkexec and t.has_fallback_exec:
1393 elif not checkexec and t.has_fallback_exec:
1397 # If the file system does not support exec bits, the mode
1394 # If the file system does not support exec bits, the mode
1398 # might not be correctly stored in the dirstate, so do not
1395 # might not be correctly stored in the dirstate, so do not
1399 # trust it.
1396 # trust it.
1400 ladd(fn)
1397 ladd(fn)
1401 elif (
1398 elif (
1402 size >= 0
1399 size >= 0
1403 and (
1400 and (
1404 (size != st.st_size and size != st.st_size & _rangemask)
1401 (size != st.st_size and size != st.st_size & _rangemask)
1405 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1402 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1406 )
1403 )
1407 or fn in copymap
1404 or fn in copymap
1408 ):
1405 ):
1409 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1406 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1410 # issue6456: Size returned may be longer due to
1407 # issue6456: Size returned may be longer due to
1411 # encryption on EXT-4 fscrypt, undecided.
1408 # encryption on EXT-4 fscrypt, undecided.
1412 ladd(fn)
1409 ladd(fn)
1413 else:
1410 else:
1414 madd(fn)
1411 madd(fn)
1415 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1412 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1416 ladd(fn)
1413 ladd(fn)
1417 elif timestamp.mtime_of(st) == lastnormaltime:
1414 elif timestamp.mtime_of(st) == lastnormaltime:
1418 # fn may have just been marked as normal and it may have
1415 # fn may have just been marked as normal and it may have
1419 # changed in the same second without changing its size.
1416 # changed in the same second without changing its size.
1420 # This can happen if we quickly do multiple commits.
1417 # This can happen if we quickly do multiple commits.
1421 # Force lookup, so we don't miss such a racy file change.
1418 # Force lookup, so we don't miss such a racy file change.
1422 ladd(fn)
1419 ladd(fn)
1423 elif listclean:
1420 elif listclean:
1424 cadd(fn)
1421 cadd(fn)
1425 status = scmutil.status(
1422 status = scmutil.status(
1426 modified, added, removed, deleted, unknown, ignored, clean
1423 modified, added, removed, deleted, unknown, ignored, clean
1427 )
1424 )
1428 return (lookup, status)
1425 return (lookup, status)
1429
1426
1430 def matches(self, match):
1427 def matches(self, match):
1431 """
1428 """
1432 return files in the dirstate (in whatever state) filtered by match
1429 return files in the dirstate (in whatever state) filtered by match
1433 """
1430 """
1434 dmap = self._map
1431 dmap = self._map
1435 if rustmod is not None:
1432 if rustmod is not None:
1436 dmap = self._map._map
1433 dmap = self._map._map
1437
1434
1438 if match.always():
1435 if match.always():
1439 return dmap.keys()
1436 return dmap.keys()
1440 files = match.files()
1437 files = match.files()
1441 if match.isexact():
1438 if match.isexact():
1442 # fast path -- filter the other way around, since typically files is
1439 # fast path -- filter the other way around, since typically files is
1443 # much smaller than dmap
1440 # much smaller than dmap
1444 return [f for f in files if f in dmap]
1441 return [f for f in files if f in dmap]
1445 if match.prefix() and all(fn in dmap for fn in files):
1442 if match.prefix() and all(fn in dmap for fn in files):
1446 # fast path -- all the values are known to be files, so just return
1443 # fast path -- all the values are known to be files, so just return
1447 # that
1444 # that
1448 return list(files)
1445 return list(files)
1449 return [f for f in dmap if match(f)]
1446 return [f for f in dmap if match(f)]
1450
1447
1451 def _actualfilename(self, tr):
1448 def _actualfilename(self, tr):
1452 if tr:
1449 if tr:
1453 return self._pendingfilename
1450 return self._pendingfilename
1454 else:
1451 else:
1455 return self._filename
1452 return self._filename
1456
1453
1457 def savebackup(self, tr, backupname):
1454 def savebackup(self, tr, backupname):
1458 '''Save current dirstate into backup file'''
1455 '''Save current dirstate into backup file'''
1459 filename = self._actualfilename(tr)
1456 filename = self._actualfilename(tr)
1460 assert backupname != filename
1457 assert backupname != filename
1461
1458
1462 # use '_writedirstate' instead of 'write' to write changes certainly,
1459 # use '_writedirstate' instead of 'write' to write changes certainly,
1463 # because the latter omits writing out if transaction is running.
1460 # because the latter omits writing out if transaction is running.
1464 # output file will be used to create backup of dirstate at this point.
1461 # output file will be used to create backup of dirstate at this point.
1465 if self._dirty or not self._opener.exists(filename):
1462 if self._dirty or not self._opener.exists(filename):
1466 self._writedirstate(
1463 self._writedirstate(
1467 tr,
1464 tr,
1468 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1465 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1469 )
1466 )
1470
1467
1471 if tr:
1468 if tr:
1472 # ensure that subsequent tr.writepending returns True for
1469 # ensure that subsequent tr.writepending returns True for
1473 # changes written out above, even if dirstate is never
1470 # changes written out above, even if dirstate is never
1474 # changed after this
1471 # changed after this
1475 tr.addfilegenerator(
1472 tr.addfilegenerator(
1476 b'dirstate',
1473 b'dirstate',
1477 (self._filename,),
1474 (self._filename,),
1478 lambda f: self._writedirstate(tr, f),
1475 lambda f: self._writedirstate(tr, f),
1479 location=b'plain',
1476 location=b'plain',
1480 )
1477 )
1481
1478
1482 # ensure that pending file written above is unlinked at
1479 # ensure that pending file written above is unlinked at
1483 # failure, even if tr.writepending isn't invoked until the
1480 # failure, even if tr.writepending isn't invoked until the
1484 # end of this transaction
1481 # end of this transaction
1485 tr.registertmp(filename, location=b'plain')
1482 tr.registertmp(filename, location=b'plain')
1486
1483
1487 self._opener.tryunlink(backupname)
1484 self._opener.tryunlink(backupname)
1488 # hardlink backup is okay because _writedirstate is always called
1485 # hardlink backup is okay because _writedirstate is always called
1489 # with an "atomictemp=True" file.
1486 # with an "atomictemp=True" file.
1490 util.copyfile(
1487 util.copyfile(
1491 self._opener.join(filename),
1488 self._opener.join(filename),
1492 self._opener.join(backupname),
1489 self._opener.join(backupname),
1493 hardlink=True,
1490 hardlink=True,
1494 )
1491 )
1495
1492
1496 def restorebackup(self, tr, backupname):
1493 def restorebackup(self, tr, backupname):
1497 '''Restore dirstate by backup file'''
1494 '''Restore dirstate by backup file'''
1498 # this "invalidate()" prevents "wlock.release()" from writing
1495 # this "invalidate()" prevents "wlock.release()" from writing
1499 # changes of dirstate out after restoring from backup file
1496 # changes of dirstate out after restoring from backup file
1500 self.invalidate()
1497 self.invalidate()
1501 filename = self._actualfilename(tr)
1498 filename = self._actualfilename(tr)
1502 o = self._opener
1499 o = self._opener
1503 if util.samefile(o.join(backupname), o.join(filename)):
1500 if util.samefile(o.join(backupname), o.join(filename)):
1504 o.unlink(backupname)
1501 o.unlink(backupname)
1505 else:
1502 else:
1506 o.rename(backupname, filename, checkambig=True)
1503 o.rename(backupname, filename, checkambig=True)
1507
1504
1508 def clearbackup(self, tr, backupname):
1505 def clearbackup(self, tr, backupname):
1509 '''Clear backup file'''
1506 '''Clear backup file'''
1510 self._opener.unlink(backupname)
1507 self._opener.unlink(backupname)
1511
1508
1512 def verify(self, m1, m2):
1509 def verify(self, m1, m2):
1513 """check the dirstate content again the parent manifest and yield errors"""
1510 """check the dirstate content again the parent manifest and yield errors"""
1514 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1511 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1515 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1512 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1516 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1513 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1517 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1514 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1518 for f, entry in self.items():
1515 for f, entry in self.items():
1519 state = entry.state
1516 state = entry.state
1520 if state in b"nr" and f not in m1:
1517 if state in b"nr" and f not in m1:
1521 yield (missing_from_p1, f, state)
1518 yield (missing_from_p1, f, state)
1522 if state in b"a" and f in m1:
1519 if state in b"a" and f in m1:
1523 yield (unexpected_in_p1, f, state)
1520 yield (unexpected_in_p1, f, state)
1524 if state in b"m" and f not in m1 and f not in m2:
1521 if state in b"m" and f not in m1 and f not in m2:
1525 yield (missing_from_ps, f, state)
1522 yield (missing_from_ps, f, state)
1526 for f in m1:
1523 for f in m1:
1527 state = self.get_entry(f).state
1524 state = self.get_entry(f).state
1528 if state not in b"nrm":
1525 if state not in b"nrm":
1529 yield (missing_from_ds, f, state)
1526 yield (missing_from_ds, f, state)
General Comments 0
You need to be logged in to leave comments. Login now