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