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