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