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