##// END OF EJS Templates
dirstate: support file tracked nowhere in `reset_state`...
marmoute -
r48812:938a7769 default
parent child Browse files
Show More
@@ -1,959 +1,962 b''
1 # dirstatemap.py
1 # dirstatemap.py
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 from __future__ import absolute_import
6 from __future__ import absolute_import
7
7
8 import errno
8 import errno
9
9
10 from .i18n import _
10 from .i18n import _
11
11
12 from . import (
12 from . import (
13 error,
13 error,
14 pathutil,
14 pathutil,
15 policy,
15 policy,
16 pycompat,
16 pycompat,
17 txnutil,
17 txnutil,
18 util,
18 util,
19 )
19 )
20
20
21 from .dirstateutils import (
21 from .dirstateutils import (
22 docket as docketmod,
22 docket as docketmod,
23 )
23 )
24
24
25 parsers = policy.importmod('parsers')
25 parsers = policy.importmod('parsers')
26 rustmod = policy.importrust('dirstate')
26 rustmod = policy.importrust('dirstate')
27
27
28 propertycache = util.propertycache
28 propertycache = util.propertycache
29
29
30 DirstateItem = parsers.DirstateItem
30 DirstateItem = parsers.DirstateItem
31
31
32 rangemask = 0x7FFFFFFF
32 rangemask = 0x7FFFFFFF
33
33
34
34
35 class dirstatemap(object):
35 class dirstatemap(object):
36 """Map encapsulating the dirstate's contents.
36 """Map encapsulating the dirstate's contents.
37
37
38 The dirstate contains the following state:
38 The dirstate contains the following state:
39
39
40 - `identity` is the identity of the dirstate file, which can be used to
40 - `identity` is the identity of the dirstate file, which can be used to
41 detect when changes have occurred to the dirstate file.
41 detect when changes have occurred to the dirstate file.
42
42
43 - `parents` is a pair containing the parents of the working copy. The
43 - `parents` is a pair containing the parents of the working copy. The
44 parents are updated by calling `setparents`.
44 parents are updated by calling `setparents`.
45
45
46 - the state map maps filenames to tuples of (state, mode, size, mtime),
46 - the state map maps filenames to tuples of (state, mode, size, mtime),
47 where state is a single character representing 'normal', 'added',
47 where state is a single character representing 'normal', 'added',
48 'removed', or 'merged'. It is read by treating the dirstate as a
48 'removed', or 'merged'. It is read by treating the dirstate as a
49 dict. File state is updated by calling various methods (see each
49 dict. File state is updated by calling various methods (see each
50 documentation for details):
50 documentation for details):
51
51
52 - `reset_state`,
52 - `reset_state`,
53 - `set_tracked`
53 - `set_tracked`
54 - `set_untracked`
54 - `set_untracked`
55 - `set_clean`
55 - `set_clean`
56 - `set_possibly_dirty`
56 - `set_possibly_dirty`
57
57
58 - `copymap` maps destination filenames to their source filename.
58 - `copymap` maps destination filenames to their source filename.
59
59
60 The dirstate also provides the following views onto the state:
60 The dirstate also provides the following views onto the state:
61
61
62 - `nonnormalset` is a set of the filenames that have state other
62 - `nonnormalset` is a set of the filenames that have state other
63 than 'normal', or are normal but have an mtime of -1 ('normallookup').
63 than 'normal', or are normal but have an mtime of -1 ('normallookup').
64
64
65 - `otherparentset` is a set of the filenames that are marked as coming
65 - `otherparentset` is a set of the filenames that are marked as coming
66 from the second parent when the dirstate is currently being merged.
66 from the second parent when the dirstate is currently being merged.
67
67
68 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
68 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
69 form that they appear as in the dirstate.
69 form that they appear as in the dirstate.
70
70
71 - `dirfoldmap` is a dict mapping normalized directory names to the
71 - `dirfoldmap` is a dict mapping normalized directory names to the
72 denormalized form that they appear as in the dirstate.
72 denormalized form that they appear as in the dirstate.
73 """
73 """
74
74
75 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
75 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
76 self._ui = ui
76 self._ui = ui
77 self._opener = opener
77 self._opener = opener
78 self._root = root
78 self._root = root
79 self._filename = b'dirstate'
79 self._filename = b'dirstate'
80 self._nodelen = 20
80 self._nodelen = 20
81 self._nodeconstants = nodeconstants
81 self._nodeconstants = nodeconstants
82 assert (
82 assert (
83 not use_dirstate_v2
83 not use_dirstate_v2
84 ), "should have detected unsupported requirement"
84 ), "should have detected unsupported requirement"
85
85
86 self._parents = None
86 self._parents = None
87 self._dirtyparents = False
87 self._dirtyparents = False
88
88
89 # for consistent view between _pl() and _read() invocations
89 # for consistent view between _pl() and _read() invocations
90 self._pendingmode = None
90 self._pendingmode = None
91
91
92 @propertycache
92 @propertycache
93 def _map(self):
93 def _map(self):
94 self._map = {}
94 self._map = {}
95 self.read()
95 self.read()
96 return self._map
96 return self._map
97
97
98 @propertycache
98 @propertycache
99 def copymap(self):
99 def copymap(self):
100 self.copymap = {}
100 self.copymap = {}
101 self._map
101 self._map
102 return self.copymap
102 return self.copymap
103
103
104 def clear(self):
104 def clear(self):
105 self._map.clear()
105 self._map.clear()
106 self.copymap.clear()
106 self.copymap.clear()
107 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
107 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
108 util.clearcachedproperty(self, b"_dirs")
108 util.clearcachedproperty(self, b"_dirs")
109 util.clearcachedproperty(self, b"_alldirs")
109 util.clearcachedproperty(self, b"_alldirs")
110 util.clearcachedproperty(self, b"filefoldmap")
110 util.clearcachedproperty(self, b"filefoldmap")
111 util.clearcachedproperty(self, b"dirfoldmap")
111 util.clearcachedproperty(self, b"dirfoldmap")
112 util.clearcachedproperty(self, b"nonnormalset")
112 util.clearcachedproperty(self, b"nonnormalset")
113 util.clearcachedproperty(self, b"otherparentset")
113 util.clearcachedproperty(self, b"otherparentset")
114
114
115 def items(self):
115 def items(self):
116 return pycompat.iteritems(self._map)
116 return pycompat.iteritems(self._map)
117
117
118 # forward for python2,3 compat
118 # forward for python2,3 compat
119 iteritems = items
119 iteritems = items
120
120
121 debug_iter = items
121 debug_iter = items
122
122
123 def __len__(self):
123 def __len__(self):
124 return len(self._map)
124 return len(self._map)
125
125
126 def __iter__(self):
126 def __iter__(self):
127 return iter(self._map)
127 return iter(self._map)
128
128
129 def get(self, key, default=None):
129 def get(self, key, default=None):
130 return self._map.get(key, default)
130 return self._map.get(key, default)
131
131
132 def __contains__(self, key):
132 def __contains__(self, key):
133 return key in self._map
133 return key in self._map
134
134
135 def __getitem__(self, key):
135 def __getitem__(self, key):
136 return self._map[key]
136 return self._map[key]
137
137
138 def keys(self):
138 def keys(self):
139 return self._map.keys()
139 return self._map.keys()
140
140
141 def preload(self):
141 def preload(self):
142 """Loads the underlying data, if it's not already loaded"""
142 """Loads the underlying data, if it's not already loaded"""
143 self._map
143 self._map
144
144
145 def _dirs_incr(self, filename, old_entry=None):
145 def _dirs_incr(self, filename, old_entry=None):
146 """incremente the dirstate counter if applicable"""
146 """incremente the dirstate counter if applicable"""
147 if (
147 if (
148 old_entry is None or old_entry.removed
148 old_entry is None or old_entry.removed
149 ) and "_dirs" in self.__dict__:
149 ) and "_dirs" in self.__dict__:
150 self._dirs.addpath(filename)
150 self._dirs.addpath(filename)
151 if old_entry is None and "_alldirs" in self.__dict__:
151 if old_entry is None and "_alldirs" in self.__dict__:
152 self._alldirs.addpath(filename)
152 self._alldirs.addpath(filename)
153
153
154 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
154 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
155 """decremente the dirstate counter if applicable"""
155 """decremente the dirstate counter if applicable"""
156 if old_entry is not None:
156 if old_entry is not None:
157 if "_dirs" in self.__dict__ and not old_entry.removed:
157 if "_dirs" in self.__dict__ and not old_entry.removed:
158 self._dirs.delpath(filename)
158 self._dirs.delpath(filename)
159 if "_alldirs" in self.__dict__ and not remove_variant:
159 if "_alldirs" in self.__dict__ and not remove_variant:
160 self._alldirs.delpath(filename)
160 self._alldirs.delpath(filename)
161 elif remove_variant and "_alldirs" in self.__dict__:
161 elif remove_variant and "_alldirs" in self.__dict__:
162 self._alldirs.addpath(filename)
162 self._alldirs.addpath(filename)
163 if "filefoldmap" in self.__dict__:
163 if "filefoldmap" in self.__dict__:
164 normed = util.normcase(filename)
164 normed = util.normcase(filename)
165 self.filefoldmap.pop(normed, None)
165 self.filefoldmap.pop(normed, None)
166
166
167 def set_possibly_dirty(self, filename):
167 def set_possibly_dirty(self, filename):
168 """record that the current state of the file on disk is unknown"""
168 """record that the current state of the file on disk is unknown"""
169 self[filename].set_possibly_dirty()
169 self[filename].set_possibly_dirty()
170
170
171 def set_clean(self, filename, mode, size, mtime):
171 def set_clean(self, filename, mode, size, mtime):
172 """mark a file as back to a clean state"""
172 """mark a file as back to a clean state"""
173 entry = self[filename]
173 entry = self[filename]
174 mtime = mtime & rangemask
174 mtime = mtime & rangemask
175 size = size & rangemask
175 size = size & rangemask
176 entry.set_clean(mode, size, mtime)
176 entry.set_clean(mode, size, mtime)
177 self.copymap.pop(filename, None)
177 self.copymap.pop(filename, None)
178 self.nonnormalset.discard(filename)
178 self.nonnormalset.discard(filename)
179
179
180 def reset_state(
180 def reset_state(
181 self,
181 self,
182 filename,
182 filename,
183 wc_tracked,
183 wc_tracked=False,
184 p1_tracked,
184 p1_tracked=False,
185 p2_tracked=False,
185 p2_tracked=False,
186 merged=False,
186 merged=False,
187 clean_p1=False,
187 clean_p1=False,
188 clean_p2=False,
188 clean_p2=False,
189 possibly_dirty=False,
189 possibly_dirty=False,
190 parentfiledata=None,
190 parentfiledata=None,
191 ):
191 ):
192 """Set a entry to a given state, diregarding all previous state
192 """Set a entry to a given state, diregarding all previous state
193
193
194 This is to be used by the part of the dirstate API dedicated to
194 This is to be used by the part of the dirstate API dedicated to
195 adjusting the dirstate after a update/merge.
195 adjusting the dirstate after a update/merge.
196
196
197 note: calling this might result to no entry existing at all if the
197 note: calling this might result to no entry existing at all if the
198 dirstate map does not see any point at having one for this file
198 dirstate map does not see any point at having one for this file
199 anymore.
199 anymore.
200 """
200 """
201 if merged and (clean_p1 or clean_p2):
201 if merged and (clean_p1 or clean_p2):
202 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
202 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
203 raise error.ProgrammingError(msg)
203 raise error.ProgrammingError(msg)
204 # copy information are now outdated
204 # copy information are now outdated
205 # (maybe new information should be in directly passed to this function)
205 # (maybe new information should be in directly passed to this function)
206 self.copymap.pop(filename, None)
206 self.copymap.pop(filename, None)
207
207
208 if not (p1_tracked or p2_tracked or wc_tracked):
208 if not (p1_tracked or p2_tracked or wc_tracked):
209 self.dropfile(filename)
209 old_entry = self._map.pop(filename, None)
210 self._dirs_decr(filename, old_entry=old_entry)
211 self.nonnormalset.discard(filename)
212 self.copymap.pop(filename, None)
210 return
213 return
211 elif merged:
214 elif merged:
212 # XXX might be merged and removed ?
215 # XXX might be merged and removed ?
213 entry = self.get(filename)
216 entry = self.get(filename)
214 if entry is None or not entry.tracked:
217 if entry is None or not entry.tracked:
215 # XXX mostly replicate dirstate.other parent. We should get
218 # XXX mostly replicate dirstate.other parent. We should get
216 # the higher layer to pass us more reliable data where `merged`
219 # the higher layer to pass us more reliable data where `merged`
217 # actually mean merged. Dropping this clause will show failure
220 # actually mean merged. Dropping this clause will show failure
218 # in `test-graft.t`
221 # in `test-graft.t`
219 merged = False
222 merged = False
220 clean_p2 = True
223 clean_p2 = True
221 elif not (p1_tracked or p2_tracked) and wc_tracked:
224 elif not (p1_tracked or p2_tracked) and wc_tracked:
222 pass # file is added, nothing special to adjust
225 pass # file is added, nothing special to adjust
223 elif (p1_tracked or p2_tracked) and not wc_tracked:
226 elif (p1_tracked or p2_tracked) and not wc_tracked:
224 pass
227 pass
225 elif clean_p2 and wc_tracked:
228 elif clean_p2 and wc_tracked:
226 if p1_tracked or self.get(filename) is not None:
229 if p1_tracked or self.get(filename) is not None:
227 # XXX the `self.get` call is catching some case in
230 # XXX the `self.get` call is catching some case in
228 # `test-merge-remove.t` where the file is tracked in p1, the
231 # `test-merge-remove.t` where the file is tracked in p1, the
229 # p1_tracked argument is False.
232 # p1_tracked argument is False.
230 #
233 #
231 # In addition, this seems to be a case where the file is marked
234 # In addition, this seems to be a case where the file is marked
232 # as merged without actually being the result of a merge
235 # as merged without actually being the result of a merge
233 # action. So thing are not ideal here.
236 # action. So thing are not ideal here.
234 merged = True
237 merged = True
235 clean_p2 = False
238 clean_p2 = False
236 elif not p1_tracked and p2_tracked and wc_tracked:
239 elif not p1_tracked and p2_tracked and wc_tracked:
237 clean_p2 = True
240 clean_p2 = True
238 elif possibly_dirty:
241 elif possibly_dirty:
239 pass
242 pass
240 elif wc_tracked:
243 elif wc_tracked:
241 # this is a "normal" file
244 # this is a "normal" file
242 if parentfiledata is None:
245 if parentfiledata is None:
243 msg = b'failed to pass parentfiledata for a normal file: %s'
246 msg = b'failed to pass parentfiledata for a normal file: %s'
244 msg %= filename
247 msg %= filename
245 raise error.ProgrammingError(msg)
248 raise error.ProgrammingError(msg)
246 else:
249 else:
247 assert False, 'unreachable'
250 assert False, 'unreachable'
248
251
249 old_entry = self._map.get(filename)
252 old_entry = self._map.get(filename)
250 self._dirs_incr(filename, old_entry)
253 self._dirs_incr(filename, old_entry)
251 entry = DirstateItem(
254 entry = DirstateItem(
252 wc_tracked=wc_tracked,
255 wc_tracked=wc_tracked,
253 p1_tracked=p1_tracked,
256 p1_tracked=p1_tracked,
254 p2_tracked=p2_tracked,
257 p2_tracked=p2_tracked,
255 merged=merged,
258 merged=merged,
256 clean_p1=clean_p1,
259 clean_p1=clean_p1,
257 clean_p2=clean_p2,
260 clean_p2=clean_p2,
258 possibly_dirty=possibly_dirty,
261 possibly_dirty=possibly_dirty,
259 parentfiledata=parentfiledata,
262 parentfiledata=parentfiledata,
260 )
263 )
261 if entry.dm_nonnormal:
264 if entry.dm_nonnormal:
262 self.nonnormalset.add(filename)
265 self.nonnormalset.add(filename)
263 else:
266 else:
264 self.nonnormalset.discard(filename)
267 self.nonnormalset.discard(filename)
265 if entry.dm_otherparent:
268 if entry.dm_otherparent:
266 self.otherparentset.add(filename)
269 self.otherparentset.add(filename)
267 else:
270 else:
268 self.otherparentset.discard(filename)
271 self.otherparentset.discard(filename)
269 self._map[filename] = entry
272 self._map[filename] = entry
270
273
271 def set_tracked(self, filename):
274 def set_tracked(self, filename):
272 new = False
275 new = False
273 entry = self.get(filename)
276 entry = self.get(filename)
274 if entry is None:
277 if entry is None:
275 self._dirs_incr(filename)
278 self._dirs_incr(filename)
276 entry = DirstateItem(
279 entry = DirstateItem(
277 p1_tracked=False,
280 p1_tracked=False,
278 p2_tracked=False,
281 p2_tracked=False,
279 wc_tracked=True,
282 wc_tracked=True,
280 merged=False,
283 merged=False,
281 clean_p1=False,
284 clean_p1=False,
282 clean_p2=False,
285 clean_p2=False,
283 possibly_dirty=False,
286 possibly_dirty=False,
284 parentfiledata=None,
287 parentfiledata=None,
285 )
288 )
286 self._map[filename] = entry
289 self._map[filename] = entry
287 if entry.dm_nonnormal:
290 if entry.dm_nonnormal:
288 self.nonnormalset.add(filename)
291 self.nonnormalset.add(filename)
289 new = True
292 new = True
290 elif not entry.tracked:
293 elif not entry.tracked:
291 self._dirs_incr(filename, entry)
294 self._dirs_incr(filename, entry)
292 entry.set_tracked()
295 entry.set_tracked()
293 new = True
296 new = True
294 else:
297 else:
295 # XXX This is probably overkill for more case, but we need this to
298 # XXX This is probably overkill for more case, but we need this to
296 # fully replace the `normallookup` call with `set_tracked` one.
299 # fully replace the `normallookup` call with `set_tracked` one.
297 # Consider smoothing this in the future.
300 # Consider smoothing this in the future.
298 self.set_possibly_dirty(filename)
301 self.set_possibly_dirty(filename)
299 return new
302 return new
300
303
301 def set_untracked(self, f):
304 def set_untracked(self, f):
302 """Mark a file as no longer tracked in the dirstate map"""
305 """Mark a file as no longer tracked in the dirstate map"""
303 entry = self.get(f)
306 entry = self.get(f)
304 if entry is None:
307 if entry is None:
305 return False
308 return False
306 else:
309 else:
307 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
310 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
308 if not entry.merged:
311 if not entry.merged:
309 self.copymap.pop(f, None)
312 self.copymap.pop(f, None)
310 if entry.added:
313 if entry.added:
311 self.nonnormalset.discard(f)
314 self.nonnormalset.discard(f)
312 self._map.pop(f, None)
315 self._map.pop(f, None)
313 else:
316 else:
314 self.nonnormalset.add(f)
317 self.nonnormalset.add(f)
315 if entry.from_p2:
318 if entry.from_p2:
316 self.otherparentset.add(f)
319 self.otherparentset.add(f)
317 entry.set_untracked()
320 entry.set_untracked()
318 return True
321 return True
319
322
320 def dropfile(self, f):
323 def dropfile(self, f):
321 """
324 """
322 Remove a file from the dirstate. Returns True if the file was
325 Remove a file from the dirstate. Returns True if the file was
323 previously recorded.
326 previously recorded.
324 """
327 """
325 old_entry = self._map.pop(f, None)
328 old_entry = self._map.pop(f, None)
326 self._dirs_decr(f, old_entry=old_entry)
329 self._dirs_decr(f, old_entry=old_entry)
327 self.nonnormalset.discard(f)
330 self.nonnormalset.discard(f)
328 self.copymap.pop(f, None)
331 self.copymap.pop(f, None)
329 return old_entry is not None
332 return old_entry is not None
330
333
331 def clearambiguoustimes(self, files, now):
334 def clearambiguoustimes(self, files, now):
332 for f in files:
335 for f in files:
333 e = self.get(f)
336 e = self.get(f)
334 if e is not None and e.need_delay(now):
337 if e is not None and e.need_delay(now):
335 e.set_possibly_dirty()
338 e.set_possibly_dirty()
336 self.nonnormalset.add(f)
339 self.nonnormalset.add(f)
337
340
338 def nonnormalentries(self):
341 def nonnormalentries(self):
339 '''Compute the nonnormal dirstate entries from the dmap'''
342 '''Compute the nonnormal dirstate entries from the dmap'''
340 try:
343 try:
341 return parsers.nonnormalotherparententries(self._map)
344 return parsers.nonnormalotherparententries(self._map)
342 except AttributeError:
345 except AttributeError:
343 nonnorm = set()
346 nonnorm = set()
344 otherparent = set()
347 otherparent = set()
345 for fname, e in pycompat.iteritems(self._map):
348 for fname, e in pycompat.iteritems(self._map):
346 if e.dm_nonnormal:
349 if e.dm_nonnormal:
347 nonnorm.add(fname)
350 nonnorm.add(fname)
348 if e.from_p2:
351 if e.from_p2:
349 otherparent.add(fname)
352 otherparent.add(fname)
350 return nonnorm, otherparent
353 return nonnorm, otherparent
351
354
352 @propertycache
355 @propertycache
353 def filefoldmap(self):
356 def filefoldmap(self):
354 """Returns a dictionary mapping normalized case paths to their
357 """Returns a dictionary mapping normalized case paths to their
355 non-normalized versions.
358 non-normalized versions.
356 """
359 """
357 try:
360 try:
358 makefilefoldmap = parsers.make_file_foldmap
361 makefilefoldmap = parsers.make_file_foldmap
359 except AttributeError:
362 except AttributeError:
360 pass
363 pass
361 else:
364 else:
362 return makefilefoldmap(
365 return makefilefoldmap(
363 self._map, util.normcasespec, util.normcasefallback
366 self._map, util.normcasespec, util.normcasefallback
364 )
367 )
365
368
366 f = {}
369 f = {}
367 normcase = util.normcase
370 normcase = util.normcase
368 for name, s in pycompat.iteritems(self._map):
371 for name, s in pycompat.iteritems(self._map):
369 if not s.removed:
372 if not s.removed:
370 f[normcase(name)] = name
373 f[normcase(name)] = name
371 f[b'.'] = b'.' # prevents useless util.fspath() invocation
374 f[b'.'] = b'.' # prevents useless util.fspath() invocation
372 return f
375 return f
373
376
374 def hastrackeddir(self, d):
377 def hastrackeddir(self, d):
375 """
378 """
376 Returns True if the dirstate contains a tracked (not removed) file
379 Returns True if the dirstate contains a tracked (not removed) file
377 in this directory.
380 in this directory.
378 """
381 """
379 return d in self._dirs
382 return d in self._dirs
380
383
381 def hasdir(self, d):
384 def hasdir(self, d):
382 """
385 """
383 Returns True if the dirstate contains a file (tracked or removed)
386 Returns True if the dirstate contains a file (tracked or removed)
384 in this directory.
387 in this directory.
385 """
388 """
386 return d in self._alldirs
389 return d in self._alldirs
387
390
388 @propertycache
391 @propertycache
389 def _dirs(self):
392 def _dirs(self):
390 return pathutil.dirs(self._map, only_tracked=True)
393 return pathutil.dirs(self._map, only_tracked=True)
391
394
392 @propertycache
395 @propertycache
393 def _alldirs(self):
396 def _alldirs(self):
394 return pathutil.dirs(self._map)
397 return pathutil.dirs(self._map)
395
398
396 def _opendirstatefile(self):
399 def _opendirstatefile(self):
397 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
400 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
398 if self._pendingmode is not None and self._pendingmode != mode:
401 if self._pendingmode is not None and self._pendingmode != mode:
399 fp.close()
402 fp.close()
400 raise error.Abort(
403 raise error.Abort(
401 _(b'working directory state may be changed parallelly')
404 _(b'working directory state may be changed parallelly')
402 )
405 )
403 self._pendingmode = mode
406 self._pendingmode = mode
404 return fp
407 return fp
405
408
406 def parents(self):
409 def parents(self):
407 if not self._parents:
410 if not self._parents:
408 try:
411 try:
409 fp = self._opendirstatefile()
412 fp = self._opendirstatefile()
410 st = fp.read(2 * self._nodelen)
413 st = fp.read(2 * self._nodelen)
411 fp.close()
414 fp.close()
412 except IOError as err:
415 except IOError as err:
413 if err.errno != errno.ENOENT:
416 if err.errno != errno.ENOENT:
414 raise
417 raise
415 # File doesn't exist, so the current state is empty
418 # File doesn't exist, so the current state is empty
416 st = b''
419 st = b''
417
420
418 l = len(st)
421 l = len(st)
419 if l == self._nodelen * 2:
422 if l == self._nodelen * 2:
420 self._parents = (
423 self._parents = (
421 st[: self._nodelen],
424 st[: self._nodelen],
422 st[self._nodelen : 2 * self._nodelen],
425 st[self._nodelen : 2 * self._nodelen],
423 )
426 )
424 elif l == 0:
427 elif l == 0:
425 self._parents = (
428 self._parents = (
426 self._nodeconstants.nullid,
429 self._nodeconstants.nullid,
427 self._nodeconstants.nullid,
430 self._nodeconstants.nullid,
428 )
431 )
429 else:
432 else:
430 raise error.Abort(
433 raise error.Abort(
431 _(b'working directory state appears damaged!')
434 _(b'working directory state appears damaged!')
432 )
435 )
433
436
434 return self._parents
437 return self._parents
435
438
436 def setparents(self, p1, p2):
439 def setparents(self, p1, p2):
437 self._parents = (p1, p2)
440 self._parents = (p1, p2)
438 self._dirtyparents = True
441 self._dirtyparents = True
439
442
440 def read(self):
443 def read(self):
441 # ignore HG_PENDING because identity is used only for writing
444 # ignore HG_PENDING because identity is used only for writing
442 self.identity = util.filestat.frompath(
445 self.identity = util.filestat.frompath(
443 self._opener.join(self._filename)
446 self._opener.join(self._filename)
444 )
447 )
445
448
446 try:
449 try:
447 fp = self._opendirstatefile()
450 fp = self._opendirstatefile()
448 try:
451 try:
449 st = fp.read()
452 st = fp.read()
450 finally:
453 finally:
451 fp.close()
454 fp.close()
452 except IOError as err:
455 except IOError as err:
453 if err.errno != errno.ENOENT:
456 if err.errno != errno.ENOENT:
454 raise
457 raise
455 return
458 return
456 if not st:
459 if not st:
457 return
460 return
458
461
459 if util.safehasattr(parsers, b'dict_new_presized'):
462 if util.safehasattr(parsers, b'dict_new_presized'):
460 # Make an estimate of the number of files in the dirstate based on
463 # Make an estimate of the number of files in the dirstate based on
461 # its size. This trades wasting some memory for avoiding costly
464 # its size. This trades wasting some memory for avoiding costly
462 # resizes. Each entry have a prefix of 17 bytes followed by one or
465 # resizes. Each entry have a prefix of 17 bytes followed by one or
463 # two path names. Studies on various large-scale real-world repositories
466 # two path names. Studies on various large-scale real-world repositories
464 # found 54 bytes a reasonable upper limit for the average path names.
467 # found 54 bytes a reasonable upper limit for the average path names.
465 # Copy entries are ignored for the sake of this estimate.
468 # Copy entries are ignored for the sake of this estimate.
466 self._map = parsers.dict_new_presized(len(st) // 71)
469 self._map = parsers.dict_new_presized(len(st) // 71)
467
470
468 # Python's garbage collector triggers a GC each time a certain number
471 # Python's garbage collector triggers a GC each time a certain number
469 # of container objects (the number being defined by
472 # of container objects (the number being defined by
470 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
473 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
471 # for each file in the dirstate. The C version then immediately marks
474 # for each file in the dirstate. The C version then immediately marks
472 # them as not to be tracked by the collector. However, this has no
475 # them as not to be tracked by the collector. However, this has no
473 # effect on when GCs are triggered, only on what objects the GC looks
476 # effect on when GCs are triggered, only on what objects the GC looks
474 # into. This means that O(number of files) GCs are unavoidable.
477 # into. This means that O(number of files) GCs are unavoidable.
475 # Depending on when in the process's lifetime the dirstate is parsed,
478 # Depending on when in the process's lifetime the dirstate is parsed,
476 # this can get very expensive. As a workaround, disable GC while
479 # this can get very expensive. As a workaround, disable GC while
477 # parsing the dirstate.
480 # parsing the dirstate.
478 #
481 #
479 # (we cannot decorate the function directly since it is in a C module)
482 # (we cannot decorate the function directly since it is in a C module)
480 parse_dirstate = util.nogc(parsers.parse_dirstate)
483 parse_dirstate = util.nogc(parsers.parse_dirstate)
481 p = parse_dirstate(self._map, self.copymap, st)
484 p = parse_dirstate(self._map, self.copymap, st)
482 if not self._dirtyparents:
485 if not self._dirtyparents:
483 self.setparents(*p)
486 self.setparents(*p)
484
487
485 # Avoid excess attribute lookups by fast pathing certain checks
488 # Avoid excess attribute lookups by fast pathing certain checks
486 self.__contains__ = self._map.__contains__
489 self.__contains__ = self._map.__contains__
487 self.__getitem__ = self._map.__getitem__
490 self.__getitem__ = self._map.__getitem__
488 self.get = self._map.get
491 self.get = self._map.get
489
492
490 def write(self, _tr, st, now):
493 def write(self, _tr, st, now):
491 st.write(
494 st.write(
492 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
495 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
493 )
496 )
494 st.close()
497 st.close()
495 self._dirtyparents = False
498 self._dirtyparents = False
496 self.nonnormalset, self.otherparentset = self.nonnormalentries()
499 self.nonnormalset, self.otherparentset = self.nonnormalentries()
497
500
498 @propertycache
501 @propertycache
499 def nonnormalset(self):
502 def nonnormalset(self):
500 nonnorm, otherparents = self.nonnormalentries()
503 nonnorm, otherparents = self.nonnormalentries()
501 self.otherparentset = otherparents
504 self.otherparentset = otherparents
502 return nonnorm
505 return nonnorm
503
506
504 @propertycache
507 @propertycache
505 def otherparentset(self):
508 def otherparentset(self):
506 nonnorm, otherparents = self.nonnormalentries()
509 nonnorm, otherparents = self.nonnormalentries()
507 self.nonnormalset = nonnorm
510 self.nonnormalset = nonnorm
508 return otherparents
511 return otherparents
509
512
510 def non_normal_or_other_parent_paths(self):
513 def non_normal_or_other_parent_paths(self):
511 return self.nonnormalset.union(self.otherparentset)
514 return self.nonnormalset.union(self.otherparentset)
512
515
513 @propertycache
516 @propertycache
514 def identity(self):
517 def identity(self):
515 self._map
518 self._map
516 return self.identity
519 return self.identity
517
520
518 @propertycache
521 @propertycache
519 def dirfoldmap(self):
522 def dirfoldmap(self):
520 f = {}
523 f = {}
521 normcase = util.normcase
524 normcase = util.normcase
522 for name in self._dirs:
525 for name in self._dirs:
523 f[normcase(name)] = name
526 f[normcase(name)] = name
524 return f
527 return f
525
528
526
529
527 if rustmod is not None:
530 if rustmod is not None:
528
531
529 class dirstatemap(object):
532 class dirstatemap(object):
530 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
533 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
531 self._use_dirstate_v2 = use_dirstate_v2
534 self._use_dirstate_v2 = use_dirstate_v2
532 self._nodeconstants = nodeconstants
535 self._nodeconstants = nodeconstants
533 self._ui = ui
536 self._ui = ui
534 self._opener = opener
537 self._opener = opener
535 self._root = root
538 self._root = root
536 self._filename = b'dirstate'
539 self._filename = b'dirstate'
537 self._nodelen = 20 # Also update Rust code when changing this!
540 self._nodelen = 20 # Also update Rust code when changing this!
538 self._parents = None
541 self._parents = None
539 self._dirtyparents = False
542 self._dirtyparents = False
540 self._docket = None
543 self._docket = None
541
544
542 # for consistent view between _pl() and _read() invocations
545 # for consistent view between _pl() and _read() invocations
543 self._pendingmode = None
546 self._pendingmode = None
544
547
545 self._use_dirstate_tree = self._ui.configbool(
548 self._use_dirstate_tree = self._ui.configbool(
546 b"experimental",
549 b"experimental",
547 b"dirstate-tree.in-memory",
550 b"dirstate-tree.in-memory",
548 False,
551 False,
549 )
552 )
550
553
551 def addfile(
554 def addfile(
552 self,
555 self,
553 f,
556 f,
554 mode=0,
557 mode=0,
555 size=None,
558 size=None,
556 mtime=None,
559 mtime=None,
557 added=False,
560 added=False,
558 merged=False,
561 merged=False,
559 from_p2=False,
562 from_p2=False,
560 possibly_dirty=False,
563 possibly_dirty=False,
561 ):
564 ):
562 ret = self._rustmap.addfile(
565 ret = self._rustmap.addfile(
563 f,
566 f,
564 mode,
567 mode,
565 size,
568 size,
566 mtime,
569 mtime,
567 added,
570 added,
568 merged,
571 merged,
569 from_p2,
572 from_p2,
570 possibly_dirty,
573 possibly_dirty,
571 )
574 )
572 if added:
575 if added:
573 self.copymap.pop(f, None)
576 self.copymap.pop(f, None)
574 return ret
577 return ret
575
578
576 def reset_state(
579 def reset_state(
577 self,
580 self,
578 filename,
581 filename,
579 wc_tracked,
582 wc_tracked=False,
580 p1_tracked,
583 p1_tracked=False,
581 p2_tracked=False,
584 p2_tracked=False,
582 merged=False,
585 merged=False,
583 clean_p1=False,
586 clean_p1=False,
584 clean_p2=False,
587 clean_p2=False,
585 possibly_dirty=False,
588 possibly_dirty=False,
586 parentfiledata=None,
589 parentfiledata=None,
587 ):
590 ):
588 """Set a entry to a given state, disregarding all previous state
591 """Set a entry to a given state, disregarding all previous state
589
592
590 This is to be used by the part of the dirstate API dedicated to
593 This is to be used by the part of the dirstate API dedicated to
591 adjusting the dirstate after a update/merge.
594 adjusting the dirstate after a update/merge.
592
595
593 note: calling this might result to no entry existing at all if the
596 note: calling this might result to no entry existing at all if the
594 dirstate map does not see any point at having one for this file
597 dirstate map does not see any point at having one for this file
595 anymore.
598 anymore.
596 """
599 """
597 if merged and (clean_p1 or clean_p2):
600 if merged and (clean_p1 or clean_p2):
598 msg = (
601 msg = (
599 b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
602 b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
600 )
603 )
601 raise error.ProgrammingError(msg)
604 raise error.ProgrammingError(msg)
602 # copy information are now outdated
605 # copy information are now outdated
603 # (maybe new information should be in directly passed to this function)
606 # (maybe new information should be in directly passed to this function)
604 self.copymap.pop(filename, None)
607 self.copymap.pop(filename, None)
605
608
606 if not (p1_tracked or p2_tracked or wc_tracked):
609 if not (p1_tracked or p2_tracked or wc_tracked):
607 self.dropfile(filename)
610 self.dropfile(filename)
608 elif merged:
611 elif merged:
609 # XXX might be merged and removed ?
612 # XXX might be merged and removed ?
610 entry = self.get(filename)
613 entry = self.get(filename)
611 if entry is not None and entry.tracked:
614 if entry is not None and entry.tracked:
612 # XXX mostly replicate dirstate.other parent. We should get
615 # XXX mostly replicate dirstate.other parent. We should get
613 # the higher layer to pass us more reliable data where `merged`
616 # the higher layer to pass us more reliable data where `merged`
614 # actually mean merged. Dropping the else clause will show
617 # actually mean merged. Dropping the else clause will show
615 # failure in `test-graft.t`
618 # failure in `test-graft.t`
616 self.addfile(filename, merged=True)
619 self.addfile(filename, merged=True)
617 else:
620 else:
618 self.addfile(filename, from_p2=True)
621 self.addfile(filename, from_p2=True)
619 elif not (p1_tracked or p2_tracked) and wc_tracked:
622 elif not (p1_tracked or p2_tracked) and wc_tracked:
620 self.addfile(
623 self.addfile(
621 filename, added=True, possibly_dirty=possibly_dirty
624 filename, added=True, possibly_dirty=possibly_dirty
622 )
625 )
623 elif (p1_tracked or p2_tracked) and not wc_tracked:
626 elif (p1_tracked or p2_tracked) and not wc_tracked:
624 # XXX might be merged and removed ?
627 # XXX might be merged and removed ?
625 self[filename] = DirstateItem.from_v1_data(b'r', 0, 0, 0)
628 self[filename] = DirstateItem.from_v1_data(b'r', 0, 0, 0)
626 self.nonnormalset.add(filename)
629 self.nonnormalset.add(filename)
627 elif clean_p2 and wc_tracked:
630 elif clean_p2 and wc_tracked:
628 if p1_tracked or self.get(filename) is not None:
631 if p1_tracked or self.get(filename) is not None:
629 # XXX the `self.get` call is catching some case in
632 # XXX the `self.get` call is catching some case in
630 # `test-merge-remove.t` where the file is tracked in p1, the
633 # `test-merge-remove.t` where the file is tracked in p1, the
631 # p1_tracked argument is False.
634 # p1_tracked argument is False.
632 #
635 #
633 # In addition, this seems to be a case where the file is marked
636 # In addition, this seems to be a case where the file is marked
634 # as merged without actually being the result of a merge
637 # as merged without actually being the result of a merge
635 # action. So thing are not ideal here.
638 # action. So thing are not ideal here.
636 self.addfile(filename, merged=True)
639 self.addfile(filename, merged=True)
637 else:
640 else:
638 self.addfile(filename, from_p2=True)
641 self.addfile(filename, from_p2=True)
639 elif not p1_tracked and p2_tracked and wc_tracked:
642 elif not p1_tracked and p2_tracked and wc_tracked:
640 self.addfile(
643 self.addfile(
641 filename, from_p2=True, possibly_dirty=possibly_dirty
644 filename, from_p2=True, possibly_dirty=possibly_dirty
642 )
645 )
643 elif possibly_dirty:
646 elif possibly_dirty:
644 self.addfile(filename, possibly_dirty=possibly_dirty)
647 self.addfile(filename, possibly_dirty=possibly_dirty)
645 elif wc_tracked:
648 elif wc_tracked:
646 # this is a "normal" file
649 # this is a "normal" file
647 if parentfiledata is None:
650 if parentfiledata is None:
648 msg = b'failed to pass parentfiledata for a normal file: %s'
651 msg = b'failed to pass parentfiledata for a normal file: %s'
649 msg %= filename
652 msg %= filename
650 raise error.ProgrammingError(msg)
653 raise error.ProgrammingError(msg)
651 mode, size, mtime = parentfiledata
654 mode, size, mtime = parentfiledata
652 self.addfile(filename, mode=mode, size=size, mtime=mtime)
655 self.addfile(filename, mode=mode, size=size, mtime=mtime)
653 self.nonnormalset.discard(filename)
656 self.nonnormalset.discard(filename)
654 else:
657 else:
655 assert False, 'unreachable'
658 assert False, 'unreachable'
656
659
657 def set_tracked(self, filename):
660 def set_tracked(self, filename):
658 new = False
661 new = False
659 entry = self.get(filename)
662 entry = self.get(filename)
660 if entry is None:
663 if entry is None:
661 self.addfile(filename, added=True)
664 self.addfile(filename, added=True)
662 new = True
665 new = True
663 elif not entry.tracked:
666 elif not entry.tracked:
664 entry.set_tracked()
667 entry.set_tracked()
665 self._rustmap.set_v1(filename, entry)
668 self._rustmap.set_v1(filename, entry)
666 new = True
669 new = True
667 else:
670 else:
668 # XXX This is probably overkill for more case, but we need this to
671 # XXX This is probably overkill for more case, but we need this to
669 # fully replace the `normallookup` call with `set_tracked` one.
672 # fully replace the `normallookup` call with `set_tracked` one.
670 # Consider smoothing this in the future.
673 # Consider smoothing this in the future.
671 self.set_possibly_dirty(filename)
674 self.set_possibly_dirty(filename)
672 return new
675 return new
673
676
674 def set_untracked(self, f):
677 def set_untracked(self, f):
675 """Mark a file as no longer tracked in the dirstate map"""
678 """Mark a file as no longer tracked in the dirstate map"""
676 # in merge is only trigger more logic, so it "fine" to pass it.
679 # in merge is only trigger more logic, so it "fine" to pass it.
677 #
680 #
678 # the inner rust dirstate map code need to be adjusted once the API
681 # the inner rust dirstate map code need to be adjusted once the API
679 # for dirstate/dirstatemap/DirstateItem is a bit more settled
682 # for dirstate/dirstatemap/DirstateItem is a bit more settled
680 entry = self.get(f)
683 entry = self.get(f)
681 if entry is None:
684 if entry is None:
682 return False
685 return False
683 else:
686 else:
684 if entry.added:
687 if entry.added:
685 self._rustmap.copymap().pop(f, None)
688 self._rustmap.copymap().pop(f, None)
686 self._rustmap.dropfile(f)
689 self._rustmap.dropfile(f)
687 else:
690 else:
688 self._rustmap.removefile(f, in_merge=True)
691 self._rustmap.removefile(f, in_merge=True)
689 return True
692 return True
690
693
691 def removefile(self, *args, **kwargs):
694 def removefile(self, *args, **kwargs):
692 return self._rustmap.removefile(*args, **kwargs)
695 return self._rustmap.removefile(*args, **kwargs)
693
696
694 def dropfile(self, f, *args, **kwargs):
697 def dropfile(self, f, *args, **kwargs):
695 self._rustmap.copymap().pop(f, None)
698 self._rustmap.copymap().pop(f, None)
696 return self._rustmap.dropfile(f, *args, **kwargs)
699 return self._rustmap.dropfile(f, *args, **kwargs)
697
700
698 def clearambiguoustimes(self, *args, **kwargs):
701 def clearambiguoustimes(self, *args, **kwargs):
699 return self._rustmap.clearambiguoustimes(*args, **kwargs)
702 return self._rustmap.clearambiguoustimes(*args, **kwargs)
700
703
701 def nonnormalentries(self):
704 def nonnormalentries(self):
702 return self._rustmap.nonnormalentries()
705 return self._rustmap.nonnormalentries()
703
706
704 def get(self, *args, **kwargs):
707 def get(self, *args, **kwargs):
705 return self._rustmap.get(*args, **kwargs)
708 return self._rustmap.get(*args, **kwargs)
706
709
707 @property
710 @property
708 def copymap(self):
711 def copymap(self):
709 return self._rustmap.copymap()
712 return self._rustmap.copymap()
710
713
711 def directories(self):
714 def directories(self):
712 return self._rustmap.directories()
715 return self._rustmap.directories()
713
716
714 def debug_iter(self):
717 def debug_iter(self):
715 return self._rustmap.debug_iter()
718 return self._rustmap.debug_iter()
716
719
717 def preload(self):
720 def preload(self):
718 self._rustmap
721 self._rustmap
719
722
720 def clear(self):
723 def clear(self):
721 self._rustmap.clear()
724 self._rustmap.clear()
722 self.setparents(
725 self.setparents(
723 self._nodeconstants.nullid, self._nodeconstants.nullid
726 self._nodeconstants.nullid, self._nodeconstants.nullid
724 )
727 )
725 util.clearcachedproperty(self, b"_dirs")
728 util.clearcachedproperty(self, b"_dirs")
726 util.clearcachedproperty(self, b"_alldirs")
729 util.clearcachedproperty(self, b"_alldirs")
727 util.clearcachedproperty(self, b"dirfoldmap")
730 util.clearcachedproperty(self, b"dirfoldmap")
728
731
729 def items(self):
732 def items(self):
730 return self._rustmap.items()
733 return self._rustmap.items()
731
734
732 def keys(self):
735 def keys(self):
733 return iter(self._rustmap)
736 return iter(self._rustmap)
734
737
735 def __contains__(self, key):
738 def __contains__(self, key):
736 return key in self._rustmap
739 return key in self._rustmap
737
740
738 def __getitem__(self, item):
741 def __getitem__(self, item):
739 return self._rustmap[item]
742 return self._rustmap[item]
740
743
741 def __len__(self):
744 def __len__(self):
742 return len(self._rustmap)
745 return len(self._rustmap)
743
746
744 def __iter__(self):
747 def __iter__(self):
745 return iter(self._rustmap)
748 return iter(self._rustmap)
746
749
747 # forward for python2,3 compat
750 # forward for python2,3 compat
748 iteritems = items
751 iteritems = items
749
752
750 def _opendirstatefile(self):
753 def _opendirstatefile(self):
751 fp, mode = txnutil.trypending(
754 fp, mode = txnutil.trypending(
752 self._root, self._opener, self._filename
755 self._root, self._opener, self._filename
753 )
756 )
754 if self._pendingmode is not None and self._pendingmode != mode:
757 if self._pendingmode is not None and self._pendingmode != mode:
755 fp.close()
758 fp.close()
756 raise error.Abort(
759 raise error.Abort(
757 _(b'working directory state may be changed parallelly')
760 _(b'working directory state may be changed parallelly')
758 )
761 )
759 self._pendingmode = mode
762 self._pendingmode = mode
760 return fp
763 return fp
761
764
762 def _readdirstatefile(self, size=-1):
765 def _readdirstatefile(self, size=-1):
763 try:
766 try:
764 with self._opendirstatefile() as fp:
767 with self._opendirstatefile() as fp:
765 return fp.read(size)
768 return fp.read(size)
766 except IOError as err:
769 except IOError as err:
767 if err.errno != errno.ENOENT:
770 if err.errno != errno.ENOENT:
768 raise
771 raise
769 # File doesn't exist, so the current state is empty
772 # File doesn't exist, so the current state is empty
770 return b''
773 return b''
771
774
772 def setparents(self, p1, p2):
775 def setparents(self, p1, p2):
773 self._parents = (p1, p2)
776 self._parents = (p1, p2)
774 self._dirtyparents = True
777 self._dirtyparents = True
775
778
776 def parents(self):
779 def parents(self):
777 if not self._parents:
780 if not self._parents:
778 if self._use_dirstate_v2:
781 if self._use_dirstate_v2:
779 self._parents = self.docket.parents
782 self._parents = self.docket.parents
780 else:
783 else:
781 read_len = self._nodelen * 2
784 read_len = self._nodelen * 2
782 st = self._readdirstatefile(read_len)
785 st = self._readdirstatefile(read_len)
783 l = len(st)
786 l = len(st)
784 if l == read_len:
787 if l == read_len:
785 self._parents = (
788 self._parents = (
786 st[: self._nodelen],
789 st[: self._nodelen],
787 st[self._nodelen : 2 * self._nodelen],
790 st[self._nodelen : 2 * self._nodelen],
788 )
791 )
789 elif l == 0:
792 elif l == 0:
790 self._parents = (
793 self._parents = (
791 self._nodeconstants.nullid,
794 self._nodeconstants.nullid,
792 self._nodeconstants.nullid,
795 self._nodeconstants.nullid,
793 )
796 )
794 else:
797 else:
795 raise error.Abort(
798 raise error.Abort(
796 _(b'working directory state appears damaged!')
799 _(b'working directory state appears damaged!')
797 )
800 )
798
801
799 return self._parents
802 return self._parents
800
803
801 @property
804 @property
802 def docket(self):
805 def docket(self):
803 if not self._docket:
806 if not self._docket:
804 if not self._use_dirstate_v2:
807 if not self._use_dirstate_v2:
805 raise error.ProgrammingError(
808 raise error.ProgrammingError(
806 b'dirstate only has a docket in v2 format'
809 b'dirstate only has a docket in v2 format'
807 )
810 )
808 self._docket = docketmod.DirstateDocket.parse(
811 self._docket = docketmod.DirstateDocket.parse(
809 self._readdirstatefile(), self._nodeconstants
812 self._readdirstatefile(), self._nodeconstants
810 )
813 )
811 return self._docket
814 return self._docket
812
815
813 @propertycache
816 @propertycache
814 def _rustmap(self):
817 def _rustmap(self):
815 """
818 """
816 Fills the Dirstatemap when called.
819 Fills the Dirstatemap when called.
817 """
820 """
818 # ignore HG_PENDING because identity is used only for writing
821 # ignore HG_PENDING because identity is used only for writing
819 self.identity = util.filestat.frompath(
822 self.identity = util.filestat.frompath(
820 self._opener.join(self._filename)
823 self._opener.join(self._filename)
821 )
824 )
822
825
823 if self._use_dirstate_v2:
826 if self._use_dirstate_v2:
824 if self.docket.uuid:
827 if self.docket.uuid:
825 # TODO: use mmap when possible
828 # TODO: use mmap when possible
826 data = self._opener.read(self.docket.data_filename())
829 data = self._opener.read(self.docket.data_filename())
827 else:
830 else:
828 data = b''
831 data = b''
829 self._rustmap = rustmod.DirstateMap.new_v2(
832 self._rustmap = rustmod.DirstateMap.new_v2(
830 data, self.docket.data_size, self.docket.tree_metadata
833 data, self.docket.data_size, self.docket.tree_metadata
831 )
834 )
832 parents = self.docket.parents
835 parents = self.docket.parents
833 else:
836 else:
834 self._rustmap, parents = rustmod.DirstateMap.new_v1(
837 self._rustmap, parents = rustmod.DirstateMap.new_v1(
835 self._use_dirstate_tree, self._readdirstatefile()
838 self._use_dirstate_tree, self._readdirstatefile()
836 )
839 )
837
840
838 if parents and not self._dirtyparents:
841 if parents and not self._dirtyparents:
839 self.setparents(*parents)
842 self.setparents(*parents)
840
843
841 self.__contains__ = self._rustmap.__contains__
844 self.__contains__ = self._rustmap.__contains__
842 self.__getitem__ = self._rustmap.__getitem__
845 self.__getitem__ = self._rustmap.__getitem__
843 self.get = self._rustmap.get
846 self.get = self._rustmap.get
844 return self._rustmap
847 return self._rustmap
845
848
846 def write(self, tr, st, now):
849 def write(self, tr, st, now):
847 if not self._use_dirstate_v2:
850 if not self._use_dirstate_v2:
848 p1, p2 = self.parents()
851 p1, p2 = self.parents()
849 packed = self._rustmap.write_v1(p1, p2, now)
852 packed = self._rustmap.write_v1(p1, p2, now)
850 st.write(packed)
853 st.write(packed)
851 st.close()
854 st.close()
852 self._dirtyparents = False
855 self._dirtyparents = False
853 return
856 return
854
857
855 # We can only append to an existing data file if there is one
858 # We can only append to an existing data file if there is one
856 can_append = self.docket.uuid is not None
859 can_append = self.docket.uuid is not None
857 packed, meta, append = self._rustmap.write_v2(now, can_append)
860 packed, meta, append = self._rustmap.write_v2(now, can_append)
858 if append:
861 if append:
859 docket = self.docket
862 docket = self.docket
860 data_filename = docket.data_filename()
863 data_filename = docket.data_filename()
861 if tr:
864 if tr:
862 tr.add(data_filename, docket.data_size)
865 tr.add(data_filename, docket.data_size)
863 with self._opener(data_filename, b'r+b') as fp:
866 with self._opener(data_filename, b'r+b') as fp:
864 fp.seek(docket.data_size)
867 fp.seek(docket.data_size)
865 assert fp.tell() == docket.data_size
868 assert fp.tell() == docket.data_size
866 written = fp.write(packed)
869 written = fp.write(packed)
867 if written is not None: # py2 may return None
870 if written is not None: # py2 may return None
868 assert written == len(packed), (written, len(packed))
871 assert written == len(packed), (written, len(packed))
869 docket.data_size += len(packed)
872 docket.data_size += len(packed)
870 docket.parents = self.parents()
873 docket.parents = self.parents()
871 docket.tree_metadata = meta
874 docket.tree_metadata = meta
872 st.write(docket.serialize())
875 st.write(docket.serialize())
873 st.close()
876 st.close()
874 else:
877 else:
875 old_docket = self.docket
878 old_docket = self.docket
876 new_docket = docketmod.DirstateDocket.with_new_uuid(
879 new_docket = docketmod.DirstateDocket.with_new_uuid(
877 self.parents(), len(packed), meta
880 self.parents(), len(packed), meta
878 )
881 )
879 data_filename = new_docket.data_filename()
882 data_filename = new_docket.data_filename()
880 if tr:
883 if tr:
881 tr.add(data_filename, 0)
884 tr.add(data_filename, 0)
882 self._opener.write(data_filename, packed)
885 self._opener.write(data_filename, packed)
883 # Write the new docket after the new data file has been
886 # Write the new docket after the new data file has been
884 # written. Because `st` was opened with `atomictemp=True`,
887 # written. Because `st` was opened with `atomictemp=True`,
885 # the actual `.hg/dirstate` file is only affected on close.
888 # the actual `.hg/dirstate` file is only affected on close.
886 st.write(new_docket.serialize())
889 st.write(new_docket.serialize())
887 st.close()
890 st.close()
888 # Remove the old data file after the new docket pointing to
891 # Remove the old data file after the new docket pointing to
889 # the new data file was written.
892 # the new data file was written.
890 if old_docket.uuid:
893 if old_docket.uuid:
891 data_filename = old_docket.data_filename()
894 data_filename = old_docket.data_filename()
892 unlink = lambda _tr=None: self._opener.unlink(data_filename)
895 unlink = lambda _tr=None: self._opener.unlink(data_filename)
893 if tr:
896 if tr:
894 category = b"dirstate-v2-clean-" + old_docket.uuid
897 category = b"dirstate-v2-clean-" + old_docket.uuid
895 tr.addpostclose(category, unlink)
898 tr.addpostclose(category, unlink)
896 else:
899 else:
897 unlink()
900 unlink()
898 self._docket = new_docket
901 self._docket = new_docket
899 # Reload from the newly-written file
902 # Reload from the newly-written file
900 util.clearcachedproperty(self, b"_rustmap")
903 util.clearcachedproperty(self, b"_rustmap")
901 self._dirtyparents = False
904 self._dirtyparents = False
902
905
903 @propertycache
906 @propertycache
904 def filefoldmap(self):
907 def filefoldmap(self):
905 """Returns a dictionary mapping normalized case paths to their
908 """Returns a dictionary mapping normalized case paths to their
906 non-normalized versions.
909 non-normalized versions.
907 """
910 """
908 return self._rustmap.filefoldmapasdict()
911 return self._rustmap.filefoldmapasdict()
909
912
910 def hastrackeddir(self, d):
913 def hastrackeddir(self, d):
911 return self._rustmap.hastrackeddir(d)
914 return self._rustmap.hastrackeddir(d)
912
915
913 def hasdir(self, d):
916 def hasdir(self, d):
914 return self._rustmap.hasdir(d)
917 return self._rustmap.hasdir(d)
915
918
916 @propertycache
919 @propertycache
917 def identity(self):
920 def identity(self):
918 self._rustmap
921 self._rustmap
919 return self.identity
922 return self.identity
920
923
921 @property
924 @property
922 def nonnormalset(self):
925 def nonnormalset(self):
923 nonnorm = self._rustmap.non_normal_entries()
926 nonnorm = self._rustmap.non_normal_entries()
924 return nonnorm
927 return nonnorm
925
928
926 @propertycache
929 @propertycache
927 def otherparentset(self):
930 def otherparentset(self):
928 otherparents = self._rustmap.other_parent_entries()
931 otherparents = self._rustmap.other_parent_entries()
929 return otherparents
932 return otherparents
930
933
931 def non_normal_or_other_parent_paths(self):
934 def non_normal_or_other_parent_paths(self):
932 return self._rustmap.non_normal_or_other_parent_paths()
935 return self._rustmap.non_normal_or_other_parent_paths()
933
936
934 @propertycache
937 @propertycache
935 def dirfoldmap(self):
938 def dirfoldmap(self):
936 f = {}
939 f = {}
937 normcase = util.normcase
940 normcase = util.normcase
938 for name in self._rustmap.tracked_dirs():
941 for name in self._rustmap.tracked_dirs():
939 f[normcase(name)] = name
942 f[normcase(name)] = name
940 return f
943 return f
941
944
942 def set_possibly_dirty(self, filename):
945 def set_possibly_dirty(self, filename):
943 """record that the current state of the file on disk is unknown"""
946 """record that the current state of the file on disk is unknown"""
944 entry = self[filename]
947 entry = self[filename]
945 entry.set_possibly_dirty()
948 entry.set_possibly_dirty()
946 self._rustmap.set_v1(filename, entry)
949 self._rustmap.set_v1(filename, entry)
947
950
948 def set_clean(self, filename, mode, size, mtime):
951 def set_clean(self, filename, mode, size, mtime):
949 """mark a file as back to a clean state"""
952 """mark a file as back to a clean state"""
950 entry = self[filename]
953 entry = self[filename]
951 mtime = mtime & rangemask
954 mtime = mtime & rangemask
952 size = size & rangemask
955 size = size & rangemask
953 entry.set_clean(mode, size, mtime)
956 entry.set_clean(mode, size, mtime)
954 self._rustmap.set_v1(filename, entry)
957 self._rustmap.set_v1(filename, entry)
955 self._rustmap.copymap().pop(filename, None)
958 self._rustmap.copymap().pop(filename, None)
956
959
957 def __setitem__(self, key, value):
960 def __setitem__(self, key, value):
958 assert isinstance(value, DirstateItem)
961 assert isinstance(value, DirstateItem)
959 self._rustmap.set_v1(key, value)
962 self._rustmap.set_v1(key, value)
General Comments 0
You need to be logged in to leave comments. Login now