##// END OF EJS Templates
rust-dirstate: fall back to v1 if reading v2 failed...
Raphaël Gomès -
r51553:bf16ef96 stable
parent child Browse files
Show More
@@ -1,786 +1,852 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 import struct
7 from .i18n import _
8 from .i18n import _
8
9
9 from . import (
10 from . import (
10 error,
11 error,
11 pathutil,
12 pathutil,
12 policy,
13 policy,
13 testing,
14 testing,
14 txnutil,
15 txnutil,
15 util,
16 util,
16 )
17 )
17
18
18 from .dirstateutils import (
19 from .dirstateutils import (
19 docket as docketmod,
20 docket as docketmod,
20 v2,
21 v2,
21 )
22 )
22
23
23 parsers = policy.importmod('parsers')
24 parsers = policy.importmod('parsers')
24 rustmod = policy.importrust('dirstate')
25 rustmod = policy.importrust('dirstate')
25
26
26 propertycache = util.propertycache
27 propertycache = util.propertycache
27
28
28 if rustmod is None:
29 if rustmod is None:
29 DirstateItem = parsers.DirstateItem
30 DirstateItem = parsers.DirstateItem
30 else:
31 else:
31 DirstateItem = rustmod.DirstateItem
32 DirstateItem = rustmod.DirstateItem
32
33
33 rangemask = 0x7FFFFFFF
34 rangemask = 0x7FFFFFFF
34
35
35 WRITE_MODE_AUTO = 0
36 WRITE_MODE_AUTO = 0
36 WRITE_MODE_FORCE_NEW = 1
37 WRITE_MODE_FORCE_NEW = 1
37 WRITE_MODE_FORCE_APPEND = 2
38 WRITE_MODE_FORCE_APPEND = 2
38
39
39
40
40 V2_MAX_READ_ATTEMPTS = 5
41 V2_MAX_READ_ATTEMPTS = 5
41
42
42
43
43 class _dirstatemapcommon:
44 class _dirstatemapcommon:
44 """
45 """
45 Methods that are identical for both implementations of the dirstatemap
46 Methods that are identical for both implementations of the dirstatemap
46 class, with and without Rust extensions enabled.
47 class, with and without Rust extensions enabled.
47 """
48 """
48
49
49 # please pytype
50 # please pytype
50
51
51 _map = None
52 _map = None
52 copymap = None
53 copymap = None
53
54
54 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
55 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
55 self._use_dirstate_v2 = use_dirstate_v2
56 self._use_dirstate_v2 = use_dirstate_v2
56 self._nodeconstants = nodeconstants
57 self._nodeconstants = nodeconstants
57 self._ui = ui
58 self._ui = ui
58 self._opener = opener
59 self._opener = opener
59 self._root = root
60 self._root = root
60 self._filename = b'dirstate'
61 self._filename = b'dirstate'
61 self._nodelen = 20 # Also update Rust code when changing this!
62 self._nodelen = 20 # Also update Rust code when changing this!
62 self._parents = None
63 self._parents = None
63 self._dirtyparents = False
64 self._dirtyparents = False
64 self._docket = None
65 self._docket = None
65 write_mode = ui.config(b"devel", b"dirstate.v2.data_update_mode")
66 write_mode = ui.config(b"devel", b"dirstate.v2.data_update_mode")
66 if write_mode == b"auto":
67 if write_mode == b"auto":
67 self._write_mode = WRITE_MODE_AUTO
68 self._write_mode = WRITE_MODE_AUTO
68 elif write_mode == b"force-append":
69 elif write_mode == b"force-append":
69 self._write_mode = WRITE_MODE_FORCE_APPEND
70 self._write_mode = WRITE_MODE_FORCE_APPEND
70 elif write_mode == b"force-new":
71 elif write_mode == b"force-new":
71 self._write_mode = WRITE_MODE_FORCE_NEW
72 self._write_mode = WRITE_MODE_FORCE_NEW
72 else:
73 else:
73 # unknown value, fallback to default
74 # unknown value, fallback to default
74 self._write_mode = WRITE_MODE_AUTO
75 self._write_mode = WRITE_MODE_AUTO
75
76
76 # for consistent view between _pl() and _read() invocations
77 # for consistent view between _pl() and _read() invocations
77 self._pendingmode = None
78 self._pendingmode = None
78
79
79 def _set_identity(self):
80 def _set_identity(self):
80 self.identity = self._get_current_identity()
81 self.identity = self._get_current_identity()
81
82
82 def _get_current_identity(self):
83 def _get_current_identity(self):
83 try:
84 try:
84 return util.cachestat(self._opener.join(self._filename))
85 return util.cachestat(self._opener.join(self._filename))
85 except FileNotFoundError:
86 except FileNotFoundError:
86 return None
87 return None
87
88
88 def may_need_refresh(self):
89 def may_need_refresh(self):
89 if 'identity' not in vars(self):
90 if 'identity' not in vars(self):
90 # no existing identity, we need a refresh
91 # no existing identity, we need a refresh
91 return True
92 return True
92 if self.identity is None:
93 if self.identity is None:
93 return True
94 return True
94 if not self.identity.cacheable():
95 if not self.identity.cacheable():
95 # We cannot trust the entry
96 # We cannot trust the entry
96 # XXX this is a problem on windows, NFS, or other inode less system
97 # XXX this is a problem on windows, NFS, or other inode less system
97 return True
98 return True
98 current_identity = self._get_current_identity()
99 current_identity = self._get_current_identity()
99 if current_identity is None:
100 if current_identity is None:
100 return True
101 return True
101 if not current_identity.cacheable():
102 if not current_identity.cacheable():
102 # We cannot trust the entry
103 # We cannot trust the entry
103 # XXX this is a problem on windows, NFS, or other inode less system
104 # XXX this is a problem on windows, NFS, or other inode less system
104 return True
105 return True
105 return current_identity != self.identity
106 return current_identity != self.identity
106
107
107 def preload(self):
108 def preload(self):
108 """Loads the underlying data, if it's not already loaded"""
109 """Loads the underlying data, if it's not already loaded"""
109 self._map
110 self._map
110
111
111 def get(self, key, default=None):
112 def get(self, key, default=None):
112 return self._map.get(key, default)
113 return self._map.get(key, default)
113
114
114 def __len__(self):
115 def __len__(self):
115 return len(self._map)
116 return len(self._map)
116
117
117 def __iter__(self):
118 def __iter__(self):
118 return iter(self._map)
119 return iter(self._map)
119
120
120 def __contains__(self, key):
121 def __contains__(self, key):
121 return key in self._map
122 return key in self._map
122
123
123 def __getitem__(self, item):
124 def __getitem__(self, item):
124 return self._map[item]
125 return self._map[item]
125
126
126 ### disk interaction
127 ### disk interaction
127
128
128 def _opendirstatefile(self):
129 def _opendirstatefile(self):
129 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
130 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
130 if self._pendingmode is not None and self._pendingmode != mode:
131 if self._pendingmode is not None and self._pendingmode != mode:
131 fp.close()
132 fp.close()
132 raise error.Abort(
133 raise error.Abort(
133 _(b'working directory state may be changed parallelly')
134 _(b'working directory state may be changed parallelly')
134 )
135 )
135 self._pendingmode = mode
136 self._pendingmode = mode
136 return fp
137 return fp
137
138
138 def _readdirstatefile(self, size=-1):
139 def _readdirstatefile(self, size=-1):
139 try:
140 try:
140 with self._opendirstatefile() as fp:
141 with self._opendirstatefile() as fp:
141 return fp.read(size)
142 return fp.read(size)
142 except FileNotFoundError:
143 except FileNotFoundError:
143 # File doesn't exist, so the current state is empty
144 # File doesn't exist, so the current state is empty
144 return b''
145 return b''
145
146
146 @property
147 @property
147 def docket(self):
148 def docket(self):
148 if not self._docket:
149 if not self._docket:
149 if not self._use_dirstate_v2:
150 if not self._use_dirstate_v2:
150 raise error.ProgrammingError(
151 raise error.ProgrammingError(
151 b'dirstate only has a docket in v2 format'
152 b'dirstate only has a docket in v2 format'
152 )
153 )
153 self._set_identity()
154 self._set_identity()
155 try:
154 self._docket = docketmod.DirstateDocket.parse(
156 self._docket = docketmod.DirstateDocket.parse(
155 self._readdirstatefile(), self._nodeconstants
157 self._readdirstatefile(), self._nodeconstants
156 )
158 )
159 except struct.error:
160 self._ui.debug(b"failed to read dirstate-v2 data")
161 raise error.CorruptedDirstate(
162 b"failed to read dirstate-v2 data"
163 )
157 return self._docket
164 return self._docket
158
165
159 def _read_v2_data(self):
166 def _read_v2_data(self):
160 data = None
167 data = None
161 attempts = 0
168 attempts = 0
162 while attempts < V2_MAX_READ_ATTEMPTS:
169 while attempts < V2_MAX_READ_ATTEMPTS:
163 attempts += 1
170 attempts += 1
164 try:
171 try:
165 # TODO: use mmap when possible
172 # TODO: use mmap when possible
166 data = self._opener.read(self.docket.data_filename())
173 data = self._opener.read(self.docket.data_filename())
167 except FileNotFoundError:
174 except FileNotFoundError:
168 # read race detected between docket and data file
175 # read race detected between docket and data file
169 # reload the docket and retry
176 # reload the docket and retry
170 self._docket = None
177 self._docket = None
171 if data is None:
178 if data is None:
172 assert attempts >= V2_MAX_READ_ATTEMPTS
179 assert attempts >= V2_MAX_READ_ATTEMPTS
173 msg = b"dirstate read race happened %d times in a row"
180 msg = b"dirstate read race happened %d times in a row"
174 msg %= attempts
181 msg %= attempts
175 raise error.Abort(msg)
182 raise error.Abort(msg)
176 return self._opener.read(self.docket.data_filename())
183 return self._opener.read(self.docket.data_filename())
177
184
178 def write_v2_no_append(self, tr, st, meta, packed):
185 def write_v2_no_append(self, tr, st, meta, packed):
186 try:
179 old_docket = self.docket
187 old_docket = self.docket
188 except error.CorruptedDirstate:
189 # This means we've identified a dirstate-v1 file on-disk when we
190 # were expecting a dirstate-v2 docket. We've managed to recover
191 # from that unexpected situation, and now we want to write back a
192 # dirstate-v2 file to make the on-disk situation right again.
193 #
194 # This shouldn't be triggered since `self.docket` is cached and
195 # we would have called parents() or read() first, but it's here
196 # just in case.
197 old_docket = None
198
180 new_docket = docketmod.DirstateDocket.with_new_uuid(
199 new_docket = docketmod.DirstateDocket.with_new_uuid(
181 self.parents(), len(packed), meta
200 self.parents(), len(packed), meta
182 )
201 )
183 if old_docket.uuid == new_docket.uuid:
202 if old_docket is not None and old_docket.uuid == new_docket.uuid:
184 raise error.ProgrammingError(b'dirstate docket name collision')
203 raise error.ProgrammingError(b'dirstate docket name collision')
185 data_filename = new_docket.data_filename()
204 data_filename = new_docket.data_filename()
186 self._opener.write(data_filename, packed)
205 self._opener.write(data_filename, packed)
187 # tell the transaction that we are adding a new file
206 # tell the transaction that we are adding a new file
188 if tr is not None:
207 if tr is not None:
189 tr.addbackup(data_filename, location=b'plain')
208 tr.addbackup(data_filename, location=b'plain')
190 # Write the new docket after the new data file has been
209 # Write the new docket after the new data file has been
191 # written. Because `st` was opened with `atomictemp=True`,
210 # written. Because `st` was opened with `atomictemp=True`,
192 # the actual `.hg/dirstate` file is only affected on close.
211 # the actual `.hg/dirstate` file is only affected on close.
193 st.write(new_docket.serialize())
212 st.write(new_docket.serialize())
194 st.close()
213 st.close()
195 # Remove the old data file after the new docket pointing to
214 # Remove the old data file after the new docket pointing to
196 # the new data file was written.
215 # the new data file was written.
197 if old_docket.uuid:
216 if old_docket is not None and old_docket.uuid:
198 data_filename = old_docket.data_filename()
217 data_filename = old_docket.data_filename()
199 if tr is not None:
218 if tr is not None:
200 tr.addbackup(data_filename, location=b'plain')
219 tr.addbackup(data_filename, location=b'plain')
201 unlink = lambda _tr=None: self._opener.unlink(data_filename)
220 unlink = lambda _tr=None: self._opener.unlink(data_filename)
202 if tr:
221 if tr:
203 category = b"dirstate-v2-clean-" + old_docket.uuid
222 category = b"dirstate-v2-clean-" + old_docket.uuid
204 tr.addpostclose(category, unlink)
223 tr.addpostclose(category, unlink)
205 else:
224 else:
206 unlink()
225 unlink()
207 self._docket = new_docket
226 self._docket = new_docket
208
227
209 ### reading/setting parents
228 ### reading/setting parents
210
229
211 def parents(self):
230 def parents(self):
212 if not self._parents:
231 if not self._parents:
213 if self._use_dirstate_v2:
232 if self._use_dirstate_v2:
233 try:
234 self.docket
235 except error.CorruptedDirstate as e:
236 # fall back to dirstate-v1 if we fail to read v2
237 self._v1_parents(e)
238 else:
214 self._parents = self.docket.parents
239 self._parents = self.docket.parents
215 else:
240 else:
241 self._v1_parents()
242
243 return self._parents
244
245 def _v1_parents(self, from_v2_exception=None):
216 read_len = self._nodelen * 2
246 read_len = self._nodelen * 2
217 st = self._readdirstatefile(read_len)
247 st = self._readdirstatefile(read_len)
218 l = len(st)
248 l = len(st)
219 if l == read_len:
249 if l == read_len:
220 self._parents = (
250 self._parents = (
221 st[: self._nodelen],
251 st[: self._nodelen],
222 st[self._nodelen : 2 * self._nodelen],
252 st[self._nodelen : 2 * self._nodelen],
223 )
253 )
224 elif l == 0:
254 elif l == 0:
225 self._parents = (
255 self._parents = (
226 self._nodeconstants.nullid,
256 self._nodeconstants.nullid,
227 self._nodeconstants.nullid,
257 self._nodeconstants.nullid,
228 )
258 )
229 else:
259 else:
260 hint = None
261 if from_v2_exception is not None:
262 hint = _(b"falling back to dirstate-v1 from v2 also failed")
230 raise error.Abort(
263 raise error.Abort(
231 _(b'working directory state appears damaged!')
264 _(b'working directory state appears damaged!'), hint
232 )
265 )
233
266
234 return self._parents
235
236
267
237 class dirstatemap(_dirstatemapcommon):
268 class dirstatemap(_dirstatemapcommon):
238 """Map encapsulating the dirstate's contents.
269 """Map encapsulating the dirstate's contents.
239
270
240 The dirstate contains the following state:
271 The dirstate contains the following state:
241
272
242 - `identity` is the identity of the dirstate file, which can be used to
273 - `identity` is the identity of the dirstate file, which can be used to
243 detect when changes have occurred to the dirstate file.
274 detect when changes have occurred to the dirstate file.
244
275
245 - `parents` is a pair containing the parents of the working copy. The
276 - `parents` is a pair containing the parents of the working copy. The
246 parents are updated by calling `setparents`.
277 parents are updated by calling `setparents`.
247
278
248 - the state map maps filenames to tuples of (state, mode, size, mtime),
279 - the state map maps filenames to tuples of (state, mode, size, mtime),
249 where state is a single character representing 'normal', 'added',
280 where state is a single character representing 'normal', 'added',
250 'removed', or 'merged'. It is read by treating the dirstate as a
281 'removed', or 'merged'. It is read by treating the dirstate as a
251 dict. File state is updated by calling various methods (see each
282 dict. File state is updated by calling various methods (see each
252 documentation for details):
283 documentation for details):
253
284
254 - `reset_state`,
285 - `reset_state`,
255 - `set_tracked`
286 - `set_tracked`
256 - `set_untracked`
287 - `set_untracked`
257 - `set_clean`
288 - `set_clean`
258 - `set_possibly_dirty`
289 - `set_possibly_dirty`
259
290
260 - `copymap` maps destination filenames to their source filename.
291 - `copymap` maps destination filenames to their source filename.
261
292
262 The dirstate also provides the following views onto the state:
293 The dirstate also provides the following views onto the state:
263
294
264 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
295 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
265 form that they appear as in the dirstate.
296 form that they appear as in the dirstate.
266
297
267 - `dirfoldmap` is a dict mapping normalized directory names to the
298 - `dirfoldmap` is a dict mapping normalized directory names to the
268 denormalized form that they appear as in the dirstate.
299 denormalized form that they appear as in the dirstate.
269 """
300 """
270
301
271 ### Core data storage and access
302 ### Core data storage and access
272
303
273 @propertycache
304 @propertycache
274 def _map(self):
305 def _map(self):
275 self._map = {}
306 self._map = {}
276 self.read()
307 self.read()
277 return self._map
308 return self._map
278
309
279 @propertycache
310 @propertycache
280 def copymap(self):
311 def copymap(self):
281 self.copymap = {}
312 self.copymap = {}
282 self._map
313 self._map
283 return self.copymap
314 return self.copymap
284
315
285 def clear(self):
316 def clear(self):
286 self._map.clear()
317 self._map.clear()
287 self.copymap.clear()
318 self.copymap.clear()
288 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
319 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
289 util.clearcachedproperty(self, b"_dirs")
320 util.clearcachedproperty(self, b"_dirs")
290 util.clearcachedproperty(self, b"_alldirs")
321 util.clearcachedproperty(self, b"_alldirs")
291 util.clearcachedproperty(self, b"filefoldmap")
322 util.clearcachedproperty(self, b"filefoldmap")
292 util.clearcachedproperty(self, b"dirfoldmap")
323 util.clearcachedproperty(self, b"dirfoldmap")
293
324
294 def items(self):
325 def items(self):
295 return self._map.items()
326 return self._map.items()
296
327
297 # forward for python2,3 compat
328 # forward for python2,3 compat
298 iteritems = items
329 iteritems = items
299
330
300 def debug_iter(self, all):
331 def debug_iter(self, all):
301 """
332 """
302 Return an iterator of (filename, state, mode, size, mtime) tuples
333 Return an iterator of (filename, state, mode, size, mtime) tuples
303
334
304 `all` is unused when Rust is not enabled
335 `all` is unused when Rust is not enabled
305 """
336 """
306 for (filename, item) in self.items():
337 for (filename, item) in self.items():
307 yield (filename, item.state, item.mode, item.size, item.mtime)
338 yield (filename, item.state, item.mode, item.size, item.mtime)
308
339
309 def keys(self):
340 def keys(self):
310 return self._map.keys()
341 return self._map.keys()
311
342
312 ### reading/setting parents
343 ### reading/setting parents
313
344
314 def setparents(self, p1, p2, fold_p2=False):
345 def setparents(self, p1, p2, fold_p2=False):
315 self._parents = (p1, p2)
346 self._parents = (p1, p2)
316 self._dirtyparents = True
347 self._dirtyparents = True
317 copies = {}
348 copies = {}
318 if fold_p2:
349 if fold_p2:
319 for f, s in self._map.items():
350 for f, s in self._map.items():
320 # Discard "merged" markers when moving away from a merge state
351 # Discard "merged" markers when moving away from a merge state
321 if s.p2_info:
352 if s.p2_info:
322 source = self.copymap.pop(f, None)
353 source = self.copymap.pop(f, None)
323 if source:
354 if source:
324 copies[f] = source
355 copies[f] = source
325 s.drop_merge_data()
356 s.drop_merge_data()
326 return copies
357 return copies
327
358
328 ### disk interaction
359 ### disk interaction
329
360
330 def read(self):
361 def read(self):
331 testing.wait_on_cfg(self._ui, b'dirstate.pre-read-file')
362 testing.wait_on_cfg(self._ui, b'dirstate.pre-read-file')
332 if self._use_dirstate_v2:
363 if self._use_dirstate_v2:
333
364 try:
365 self.docket
366 except error.CorruptedDirstate:
367 # fall back to dirstate-v1 if we fail to read v2
368 self._set_identity()
369 st = self._readdirstatefile()
370 else:
334 if not self.docket.uuid:
371 if not self.docket.uuid:
335 return
372 return
336 testing.wait_on_cfg(self._ui, b'dirstate.post-docket-read-file')
373 testing.wait_on_cfg(self._ui, b'dirstate.post-docket-read-file')
337 st = self._read_v2_data()
374 st = self._read_v2_data()
338 else:
375 else:
339 self._set_identity()
376 self._set_identity()
340 st = self._readdirstatefile()
377 st = self._readdirstatefile()
341
378
342 if not st:
379 if not st:
343 return
380 return
344
381
345 # TODO: adjust this estimate for dirstate-v2
382 # TODO: adjust this estimate for dirstate-v2
346 if util.safehasattr(parsers, b'dict_new_presized'):
383 if util.safehasattr(parsers, b'dict_new_presized'):
347 # Make an estimate of the number of files in the dirstate based on
384 # Make an estimate of the number of files in the dirstate based on
348 # its size. This trades wasting some memory for avoiding costly
385 # its size. This trades wasting some memory for avoiding costly
349 # resizes. Each entry have a prefix of 17 bytes followed by one or
386 # 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
387 # two path names. Studies on various large-scale real-world repositories
351 # found 54 bytes a reasonable upper limit for the average path names.
388 # found 54 bytes a reasonable upper limit for the average path names.
352 # Copy entries are ignored for the sake of this estimate.
389 # Copy entries are ignored for the sake of this estimate.
353 self._map = parsers.dict_new_presized(len(st) // 71)
390 self._map = parsers.dict_new_presized(len(st) // 71)
354
391
355 # Python's garbage collector triggers a GC each time a certain number
392 # Python's garbage collector triggers a GC each time a certain number
356 # of container objects (the number being defined by
393 # of container objects (the number being defined by
357 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
394 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
358 # for each file in the dirstate. The C version then immediately marks
395 # 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
396 # 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
397 # 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.
398 # into. This means that O(number of files) GCs are unavoidable.
362 # Depending on when in the process's lifetime the dirstate is parsed,
399 # Depending on when in the process's lifetime the dirstate is parsed,
363 # this can get very expensive. As a workaround, disable GC while
400 # this can get very expensive. As a workaround, disable GC while
364 # parsing the dirstate.
401 # parsing the dirstate.
365 #
402 #
366 # (we cannot decorate the function directly since it is in a C module)
403 # (we cannot decorate the function directly since it is in a C module)
367 if self._use_dirstate_v2:
404 if self._use_dirstate_v2:
405 try:
406 self.docket
407 except error.CorruptedDirstate:
408 # fall back to dirstate-v1 if we fail to parse v2
409 parse_dirstate = util.nogc(parsers.parse_dirstate)
410 p = parse_dirstate(self._map, self.copymap, st)
411 else:
368 p = self.docket.parents
412 p = self.docket.parents
369 meta = self.docket.tree_metadata
413 meta = self.docket.tree_metadata
370 parse_dirstate = util.nogc(v2.parse_dirstate)
414 parse_dirstate = util.nogc(v2.parse_dirstate)
371 parse_dirstate(self._map, self.copymap, st, meta)
415 parse_dirstate(self._map, self.copymap, st, meta)
372 else:
416 else:
373 parse_dirstate = util.nogc(parsers.parse_dirstate)
417 parse_dirstate = util.nogc(parsers.parse_dirstate)
374 p = parse_dirstate(self._map, self.copymap, st)
418 p = parse_dirstate(self._map, self.copymap, st)
375 if not self._dirtyparents:
419 if not self._dirtyparents:
376 self.setparents(*p)
420 self.setparents(*p)
377
421
378 # Avoid excess attribute lookups by fast pathing certain checks
422 # Avoid excess attribute lookups by fast pathing certain checks
379 self.__contains__ = self._map.__contains__
423 self.__contains__ = self._map.__contains__
380 self.__getitem__ = self._map.__getitem__
424 self.__getitem__ = self._map.__getitem__
381 self.get = self._map.get
425 self.get = self._map.get
382
426
383 def write(self, tr, st):
427 def write(self, tr, st):
384 if self._use_dirstate_v2:
428 if self._use_dirstate_v2:
385 packed, meta = v2.pack_dirstate(self._map, self.copymap)
429 packed, meta = v2.pack_dirstate(self._map, self.copymap)
386 self.write_v2_no_append(tr, st, meta, packed)
430 self.write_v2_no_append(tr, st, meta, packed)
387 else:
431 else:
388 packed = parsers.pack_dirstate(
432 packed = parsers.pack_dirstate(
389 self._map, self.copymap, self.parents()
433 self._map, self.copymap, self.parents()
390 )
434 )
391 st.write(packed)
435 st.write(packed)
392 st.close()
436 st.close()
393 self._dirtyparents = False
437 self._dirtyparents = False
394
438
395 @propertycache
439 @propertycache
396 def identity(self):
440 def identity(self):
397 self._map
441 self._map
398 return self.identity
442 return self.identity
399
443
400 ### code related to maintaining and accessing "extra" property
444 ### code related to maintaining and accessing "extra" property
401 # (e.g. "has_dir")
445 # (e.g. "has_dir")
402
446
403 def _dirs_incr(self, filename, old_entry=None):
447 def _dirs_incr(self, filename, old_entry=None):
404 """increment the dirstate counter if applicable"""
448 """increment the dirstate counter if applicable"""
405 if (
449 if (
406 old_entry is None or old_entry.removed
450 old_entry is None or old_entry.removed
407 ) and "_dirs" in self.__dict__:
451 ) and "_dirs" in self.__dict__:
408 self._dirs.addpath(filename)
452 self._dirs.addpath(filename)
409 if old_entry is None and "_alldirs" in self.__dict__:
453 if old_entry is None and "_alldirs" in self.__dict__:
410 self._alldirs.addpath(filename)
454 self._alldirs.addpath(filename)
411
455
412 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
456 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
413 """decrement the dirstate counter if applicable"""
457 """decrement the dirstate counter if applicable"""
414 if old_entry is not None:
458 if old_entry is not None:
415 if "_dirs" in self.__dict__ and not old_entry.removed:
459 if "_dirs" in self.__dict__ and not old_entry.removed:
416 self._dirs.delpath(filename)
460 self._dirs.delpath(filename)
417 if "_alldirs" in self.__dict__ and not remove_variant:
461 if "_alldirs" in self.__dict__ and not remove_variant:
418 self._alldirs.delpath(filename)
462 self._alldirs.delpath(filename)
419 elif remove_variant and "_alldirs" in self.__dict__:
463 elif remove_variant and "_alldirs" in self.__dict__:
420 self._alldirs.addpath(filename)
464 self._alldirs.addpath(filename)
421 if "filefoldmap" in self.__dict__:
465 if "filefoldmap" in self.__dict__:
422 normed = util.normcase(filename)
466 normed = util.normcase(filename)
423 self.filefoldmap.pop(normed, None)
467 self.filefoldmap.pop(normed, None)
424
468
425 @propertycache
469 @propertycache
426 def filefoldmap(self):
470 def filefoldmap(self):
427 """Returns a dictionary mapping normalized case paths to their
471 """Returns a dictionary mapping normalized case paths to their
428 non-normalized versions.
472 non-normalized versions.
429 """
473 """
430 try:
474 try:
431 makefilefoldmap = parsers.make_file_foldmap
475 makefilefoldmap = parsers.make_file_foldmap
432 except AttributeError:
476 except AttributeError:
433 pass
477 pass
434 else:
478 else:
435 return makefilefoldmap(
479 return makefilefoldmap(
436 self._map, util.normcasespec, util.normcasefallback
480 self._map, util.normcasespec, util.normcasefallback
437 )
481 )
438
482
439 f = {}
483 f = {}
440 normcase = util.normcase
484 normcase = util.normcase
441 for name, s in self._map.items():
485 for name, s in self._map.items():
442 if not s.removed:
486 if not s.removed:
443 f[normcase(name)] = name
487 f[normcase(name)] = name
444 f[b'.'] = b'.' # prevents useless util.fspath() invocation
488 f[b'.'] = b'.' # prevents useless util.fspath() invocation
445 return f
489 return f
446
490
447 @propertycache
491 @propertycache
448 def dirfoldmap(self):
492 def dirfoldmap(self):
449 f = {}
493 f = {}
450 normcase = util.normcase
494 normcase = util.normcase
451 for name in self._dirs:
495 for name in self._dirs:
452 f[normcase(name)] = name
496 f[normcase(name)] = name
453 return f
497 return f
454
498
455 def hastrackeddir(self, d):
499 def hastrackeddir(self, d):
456 """
500 """
457 Returns True if the dirstate contains a tracked (not removed) file
501 Returns True if the dirstate contains a tracked (not removed) file
458 in this directory.
502 in this directory.
459 """
503 """
460 return d in self._dirs
504 return d in self._dirs
461
505
462 def hasdir(self, d):
506 def hasdir(self, d):
463 """
507 """
464 Returns True if the dirstate contains a file (tracked or removed)
508 Returns True if the dirstate contains a file (tracked or removed)
465 in this directory.
509 in this directory.
466 """
510 """
467 return d in self._alldirs
511 return d in self._alldirs
468
512
469 @propertycache
513 @propertycache
470 def _dirs(self):
514 def _dirs(self):
471 return pathutil.dirs(self._map, only_tracked=True)
515 return pathutil.dirs(self._map, only_tracked=True)
472
516
473 @propertycache
517 @propertycache
474 def _alldirs(self):
518 def _alldirs(self):
475 return pathutil.dirs(self._map)
519 return pathutil.dirs(self._map)
476
520
477 ### code related to manipulation of entries and copy-sources
521 ### code related to manipulation of entries and copy-sources
478
522
479 def reset_state(
523 def reset_state(
480 self,
524 self,
481 filename,
525 filename,
482 wc_tracked=False,
526 wc_tracked=False,
483 p1_tracked=False,
527 p1_tracked=False,
484 p2_info=False,
528 p2_info=False,
485 has_meaningful_mtime=True,
529 has_meaningful_mtime=True,
486 parentfiledata=None,
530 parentfiledata=None,
487 ):
531 ):
488 """Set a entry to a given state, diregarding all previous state
532 """Set a entry to a given state, diregarding all previous state
489
533
490 This is to be used by the part of the dirstate API dedicated to
534 This is to be used by the part of the dirstate API dedicated to
491 adjusting the dirstate after a update/merge.
535 adjusting the dirstate after a update/merge.
492
536
493 note: calling this might result to no entry existing at all if the
537 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
538 dirstate map does not see any point at having one for this file
495 anymore.
539 anymore.
496 """
540 """
497 # copy information are now outdated
541 # copy information are now outdated
498 # (maybe new information should be in directly passed to this function)
542 # (maybe new information should be in directly passed to this function)
499 self.copymap.pop(filename, None)
543 self.copymap.pop(filename, None)
500
544
501 if not (p1_tracked or p2_info or wc_tracked):
545 if not (p1_tracked or p2_info or wc_tracked):
502 old_entry = self._map.get(filename)
546 old_entry = self._map.get(filename)
503 self._drop_entry(filename)
547 self._drop_entry(filename)
504 self._dirs_decr(filename, old_entry=old_entry)
548 self._dirs_decr(filename, old_entry=old_entry)
505 return
549 return
506
550
507 old_entry = self._map.get(filename)
551 old_entry = self._map.get(filename)
508 self._dirs_incr(filename, old_entry)
552 self._dirs_incr(filename, old_entry)
509 entry = DirstateItem(
553 entry = DirstateItem(
510 wc_tracked=wc_tracked,
554 wc_tracked=wc_tracked,
511 p1_tracked=p1_tracked,
555 p1_tracked=p1_tracked,
512 p2_info=p2_info,
556 p2_info=p2_info,
513 has_meaningful_mtime=has_meaningful_mtime,
557 has_meaningful_mtime=has_meaningful_mtime,
514 parentfiledata=parentfiledata,
558 parentfiledata=parentfiledata,
515 )
559 )
516 self._map[filename] = entry
560 self._map[filename] = entry
517
561
518 def set_tracked(self, filename):
562 def set_tracked(self, filename):
519 new = False
563 new = False
520 entry = self.get(filename)
564 entry = self.get(filename)
521 if entry is None:
565 if entry is None:
522 self._dirs_incr(filename)
566 self._dirs_incr(filename)
523 entry = DirstateItem(
567 entry = DirstateItem(
524 wc_tracked=True,
568 wc_tracked=True,
525 )
569 )
526
570
527 self._map[filename] = entry
571 self._map[filename] = entry
528 new = True
572 new = True
529 elif not entry.tracked:
573 elif not entry.tracked:
530 self._dirs_incr(filename, entry)
574 self._dirs_incr(filename, entry)
531 entry.set_tracked()
575 entry.set_tracked()
532 self._refresh_entry(filename, entry)
576 self._refresh_entry(filename, entry)
533 new = True
577 new = True
534 else:
578 else:
535 # XXX This is probably overkill for more case, but we need this to
579 # XXX This is probably overkill for more case, but we need this to
536 # fully replace the `normallookup` call with `set_tracked` one.
580 # fully replace the `normallookup` call with `set_tracked` one.
537 # Consider smoothing this in the future.
581 # Consider smoothing this in the future.
538 entry.set_possibly_dirty()
582 entry.set_possibly_dirty()
539 self._refresh_entry(filename, entry)
583 self._refresh_entry(filename, entry)
540 return new
584 return new
541
585
542 def set_untracked(self, f):
586 def set_untracked(self, f):
543 """Mark a file as no longer tracked in the dirstate map"""
587 """Mark a file as no longer tracked in the dirstate map"""
544 entry = self.get(f)
588 entry = self.get(f)
545 if entry is None:
589 if entry is None:
546 return False
590 return False
547 else:
591 else:
548 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
592 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
549 if not entry.p2_info:
593 if not entry.p2_info:
550 self.copymap.pop(f, None)
594 self.copymap.pop(f, None)
551 entry.set_untracked()
595 entry.set_untracked()
552 self._refresh_entry(f, entry)
596 self._refresh_entry(f, entry)
553 return True
597 return True
554
598
555 def set_clean(self, filename, mode, size, mtime):
599 def set_clean(self, filename, mode, size, mtime):
556 """mark a file as back to a clean state"""
600 """mark a file as back to a clean state"""
557 entry = self[filename]
601 entry = self[filename]
558 size = size & rangemask
602 size = size & rangemask
559 entry.set_clean(mode, size, mtime)
603 entry.set_clean(mode, size, mtime)
560 self._refresh_entry(filename, entry)
604 self._refresh_entry(filename, entry)
561 self.copymap.pop(filename, None)
605 self.copymap.pop(filename, None)
562
606
563 def set_possibly_dirty(self, filename):
607 def set_possibly_dirty(self, filename):
564 """record that the current state of the file on disk is unknown"""
608 """record that the current state of the file on disk is unknown"""
565 entry = self[filename]
609 entry = self[filename]
566 entry.set_possibly_dirty()
610 entry.set_possibly_dirty()
567 self._refresh_entry(filename, entry)
611 self._refresh_entry(filename, entry)
568
612
569 def _refresh_entry(self, f, entry):
613 def _refresh_entry(self, f, entry):
570 """record updated state of an entry"""
614 """record updated state of an entry"""
571 if not entry.any_tracked:
615 if not entry.any_tracked:
572 self._map.pop(f, None)
616 self._map.pop(f, None)
573
617
574 def _drop_entry(self, f):
618 def _drop_entry(self, f):
575 """remove any entry for file f
619 """remove any entry for file f
576
620
577 This should also drop associated copy information
621 This should also drop associated copy information
578
622
579 The fact we actually need to drop it is the responsability of the caller"""
623 The fact we actually need to drop it is the responsability of the caller"""
580 self._map.pop(f, None)
624 self._map.pop(f, None)
581 self.copymap.pop(f, None)
625 self.copymap.pop(f, None)
582
626
583
627
584 if rustmod is not None:
628 if rustmod is not None:
585
629
586 class dirstatemap(_dirstatemapcommon):
630 class dirstatemap(_dirstatemapcommon):
587
631
588 ### Core data storage and access
632 ### Core data storage and access
589
633
590 @propertycache
634 @propertycache
591 def _map(self):
635 def _map(self):
592 """
636 """
593 Fills the Dirstatemap when called.
637 Fills the Dirstatemap when called.
594 """
638 """
595 # ignore HG_PENDING because identity is used only for writing
639 # ignore HG_PENDING because identity is used only for writing
596 self._set_identity()
640 self._set_identity()
597
641
598 testing.wait_on_cfg(self._ui, b'dirstate.pre-read-file')
642 testing.wait_on_cfg(self._ui, b'dirstate.pre-read-file')
599 if self._use_dirstate_v2:
643 if self._use_dirstate_v2:
600 self.docket # load the data if needed
644 try:
645 self.docket
646 except error.CorruptedDirstate as e:
647 # fall back to dirstate-v1 if we fail to read v2
648 parents = self._v1_map(e)
649 else:
650 parents = self.docket.parents
601 inode = (
651 inode = (
602 self.identity.stat.st_ino
652 self.identity.stat.st_ino
603 if self.identity is not None
653 if self.identity is not None
604 and self.identity.stat is not None
654 and self.identity.stat is not None
605 else None
655 else None
606 )
656 )
607 testing.wait_on_cfg(self._ui, b'dirstate.post-docket-read-file')
657 testing.wait_on_cfg(
658 self._ui, b'dirstate.post-docket-read-file'
659 )
608 if not self.docket.uuid:
660 if not self.docket.uuid:
609 data = b''
661 data = b''
610 self._map = rustmod.DirstateMap.new_empty()
662 self._map = rustmod.DirstateMap.new_empty()
611 else:
663 else:
612 data = self._read_v2_data()
664 data = self._read_v2_data()
613 self._map = rustmod.DirstateMap.new_v2(
665 self._map = rustmod.DirstateMap.new_v2(
614 data,
666 data,
615 self.docket.data_size,
667 self.docket.data_size,
616 self.docket.tree_metadata,
668 self.docket.tree_metadata,
617 self.docket.uuid,
669 self.docket.uuid,
618 inode,
670 inode,
619 )
671 )
620 parents = self.docket.parents
672 parents = self.docket.parents
621 else:
673 else:
622 self._set_identity()
674 parents = self._v1_map()
623 inode = (
624 self.identity.stat.st_ino
625 if self.identity is not None
626 and self.identity.stat is not None
627 else None
628 )
629 self._map, parents = rustmod.DirstateMap.new_v1(
630 self._readdirstatefile(), inode
631 )
632
675
633 if parents and not self._dirtyparents:
676 if parents and not self._dirtyparents:
634 self.setparents(*parents)
677 self.setparents(*parents)
635
678
636 self.__contains__ = self._map.__contains__
679 self.__contains__ = self._map.__contains__
637 self.__getitem__ = self._map.__getitem__
680 self.__getitem__ = self._map.__getitem__
638 self.get = self._map.get
681 self.get = self._map.get
639 return self._map
682 return self._map
640
683
684 def _v1_map(self, from_v2_exception=None):
685 self._set_identity()
686 inode = (
687 self.identity.stat.st_ino
688 if self.identity is not None and self.identity.stat is not None
689 else None
690 )
691 try:
692 self._map, parents = rustmod.DirstateMap.new_v1(
693 self._readdirstatefile(), inode
694 )
695 except OSError as e:
696 if from_v2_exception is not None:
697 raise e from from_v2_exception
698 raise
699 return parents
700
641 @property
701 @property
642 def copymap(self):
702 def copymap(self):
643 return self._map.copymap()
703 return self._map.copymap()
644
704
645 def debug_iter(self, all):
705 def debug_iter(self, all):
646 """
706 """
647 Return an iterator of (filename, state, mode, size, mtime) tuples
707 Return an iterator of (filename, state, mode, size, mtime) tuples
648
708
649 `all`: also include with `state == b' '` dirstate tree nodes that
709 `all`: also include with `state == b' '` dirstate tree nodes that
650 don't have an associated `DirstateItem`.
710 don't have an associated `DirstateItem`.
651
711
652 """
712 """
653 return self._map.debug_iter(all)
713 return self._map.debug_iter(all)
654
714
655 def clear(self):
715 def clear(self):
656 self._map.clear()
716 self._map.clear()
657 self.setparents(
717 self.setparents(
658 self._nodeconstants.nullid, self._nodeconstants.nullid
718 self._nodeconstants.nullid, self._nodeconstants.nullid
659 )
719 )
660 util.clearcachedproperty(self, b"_dirs")
720 util.clearcachedproperty(self, b"_dirs")
661 util.clearcachedproperty(self, b"_alldirs")
721 util.clearcachedproperty(self, b"_alldirs")
662 util.clearcachedproperty(self, b"dirfoldmap")
722 util.clearcachedproperty(self, b"dirfoldmap")
663
723
664 def items(self):
724 def items(self):
665 return self._map.items()
725 return self._map.items()
666
726
667 # forward for python2,3 compat
727 # forward for python2,3 compat
668 iteritems = items
728 iteritems = items
669
729
670 def keys(self):
730 def keys(self):
671 return iter(self._map)
731 return iter(self._map)
672
732
673 ### reading/setting parents
733 ### reading/setting parents
674
734
675 def setparents(self, p1, p2, fold_p2=False):
735 def setparents(self, p1, p2, fold_p2=False):
676 self._parents = (p1, p2)
736 self._parents = (p1, p2)
677 self._dirtyparents = True
737 self._dirtyparents = True
678 copies = {}
738 copies = {}
679 if fold_p2:
739 if fold_p2:
680 copies = self._map.setparents_fixup()
740 copies = self._map.setparents_fixup()
681 return copies
741 return copies
682
742
683 ### disk interaction
743 ### disk interaction
684
744
685 @propertycache
745 @propertycache
686 def identity(self):
746 def identity(self):
687 self._map
747 self._map
688 return self.identity
748 return self.identity
689
749
690 def write(self, tr, st):
750 def write(self, tr, st):
691 if not self._use_dirstate_v2:
751 if not self._use_dirstate_v2:
692 p1, p2 = self.parents()
752 p1, p2 = self.parents()
693 packed = self._map.write_v1(p1, p2)
753 packed = self._map.write_v1(p1, p2)
694 st.write(packed)
754 st.write(packed)
695 st.close()
755 st.close()
696 self._dirtyparents = False
756 self._dirtyparents = False
697 return
757 return
698
758
759 write_mode = self._write_mode
760 try:
761 docket = self.docket
762 except error.CorruptedDirstate:
763 # fall back to dirstate-v1 if we fail to parse v2
764 docket = None
765
699 # We can only append to an existing data file if there is one
766 # We can only append to an existing data file if there is one
700 write_mode = self._write_mode
767 if docket is None or docket.uuid is None:
701 if self.docket.uuid is None:
702 write_mode = WRITE_MODE_FORCE_NEW
768 write_mode = WRITE_MODE_FORCE_NEW
703 packed, meta, append = self._map.write_v2(write_mode)
769 packed, meta, append = self._map.write_v2(write_mode)
704 if append:
770 if append:
705 docket = self.docket
771 docket = self.docket
706 data_filename = docket.data_filename()
772 data_filename = docket.data_filename()
707 # We mark it for backup to make sure a future `hg rollback` (or
773 # 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
774 # `hg recover`?) call find the data it needs to restore a
709 # working repository.
775 # working repository.
710 #
776 #
711 # The backup can use a hardlink because the format is resistant
777 # The backup can use a hardlink because the format is resistant
712 # to trailing "dead" data.
778 # to trailing "dead" data.
713 if tr is not None:
779 if tr is not None:
714 tr.addbackup(data_filename, location=b'plain')
780 tr.addbackup(data_filename, location=b'plain')
715 with self._opener(data_filename, b'r+b') as fp:
781 with self._opener(data_filename, b'r+b') as fp:
716 fp.seek(docket.data_size)
782 fp.seek(docket.data_size)
717 assert fp.tell() == docket.data_size
783 assert fp.tell() == docket.data_size
718 written = fp.write(packed)
784 written = fp.write(packed)
719 if written is not None: # py2 may return None
785 if written is not None: # py2 may return None
720 assert written == len(packed), (written, len(packed))
786 assert written == len(packed), (written, len(packed))
721 docket.data_size += len(packed)
787 docket.data_size += len(packed)
722 docket.parents = self.parents()
788 docket.parents = self.parents()
723 docket.tree_metadata = meta
789 docket.tree_metadata = meta
724 st.write(docket.serialize())
790 st.write(docket.serialize())
725 st.close()
791 st.close()
726 else:
792 else:
727 self.write_v2_no_append(tr, st, meta, packed)
793 self.write_v2_no_append(tr, st, meta, packed)
728 # Reload from the newly-written file
794 # Reload from the newly-written file
729 util.clearcachedproperty(self, b"_map")
795 util.clearcachedproperty(self, b"_map")
730 self._dirtyparents = False
796 self._dirtyparents = False
731
797
732 ### code related to maintaining and accessing "extra" property
798 ### code related to maintaining and accessing "extra" property
733 # (e.g. "has_dir")
799 # (e.g. "has_dir")
734
800
735 @propertycache
801 @propertycache
736 def filefoldmap(self):
802 def filefoldmap(self):
737 """Returns a dictionary mapping normalized case paths to their
803 """Returns a dictionary mapping normalized case paths to their
738 non-normalized versions.
804 non-normalized versions.
739 """
805 """
740 return self._map.filefoldmapasdict()
806 return self._map.filefoldmapasdict()
741
807
742 def hastrackeddir(self, d):
808 def hastrackeddir(self, d):
743 return self._map.hastrackeddir(d)
809 return self._map.hastrackeddir(d)
744
810
745 def hasdir(self, d):
811 def hasdir(self, d):
746 return self._map.hasdir(d)
812 return self._map.hasdir(d)
747
813
748 @propertycache
814 @propertycache
749 def dirfoldmap(self):
815 def dirfoldmap(self):
750 f = {}
816 f = {}
751 normcase = util.normcase
817 normcase = util.normcase
752 for name in self._map.tracked_dirs():
818 for name in self._map.tracked_dirs():
753 f[normcase(name)] = name
819 f[normcase(name)] = name
754 return f
820 return f
755
821
756 ### code related to manipulation of entries and copy-sources
822 ### code related to manipulation of entries and copy-sources
757
823
758 def set_tracked(self, f):
824 def set_tracked(self, f):
759 return self._map.set_tracked(f)
825 return self._map.set_tracked(f)
760
826
761 def set_untracked(self, f):
827 def set_untracked(self, f):
762 return self._map.set_untracked(f)
828 return self._map.set_untracked(f)
763
829
764 def set_clean(self, filename, mode, size, mtime):
830 def set_clean(self, filename, mode, size, mtime):
765 self._map.set_clean(filename, mode, size, mtime)
831 self._map.set_clean(filename, mode, size, mtime)
766
832
767 def set_possibly_dirty(self, f):
833 def set_possibly_dirty(self, f):
768 self._map.set_possibly_dirty(f)
834 self._map.set_possibly_dirty(f)
769
835
770 def reset_state(
836 def reset_state(
771 self,
837 self,
772 filename,
838 filename,
773 wc_tracked=False,
839 wc_tracked=False,
774 p1_tracked=False,
840 p1_tracked=False,
775 p2_info=False,
841 p2_info=False,
776 has_meaningful_mtime=True,
842 has_meaningful_mtime=True,
777 parentfiledata=None,
843 parentfiledata=None,
778 ):
844 ):
779 return self._map.reset_state(
845 return self._map.reset_state(
780 filename,
846 filename,
781 wc_tracked,
847 wc_tracked,
782 p1_tracked,
848 p1_tracked,
783 p2_info,
849 p2_info,
784 has_meaningful_mtime,
850 has_meaningful_mtime,
785 parentfiledata,
851 parentfiledata,
786 )
852 )
@@ -1,674 +1,681 b''
1 # error.py - Mercurial exceptions
1 # error.py - Mercurial exceptions
2 #
2 #
3 # Copyright 2005-2008 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2008 Olivia Mackall <olivia@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Mercurial exceptions.
8 """Mercurial exceptions.
9
9
10 This allows us to catch exceptions at higher levels without forcing
10 This allows us to catch exceptions at higher levels without forcing
11 imports.
11 imports.
12 """
12 """
13
13
14
14
15 import difflib
15 import difflib
16
16
17 # Do not import anything but pycompat here, please
17 # Do not import anything but pycompat here, please
18 from . import pycompat
18 from . import pycompat
19
19
20 if pycompat.TYPE_CHECKING:
20 if pycompat.TYPE_CHECKING:
21 from typing import (
21 from typing import (
22 Any,
22 Any,
23 AnyStr,
23 AnyStr,
24 Iterable,
24 Iterable,
25 List,
25 List,
26 Optional,
26 Optional,
27 Sequence,
27 Sequence,
28 Union,
28 Union,
29 )
29 )
30
30
31
31
32 def _tobytes(exc):
32 def _tobytes(exc):
33 # type: (...) -> bytes
33 # type: (...) -> bytes
34 """Byte-stringify exception in the same way as BaseException_str()"""
34 """Byte-stringify exception in the same way as BaseException_str()"""
35 if not exc.args:
35 if not exc.args:
36 return b''
36 return b''
37 if len(exc.args) == 1:
37 if len(exc.args) == 1:
38 return pycompat.bytestr(exc.args[0])
38 return pycompat.bytestr(exc.args[0])
39 return b'(%s)' % b', '.join(b"'%s'" % pycompat.bytestr(a) for a in exc.args)
39 return b'(%s)' % b', '.join(b"'%s'" % pycompat.bytestr(a) for a in exc.args)
40
40
41
41
42 class Hint:
42 class Hint:
43 """Mix-in to provide a hint of an error
43 """Mix-in to provide a hint of an error
44
44
45 This should come first in the inheritance list to consume a hint and
45 This should come first in the inheritance list to consume a hint and
46 pass remaining arguments to the exception class.
46 pass remaining arguments to the exception class.
47 """
47 """
48
48
49 def __init__(self, *args, **kw):
49 def __init__(self, *args, **kw):
50 self.hint = kw.pop('hint', None) # type: Optional[bytes]
50 self.hint = kw.pop('hint', None) # type: Optional[bytes]
51 super(Hint, self).__init__(*args, **kw)
51 super(Hint, self).__init__(*args, **kw)
52
52
53
53
54 class Error(Hint, Exception):
54 class Error(Hint, Exception):
55 """Base class for Mercurial errors."""
55 """Base class for Mercurial errors."""
56
56
57 coarse_exit_code = None
57 coarse_exit_code = None
58 detailed_exit_code = None
58 detailed_exit_code = None
59
59
60 def __init__(self, message, hint=None):
60 def __init__(self, message, hint=None):
61 # type: (bytes, Optional[bytes]) -> None
61 # type: (bytes, Optional[bytes]) -> None
62 self.message = message
62 self.message = message
63 self.hint = hint
63 self.hint = hint
64 # Pass the message into the Exception constructor to help extensions
64 # Pass the message into the Exception constructor to help extensions
65 # that look for exc.args[0].
65 # that look for exc.args[0].
66 Exception.__init__(self, message)
66 Exception.__init__(self, message)
67
67
68 def __bytes__(self):
68 def __bytes__(self):
69 return self.message
69 return self.message
70
70
71 def __str__(self):
71 def __str__(self):
72 # type: () -> str
72 # type: () -> str
73 # the output would be unreadable if the message was translated,
73 # the output would be unreadable if the message was translated,
74 # but do not replace it with encoding.strfromlocal(), which
74 # but do not replace it with encoding.strfromlocal(), which
75 # may raise another exception.
75 # may raise another exception.
76 return pycompat.sysstr(self.__bytes__())
76 return pycompat.sysstr(self.__bytes__())
77
77
78 def format(self):
78 def format(self):
79 # type: () -> bytes
79 # type: () -> bytes
80 from .i18n import _
80 from .i18n import _
81
81
82 message = _(b"abort: %s\n") % self.message
82 message = _(b"abort: %s\n") % self.message
83 if self.hint:
83 if self.hint:
84 message += _(b"(%s)\n") % self.hint
84 message += _(b"(%s)\n") % self.hint
85 return message
85 return message
86
86
87
87
88 class Abort(Error):
88 class Abort(Error):
89 """Raised if a command needs to print an error and exit."""
89 """Raised if a command needs to print an error and exit."""
90
90
91
91
92 class StorageError(Error):
92 class StorageError(Error):
93 """Raised when an error occurs in a storage layer.
93 """Raised when an error occurs in a storage layer.
94
94
95 Usually subclassed by a storage-specific exception.
95 Usually subclassed by a storage-specific exception.
96 """
96 """
97
97
98 detailed_exit_code = 50
98 detailed_exit_code = 50
99
99
100
100
101 class RevlogError(StorageError):
101 class RevlogError(StorageError):
102 pass
102 pass
103
103
104
104
105 class SidedataHashError(RevlogError):
105 class SidedataHashError(RevlogError):
106 def __init__(self, key, expected, got):
106 def __init__(self, key, expected, got):
107 # type: (int, bytes, bytes) -> None
107 # type: (int, bytes, bytes) -> None
108 self.hint = None
108 self.hint = None
109 self.sidedatakey = key
109 self.sidedatakey = key
110 self.expecteddigest = expected
110 self.expecteddigest = expected
111 self.actualdigest = got
111 self.actualdigest = got
112
112
113
113
114 class FilteredIndexError(IndexError):
114 class FilteredIndexError(IndexError):
115 __bytes__ = _tobytes
115 __bytes__ = _tobytes
116
116
117
117
118 class LookupError(RevlogError, KeyError):
118 class LookupError(RevlogError, KeyError):
119 def __init__(self, name, index, message):
119 def __init__(self, name, index, message):
120 # type: (bytes, bytes, bytes) -> None
120 # type: (bytes, bytes, bytes) -> None
121 self.name = name
121 self.name = name
122 self.index = index
122 self.index = index
123 # this can't be called 'message' because at least some installs of
123 # this can't be called 'message' because at least some installs of
124 # Python 2.6+ complain about the 'message' property being deprecated
124 # Python 2.6+ complain about the 'message' property being deprecated
125 self.lookupmessage = message
125 self.lookupmessage = message
126 if isinstance(name, bytes) and len(name) == 20:
126 if isinstance(name, bytes) and len(name) == 20:
127 from .node import hex
127 from .node import hex
128
128
129 name = hex(name)
129 name = hex(name)
130 # if name is a binary node, it can be None
130 # if name is a binary node, it can be None
131 RevlogError.__init__(
131 RevlogError.__init__(
132 self, b'%s@%s: %s' % (index, pycompat.bytestr(name), message)
132 self, b'%s@%s: %s' % (index, pycompat.bytestr(name), message)
133 )
133 )
134
134
135 def __bytes__(self):
135 def __bytes__(self):
136 return RevlogError.__bytes__(self)
136 return RevlogError.__bytes__(self)
137
137
138 def __str__(self):
138 def __str__(self):
139 return RevlogError.__str__(self)
139 return RevlogError.__str__(self)
140
140
141
141
142 class AmbiguousPrefixLookupError(LookupError):
142 class AmbiguousPrefixLookupError(LookupError):
143 pass
143 pass
144
144
145
145
146 class FilteredLookupError(LookupError):
146 class FilteredLookupError(LookupError):
147 pass
147 pass
148
148
149
149
150 class ManifestLookupError(LookupError):
150 class ManifestLookupError(LookupError):
151 pass
151 pass
152
152
153
153
154 class CommandError(Exception):
154 class CommandError(Exception):
155 """Exception raised on errors in parsing the command line."""
155 """Exception raised on errors in parsing the command line."""
156
156
157 def __init__(self, command, message):
157 def __init__(self, command, message):
158 # type: (Optional[bytes], bytes) -> None
158 # type: (Optional[bytes], bytes) -> None
159 self.command = command
159 self.command = command
160 self.message = message
160 self.message = message
161 super(CommandError, self).__init__()
161 super(CommandError, self).__init__()
162
162
163 __bytes__ = _tobytes
163 __bytes__ = _tobytes
164
164
165
165
166 class UnknownCommand(Exception):
166 class UnknownCommand(Exception):
167 """Exception raised if command is not in the command table."""
167 """Exception raised if command is not in the command table."""
168
168
169 def __init__(self, command, all_commands=None):
169 def __init__(self, command, all_commands=None):
170 # type: (bytes, Optional[List[bytes]]) -> None
170 # type: (bytes, Optional[List[bytes]]) -> None
171 self.command = command
171 self.command = command
172 self.all_commands = all_commands
172 self.all_commands = all_commands
173 super(UnknownCommand, self).__init__()
173 super(UnknownCommand, self).__init__()
174
174
175 __bytes__ = _tobytes
175 __bytes__ = _tobytes
176
176
177
177
178 class AmbiguousCommand(Exception):
178 class AmbiguousCommand(Exception):
179 """Exception raised if command shortcut matches more than one command."""
179 """Exception raised if command shortcut matches more than one command."""
180
180
181 def __init__(self, prefix, matches):
181 def __init__(self, prefix, matches):
182 # type: (bytes, List[bytes]) -> None
182 # type: (bytes, List[bytes]) -> None
183 self.prefix = prefix
183 self.prefix = prefix
184 self.matches = matches
184 self.matches = matches
185 super(AmbiguousCommand, self).__init__()
185 super(AmbiguousCommand, self).__init__()
186
186
187 __bytes__ = _tobytes
187 __bytes__ = _tobytes
188
188
189
189
190 class WorkerError(Exception):
190 class WorkerError(Exception):
191 """Exception raised when a worker process dies."""
191 """Exception raised when a worker process dies."""
192
192
193 def __init__(self, status_code):
193 def __init__(self, status_code):
194 # type: (int) -> None
194 # type: (int) -> None
195 self.status_code = status_code
195 self.status_code = status_code
196 # Pass status code to superclass just so it becomes part of __bytes__
196 # Pass status code to superclass just so it becomes part of __bytes__
197 super(WorkerError, self).__init__(status_code)
197 super(WorkerError, self).__init__(status_code)
198
198
199 __bytes__ = _tobytes
199 __bytes__ = _tobytes
200
200
201
201
202 class InterventionRequired(Abort):
202 class InterventionRequired(Abort):
203 """Exception raised when a command requires human intervention."""
203 """Exception raised when a command requires human intervention."""
204
204
205 coarse_exit_code = 1
205 coarse_exit_code = 1
206 detailed_exit_code = 240
206 detailed_exit_code = 240
207
207
208 def format(self):
208 def format(self):
209 # type: () -> bytes
209 # type: () -> bytes
210 from .i18n import _
210 from .i18n import _
211
211
212 message = _(b"%s\n") % self.message
212 message = _(b"%s\n") % self.message
213 if self.hint:
213 if self.hint:
214 message += _(b"(%s)\n") % self.hint
214 message += _(b"(%s)\n") % self.hint
215 return message
215 return message
216
216
217
217
218 class ConflictResolutionRequired(InterventionRequired):
218 class ConflictResolutionRequired(InterventionRequired):
219 """Exception raised when a continuable command required merge conflict resolution."""
219 """Exception raised when a continuable command required merge conflict resolution."""
220
220
221 def __init__(self, opname):
221 def __init__(self, opname):
222 # type: (bytes) -> None
222 # type: (bytes) -> None
223 from .i18n import _
223 from .i18n import _
224
224
225 self.opname = opname
225 self.opname = opname
226 InterventionRequired.__init__(
226 InterventionRequired.__init__(
227 self,
227 self,
228 _(
228 _(
229 b"unresolved conflicts (see 'hg resolve', then 'hg %s --continue')"
229 b"unresolved conflicts (see 'hg resolve', then 'hg %s --continue')"
230 )
230 )
231 % opname,
231 % opname,
232 )
232 )
233
233
234
234
235 class InputError(Abort):
235 class InputError(Abort):
236 """Indicates that the user made an error in their input.
236 """Indicates that the user made an error in their input.
237
237
238 Examples: Invalid command, invalid flags, invalid revision.
238 Examples: Invalid command, invalid flags, invalid revision.
239 """
239 """
240
240
241 detailed_exit_code = 10
241 detailed_exit_code = 10
242
242
243
243
244 class StateError(Abort):
244 class StateError(Abort):
245 """Indicates that the operation might work if retried in a different state.
245 """Indicates that the operation might work if retried in a different state.
246
246
247 Examples: Unresolved merge conflicts, unfinished operations.
247 Examples: Unresolved merge conflicts, unfinished operations.
248 """
248 """
249
249
250 detailed_exit_code = 20
250 detailed_exit_code = 20
251
251
252
252
253 class CanceledError(Abort):
253 class CanceledError(Abort):
254 """Indicates that the user canceled the operation.
254 """Indicates that the user canceled the operation.
255
255
256 Examples: Close commit editor with error status, quit chistedit.
256 Examples: Close commit editor with error status, quit chistedit.
257 """
257 """
258
258
259 detailed_exit_code = 250
259 detailed_exit_code = 250
260
260
261
261
262 class SecurityError(Abort):
262 class SecurityError(Abort):
263 """Indicates that some aspect of security failed.
263 """Indicates that some aspect of security failed.
264
264
265 Examples: Bad server credentials, expired local credentials for network
265 Examples: Bad server credentials, expired local credentials for network
266 filesystem, mismatched GPG signature, DoS protection.
266 filesystem, mismatched GPG signature, DoS protection.
267 """
267 """
268
268
269 detailed_exit_code = 150
269 detailed_exit_code = 150
270
270
271
271
272 class HookLoadError(Abort):
272 class HookLoadError(Abort):
273 """raised when loading a hook fails, aborting an operation
273 """raised when loading a hook fails, aborting an operation
274
274
275 Exists to allow more specialized catching."""
275 Exists to allow more specialized catching."""
276
276
277
277
278 class HookAbort(Abort):
278 class HookAbort(Abort):
279 """raised when a validation hook fails, aborting an operation
279 """raised when a validation hook fails, aborting an operation
280
280
281 Exists to allow more specialized catching."""
281 Exists to allow more specialized catching."""
282
282
283 detailed_exit_code = 40
283 detailed_exit_code = 40
284
284
285
285
286 class ConfigError(Abort):
286 class ConfigError(Abort):
287 """Exception raised when parsing config files"""
287 """Exception raised when parsing config files"""
288
288
289 detailed_exit_code = 30
289 detailed_exit_code = 30
290
290
291 def __init__(self, message, location=None, hint=None):
291 def __init__(self, message, location=None, hint=None):
292 # type: (bytes, Optional[bytes], Optional[bytes]) -> None
292 # type: (bytes, Optional[bytes], Optional[bytes]) -> None
293 super(ConfigError, self).__init__(message, hint=hint)
293 super(ConfigError, self).__init__(message, hint=hint)
294 self.location = location
294 self.location = location
295
295
296 def format(self):
296 def format(self):
297 # type: () -> bytes
297 # type: () -> bytes
298 from .i18n import _
298 from .i18n import _
299
299
300 if self.location is not None:
300 if self.location is not None:
301 message = _(b"config error at %s: %s\n") % (
301 message = _(b"config error at %s: %s\n") % (
302 pycompat.bytestr(self.location),
302 pycompat.bytestr(self.location),
303 self.message,
303 self.message,
304 )
304 )
305 else:
305 else:
306 message = _(b"config error: %s\n") % self.message
306 message = _(b"config error: %s\n") % self.message
307 if self.hint:
307 if self.hint:
308 message += _(b"(%s)\n") % self.hint
308 message += _(b"(%s)\n") % self.hint
309 return message
309 return message
310
310
311
311
312 class UpdateAbort(Abort):
312 class UpdateAbort(Abort):
313 """Raised when an update is aborted for destination issue"""
313 """Raised when an update is aborted for destination issue"""
314
314
315
315
316 class MergeDestAbort(Abort):
316 class MergeDestAbort(Abort):
317 """Raised when an update is aborted for destination issues"""
317 """Raised when an update is aborted for destination issues"""
318
318
319
319
320 class NoMergeDestAbort(MergeDestAbort):
320 class NoMergeDestAbort(MergeDestAbort):
321 """Raised when an update is aborted because there is nothing to merge"""
321 """Raised when an update is aborted because there is nothing to merge"""
322
322
323
323
324 class ManyMergeDestAbort(MergeDestAbort):
324 class ManyMergeDestAbort(MergeDestAbort):
325 """Raised when an update is aborted because destination is ambiguous"""
325 """Raised when an update is aborted because destination is ambiguous"""
326
326
327
327
328 class ResponseExpected(Abort):
328 class ResponseExpected(Abort):
329 """Raised when an EOF is received for a prompt"""
329 """Raised when an EOF is received for a prompt"""
330
330
331 def __init__(self):
331 def __init__(self):
332 from .i18n import _
332 from .i18n import _
333
333
334 Abort.__init__(self, _(b'response expected'))
334 Abort.__init__(self, _(b'response expected'))
335
335
336
336
337 class RemoteError(Abort):
337 class RemoteError(Abort):
338 """Exception raised when interacting with a remote repo fails"""
338 """Exception raised when interacting with a remote repo fails"""
339
339
340 detailed_exit_code = 100
340 detailed_exit_code = 100
341
341
342
342
343 class OutOfBandError(RemoteError):
343 class OutOfBandError(RemoteError):
344 """Exception raised when a remote repo reports failure"""
344 """Exception raised when a remote repo reports failure"""
345
345
346 def __init__(self, message=None, hint=None):
346 def __init__(self, message=None, hint=None):
347 # type: (Optional[bytes], Optional[bytes]) -> None
347 # type: (Optional[bytes], Optional[bytes]) -> None
348 from .i18n import _
348 from .i18n import _
349
349
350 if message:
350 if message:
351 # Abort.format() adds a trailing newline
351 # Abort.format() adds a trailing newline
352 message = _(b"remote error:\n%s") % message.rstrip(b'\n')
352 message = _(b"remote error:\n%s") % message.rstrip(b'\n')
353 else:
353 else:
354 message = _(b"remote error")
354 message = _(b"remote error")
355 super(OutOfBandError, self).__init__(message, hint=hint)
355 super(OutOfBandError, self).__init__(message, hint=hint)
356
356
357
357
358 class ParseError(Abort):
358 class ParseError(Abort):
359 """Raised when parsing config files and {rev,file}sets (msg[, pos])"""
359 """Raised when parsing config files and {rev,file}sets (msg[, pos])"""
360
360
361 detailed_exit_code = 10
361 detailed_exit_code = 10
362
362
363 def __init__(self, message, location=None, hint=None):
363 def __init__(self, message, location=None, hint=None):
364 # type: (bytes, Optional[Union[bytes, int]], Optional[bytes]) -> None
364 # type: (bytes, Optional[Union[bytes, int]], Optional[bytes]) -> None
365 super(ParseError, self).__init__(message, hint=hint)
365 super(ParseError, self).__init__(message, hint=hint)
366 self.location = location
366 self.location = location
367
367
368 def format(self):
368 def format(self):
369 # type: () -> bytes
369 # type: () -> bytes
370 from .i18n import _
370 from .i18n import _
371
371
372 if self.location is not None:
372 if self.location is not None:
373 message = _(b"hg: parse error at %s: %s\n") % (
373 message = _(b"hg: parse error at %s: %s\n") % (
374 pycompat.bytestr(self.location),
374 pycompat.bytestr(self.location),
375 self.message,
375 self.message,
376 )
376 )
377 else:
377 else:
378 message = _(b"hg: parse error: %s\n") % self.message
378 message = _(b"hg: parse error: %s\n") % self.message
379 if self.hint:
379 if self.hint:
380 message += _(b"(%s)\n") % self.hint
380 message += _(b"(%s)\n") % self.hint
381 return message
381 return message
382
382
383
383
384 class PatchError(Exception):
384 class PatchError(Exception):
385 __bytes__ = _tobytes
385 __bytes__ = _tobytes
386
386
387
387
388 class PatchParseError(PatchError):
388 class PatchParseError(PatchError):
389 __bytes__ = _tobytes
389 __bytes__ = _tobytes
390
390
391
391
392 class PatchApplicationError(PatchError):
392 class PatchApplicationError(PatchError):
393 __bytes__ = _tobytes
393 __bytes__ = _tobytes
394
394
395
395
396 def getsimilar(symbols, value):
396 def getsimilar(symbols, value):
397 # type: (Iterable[bytes], bytes) -> List[bytes]
397 # type: (Iterable[bytes], bytes) -> List[bytes]
398 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
398 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
399 # The cutoff for similarity here is pretty arbitrary. It should
399 # The cutoff for similarity here is pretty arbitrary. It should
400 # probably be investigated and tweaked.
400 # probably be investigated and tweaked.
401 return [s for s in symbols if sim(s) > 0.6]
401 return [s for s in symbols if sim(s) > 0.6]
402
402
403
403
404 def similarity_hint(similar):
404 def similarity_hint(similar):
405 # type: (List[bytes]) -> Optional[bytes]
405 # type: (List[bytes]) -> Optional[bytes]
406 from .i18n import _
406 from .i18n import _
407
407
408 if len(similar) == 1:
408 if len(similar) == 1:
409 return _(b"did you mean %s?") % similar[0]
409 return _(b"did you mean %s?") % similar[0]
410 elif similar:
410 elif similar:
411 ss = b", ".join(sorted(similar))
411 ss = b", ".join(sorted(similar))
412 return _(b"did you mean one of %s?") % ss
412 return _(b"did you mean one of %s?") % ss
413 else:
413 else:
414 return None
414 return None
415
415
416
416
417 class UnknownIdentifier(ParseError):
417 class UnknownIdentifier(ParseError):
418 """Exception raised when a {rev,file}set references an unknown identifier"""
418 """Exception raised when a {rev,file}set references an unknown identifier"""
419
419
420 def __init__(self, function, symbols):
420 def __init__(self, function, symbols):
421 # type: (bytes, Iterable[bytes]) -> None
421 # type: (bytes, Iterable[bytes]) -> None
422 from .i18n import _
422 from .i18n import _
423
423
424 similar = getsimilar(symbols, function)
424 similar = getsimilar(symbols, function)
425 hint = similarity_hint(similar)
425 hint = similarity_hint(similar)
426
426
427 ParseError.__init__(
427 ParseError.__init__(
428 self, _(b"unknown identifier: %s") % function, hint=hint
428 self, _(b"unknown identifier: %s") % function, hint=hint
429 )
429 )
430
430
431
431
432 class RepoError(Hint, Exception):
432 class RepoError(Hint, Exception):
433 __bytes__ = _tobytes
433 __bytes__ = _tobytes
434
434
435
435
436 class RepoLookupError(RepoError):
436 class RepoLookupError(RepoError):
437 pass
437 pass
438
438
439
439
440 class FilteredRepoLookupError(RepoLookupError):
440 class FilteredRepoLookupError(RepoLookupError):
441 pass
441 pass
442
442
443
443
444 class CapabilityError(RepoError):
444 class CapabilityError(RepoError):
445 pass
445 pass
446
446
447
447
448 class RequirementError(RepoError):
448 class RequirementError(RepoError):
449 """Exception raised if .hg/requires has an unknown entry."""
449 """Exception raised if .hg/requires has an unknown entry."""
450
450
451
451
452 class StdioError(IOError):
452 class StdioError(IOError):
453 """Raised if I/O to stdout or stderr fails"""
453 """Raised if I/O to stdout or stderr fails"""
454
454
455 def __init__(self, err):
455 def __init__(self, err):
456 # type: (IOError) -> None
456 # type: (IOError) -> None
457 IOError.__init__(self, err.errno, err.strerror)
457 IOError.__init__(self, err.errno, err.strerror)
458
458
459 # no __bytes__() because error message is derived from the standard IOError
459 # no __bytes__() because error message is derived from the standard IOError
460
460
461
461
462 class UnsupportedMergeRecords(Abort):
462 class UnsupportedMergeRecords(Abort):
463 def __init__(self, recordtypes):
463 def __init__(self, recordtypes):
464 # type: (Iterable[bytes]) -> None
464 # type: (Iterable[bytes]) -> None
465 from .i18n import _
465 from .i18n import _
466
466
467 self.recordtypes = sorted(recordtypes)
467 self.recordtypes = sorted(recordtypes)
468 s = b' '.join(self.recordtypes)
468 s = b' '.join(self.recordtypes)
469 Abort.__init__(
469 Abort.__init__(
470 self,
470 self,
471 _(b'unsupported merge state records: %s') % s,
471 _(b'unsupported merge state records: %s') % s,
472 hint=_(
472 hint=_(
473 b'see https://mercurial-scm.org/wiki/MergeStateRecords for '
473 b'see https://mercurial-scm.org/wiki/MergeStateRecords for '
474 b'more information'
474 b'more information'
475 ),
475 ),
476 )
476 )
477
477
478
478
479 class UnknownVersion(Abort):
479 class UnknownVersion(Abort):
480 """generic exception for aborting from an encounter with an unknown version"""
480 """generic exception for aborting from an encounter with an unknown version"""
481
481
482 def __init__(self, msg, hint=None, version=None):
482 def __init__(self, msg, hint=None, version=None):
483 # type: (bytes, Optional[bytes], Optional[bytes]) -> None
483 # type: (bytes, Optional[bytes], Optional[bytes]) -> None
484 self.version = version
484 self.version = version
485 super(UnknownVersion, self).__init__(msg, hint=hint)
485 super(UnknownVersion, self).__init__(msg, hint=hint)
486
486
487
487
488 class LockError(IOError):
488 class LockError(IOError):
489 def __init__(self, errno, strerror, filename, desc):
489 def __init__(self, errno, strerror, filename, desc):
490 # TODO: figure out if this should be bytes or str
490 # TODO: figure out if this should be bytes or str
491 # _type: (int, str, str, bytes) -> None
491 # _type: (int, str, str, bytes) -> None
492 IOError.__init__(self, errno, strerror, filename)
492 IOError.__init__(self, errno, strerror, filename)
493 self.desc = desc
493 self.desc = desc
494
494
495 # no __bytes__() because error message is derived from the standard IOError
495 # no __bytes__() because error message is derived from the standard IOError
496
496
497
497
498 class LockHeld(LockError):
498 class LockHeld(LockError):
499 def __init__(self, errno, filename, desc, locker):
499 def __init__(self, errno, filename, desc, locker):
500 LockError.__init__(self, errno, b'Lock held', filename, desc)
500 LockError.__init__(self, errno, b'Lock held', filename, desc)
501 self.locker = locker
501 self.locker = locker
502
502
503
503
504 class LockUnavailable(LockError):
504 class LockUnavailable(LockError):
505 pass
505 pass
506
506
507
507
508 # LockError is for errors while acquiring the lock -- this is unrelated
508 # LockError is for errors while acquiring the lock -- this is unrelated
509 class LockInheritanceContractViolation(RuntimeError):
509 class LockInheritanceContractViolation(RuntimeError):
510 __bytes__ = _tobytes
510 __bytes__ = _tobytes
511
511
512
512
513 class ResponseError(Exception):
513 class ResponseError(Exception):
514 """Raised to print an error with part of output and exit."""
514 """Raised to print an error with part of output and exit."""
515
515
516 __bytes__ = _tobytes
516 __bytes__ = _tobytes
517
517
518
518
519 # derived from KeyboardInterrupt to simplify some breakout code
519 # derived from KeyboardInterrupt to simplify some breakout code
520 class SignalInterrupt(KeyboardInterrupt):
520 class SignalInterrupt(KeyboardInterrupt):
521 """Exception raised on SIGTERM and SIGHUP."""
521 """Exception raised on SIGTERM and SIGHUP."""
522
522
523
523
524 class SignatureError(Exception):
524 class SignatureError(Exception):
525 __bytes__ = _tobytes
525 __bytes__ = _tobytes
526
526
527
527
528 class PushRaced(RuntimeError):
528 class PushRaced(RuntimeError):
529 """An exception raised during unbundling that indicate a push race"""
529 """An exception raised during unbundling that indicate a push race"""
530
530
531 __bytes__ = _tobytes
531 __bytes__ = _tobytes
532
532
533
533
534 class ProgrammingError(Hint, RuntimeError):
534 class ProgrammingError(Hint, RuntimeError):
535 """Raised if a mercurial (core or extension) developer made a mistake"""
535 """Raised if a mercurial (core or extension) developer made a mistake"""
536
536
537 def __init__(self, msg, *args, **kwargs):
537 def __init__(self, msg, *args, **kwargs):
538 # type: (AnyStr, Any, Any) -> None
538 # type: (AnyStr, Any, Any) -> None
539 # On Python 3, turn the message back into a string since this is
539 # On Python 3, turn the message back into a string since this is
540 # an internal-only error that won't be printed except in a
540 # an internal-only error that won't be printed except in a
541 # stack traces.
541 # stack traces.
542 msg = pycompat.sysstr(msg)
542 msg = pycompat.sysstr(msg)
543 super(ProgrammingError, self).__init__(msg, *args, **kwargs)
543 super(ProgrammingError, self).__init__(msg, *args, **kwargs)
544
544
545 __bytes__ = _tobytes
545 __bytes__ = _tobytes
546
546
547
547
548 class WdirUnsupported(Exception):
548 class WdirUnsupported(Exception):
549 """An exception which is raised when 'wdir()' is not supported"""
549 """An exception which is raised when 'wdir()' is not supported"""
550
550
551 __bytes__ = _tobytes
551 __bytes__ = _tobytes
552
552
553
553
554 # bundle2 related errors
554 # bundle2 related errors
555 class BundleValueError(ValueError):
555 class BundleValueError(ValueError):
556 """error raised when bundle2 cannot be processed"""
556 """error raised when bundle2 cannot be processed"""
557
557
558 __bytes__ = _tobytes
558 __bytes__ = _tobytes
559
559
560
560
561 class BundleUnknownFeatureError(BundleValueError):
561 class BundleUnknownFeatureError(BundleValueError):
562 def __init__(self, parttype=None, params=(), values=()):
562 def __init__(self, parttype=None, params=(), values=()):
563 self.parttype = parttype
563 self.parttype = parttype
564 self.params = params
564 self.params = params
565 self.values = values
565 self.values = values
566 if self.parttype is None:
566 if self.parttype is None:
567 msg = b'Stream Parameter'
567 msg = b'Stream Parameter'
568 else:
568 else:
569 msg = parttype
569 msg = parttype
570 entries = self.params
570 entries = self.params
571 if self.params and self.values:
571 if self.params and self.values:
572 assert len(self.params) == len(self.values)
572 assert len(self.params) == len(self.values)
573 entries = []
573 entries = []
574 for idx, par in enumerate(self.params):
574 for idx, par in enumerate(self.params):
575 val = self.values[idx]
575 val = self.values[idx]
576 if val is None:
576 if val is None:
577 entries.append(val)
577 entries.append(val)
578 else:
578 else:
579 entries.append(b"%s=%r" % (par, pycompat.maybebytestr(val)))
579 entries.append(b"%s=%r" % (par, pycompat.maybebytestr(val)))
580 if entries:
580 if entries:
581 msg = b'%s - %s' % (msg, b', '.join(entries))
581 msg = b'%s - %s' % (msg, b', '.join(entries))
582 ValueError.__init__(self, msg) # TODO: convert to str?
582 ValueError.__init__(self, msg) # TODO: convert to str?
583
583
584
584
585 class ReadOnlyPartError(RuntimeError):
585 class ReadOnlyPartError(RuntimeError):
586 """error raised when code tries to alter a part being generated"""
586 """error raised when code tries to alter a part being generated"""
587
587
588 __bytes__ = _tobytes
588 __bytes__ = _tobytes
589
589
590
590
591 class PushkeyFailed(Abort):
591 class PushkeyFailed(Abort):
592 """error raised when a pushkey part failed to update a value"""
592 """error raised when a pushkey part failed to update a value"""
593
593
594 def __init__(
594 def __init__(
595 self, partid, namespace=None, key=None, new=None, old=None, ret=None
595 self, partid, namespace=None, key=None, new=None, old=None, ret=None
596 ):
596 ):
597 self.partid = partid
597 self.partid = partid
598 self.namespace = namespace
598 self.namespace = namespace
599 self.key = key
599 self.key = key
600 self.new = new
600 self.new = new
601 self.old = old
601 self.old = old
602 self.ret = ret
602 self.ret = ret
603 # no i18n expected to be processed into a better message
603 # no i18n expected to be processed into a better message
604 Abort.__init__(
604 Abort.__init__(
605 self, b'failed to update value for "%s/%s"' % (namespace, key)
605 self, b'failed to update value for "%s/%s"' % (namespace, key)
606 )
606 )
607
607
608
608
609 class CensoredNodeError(StorageError):
609 class CensoredNodeError(StorageError):
610 """error raised when content verification fails on a censored node
610 """error raised when content verification fails on a censored node
611
611
612 Also contains the tombstone data substituted for the uncensored data.
612 Also contains the tombstone data substituted for the uncensored data.
613 """
613 """
614
614
615 def __init__(self, filename, node, tombstone):
615 def __init__(self, filename, node, tombstone):
616 # type: (bytes, bytes, bytes) -> None
616 # type: (bytes, bytes, bytes) -> None
617 from .node import short
617 from .node import short
618
618
619 StorageError.__init__(self, b'%s:%s' % (filename, short(node)))
619 StorageError.__init__(self, b'%s:%s' % (filename, short(node)))
620 self.tombstone = tombstone
620 self.tombstone = tombstone
621
621
622
622
623 class CensoredBaseError(StorageError):
623 class CensoredBaseError(StorageError):
624 """error raised when a delta is rejected because its base is censored
624 """error raised when a delta is rejected because its base is censored
625
625
626 A delta based on a censored revision must be formed as single patch
626 A delta based on a censored revision must be formed as single patch
627 operation which replaces the entire base with new content. This ensures
627 operation which replaces the entire base with new content. This ensures
628 the delta may be applied by clones which have not censored the base.
628 the delta may be applied by clones which have not censored the base.
629 """
629 """
630
630
631
631
632 class InvalidBundleSpecification(Exception):
632 class InvalidBundleSpecification(Exception):
633 """error raised when a bundle specification is invalid.
633 """error raised when a bundle specification is invalid.
634
634
635 This is used for syntax errors as opposed to support errors.
635 This is used for syntax errors as opposed to support errors.
636 """
636 """
637
637
638 __bytes__ = _tobytes
638 __bytes__ = _tobytes
639
639
640
640
641 class UnsupportedBundleSpecification(Exception):
641 class UnsupportedBundleSpecification(Exception):
642 """error raised when a bundle specification is not supported."""
642 """error raised when a bundle specification is not supported."""
643
643
644 __bytes__ = _tobytes
644 __bytes__ = _tobytes
645
645
646
646
647 class CorruptedState(Exception):
647 class CorruptedState(Exception):
648 """error raised when a command is not able to read its state from file"""
648 """error raised when a command is not able to read its state from file"""
649
649
650 __bytes__ = _tobytes
650 __bytes__ = _tobytes
651
651
652
652
653 class CorruptedDirstate(Exception):
654 """error raised the dirstate appears corrupted on-disk. It may be due to
655 a dirstate version mismatch (i.e. expecting v2 and finding v1 on disk)."""
656
657 __bytes__ = _tobytes
658
659
653 class PeerTransportError(Abort):
660 class PeerTransportError(Abort):
654 """Transport-level I/O error when communicating with a peer repo."""
661 """Transport-level I/O error when communicating with a peer repo."""
655
662
656
663
657 class InMemoryMergeConflictsError(Exception):
664 class InMemoryMergeConflictsError(Exception):
658 """Exception raised when merge conflicts arose during an in-memory merge."""
665 """Exception raised when merge conflicts arose during an in-memory merge."""
659
666
660 __bytes__ = _tobytes
667 __bytes__ = _tobytes
661
668
662
669
663 class WireprotoCommandError(Exception):
670 class WireprotoCommandError(Exception):
664 """Represents an error during execution of a wire protocol command.
671 """Represents an error during execution of a wire protocol command.
665
672
666 Should only be thrown by wire protocol version 2 commands.
673 Should only be thrown by wire protocol version 2 commands.
667
674
668 The error is a formatter string and an optional iterable of arguments.
675 The error is a formatter string and an optional iterable of arguments.
669 """
676 """
670
677
671 def __init__(self, message, args=None):
678 def __init__(self, message, args=None):
672 # type: (bytes, Optional[Sequence[bytes]]) -> None
679 # type: (bytes, Optional[Sequence[bytes]]) -> None
673 self.message = message
680 self.message = message
674 self.messageargs = args
681 self.messageargs = args
@@ -1,745 +1,782 b''
1 use crate::changelog::Changelog;
1 use crate::changelog::Changelog;
2 use crate::config::{Config, ConfigError, ConfigParseError};
2 use crate::config::{Config, ConfigError, ConfigParseError};
3 use crate::dirstate::DirstateParents;
3 use crate::dirstate::DirstateParents;
4 use crate::dirstate_tree::dirstate_map::DirstateMapWriteMode;
4 use crate::dirstate_tree::dirstate_map::DirstateMapWriteMode;
5 use crate::dirstate_tree::on_disk::Docket as DirstateDocket;
5 use crate::dirstate_tree::on_disk::Docket as DirstateDocket;
6 use crate::dirstate_tree::owning::OwningDirstateMap;
6 use crate::dirstate_tree::owning::OwningDirstateMap;
7 use crate::errors::HgResultExt;
7 use crate::errors::HgResultExt;
8 use crate::errors::{HgError, IoResultExt};
8 use crate::errors::{HgError, IoResultExt};
9 use crate::lock::{try_with_lock_no_wait, LockError};
9 use crate::lock::{try_with_lock_no_wait, LockError};
10 use crate::manifest::{Manifest, Manifestlog};
10 use crate::manifest::{Manifest, Manifestlog};
11 use crate::revlog::filelog::Filelog;
11 use crate::revlog::filelog::Filelog;
12 use crate::revlog::RevlogError;
12 use crate::revlog::RevlogError;
13 use crate::utils::debug::debug_wait_for_file_or_print;
13 use crate::utils::debug::debug_wait_for_file_or_print;
14 use crate::utils::files::get_path_from_bytes;
14 use crate::utils::files::get_path_from_bytes;
15 use crate::utils::hg_path::HgPath;
15 use crate::utils::hg_path::HgPath;
16 use crate::utils::SliceExt;
16 use crate::utils::SliceExt;
17 use crate::vfs::{is_dir, is_file, Vfs};
17 use crate::vfs::{is_dir, is_file, Vfs};
18 use crate::{requirements, NodePrefix};
18 use crate::{requirements, NodePrefix};
19 use crate::{DirstateError, Revision};
19 use crate::{DirstateError, Revision};
20 use std::cell::{Ref, RefCell, RefMut};
20 use std::cell::{Ref, RefCell, RefMut};
21 use std::collections::HashSet;
21 use std::collections::HashSet;
22 use std::io::Seek;
22 use std::io::Seek;
23 use std::io::SeekFrom;
23 use std::io::SeekFrom;
24 use std::io::Write as IoWrite;
24 use std::io::Write as IoWrite;
25 use std::path::{Path, PathBuf};
25 use std::path::{Path, PathBuf};
26
26
27 const V2_MAX_READ_ATTEMPTS: usize = 5;
27 const V2_MAX_READ_ATTEMPTS: usize = 5;
28
28
29 type DirstateMapIdentity = (Option<u64>, Option<Vec<u8>>, usize);
29 type DirstateMapIdentity = (Option<u64>, Option<Vec<u8>>, usize);
30
30
31 /// A repository on disk
31 /// A repository on disk
32 pub struct Repo {
32 pub struct Repo {
33 working_directory: PathBuf,
33 working_directory: PathBuf,
34 dot_hg: PathBuf,
34 dot_hg: PathBuf,
35 store: PathBuf,
35 store: PathBuf,
36 requirements: HashSet<String>,
36 requirements: HashSet<String>,
37 config: Config,
37 config: Config,
38 dirstate_parents: LazyCell<DirstateParents>,
38 dirstate_parents: LazyCell<DirstateParents>,
39 dirstate_map: LazyCell<OwningDirstateMap>,
39 dirstate_map: LazyCell<OwningDirstateMap>,
40 changelog: LazyCell<Changelog>,
40 changelog: LazyCell<Changelog>,
41 manifestlog: LazyCell<Manifestlog>,
41 manifestlog: LazyCell<Manifestlog>,
42 }
42 }
43
43
44 #[derive(Debug, derive_more::From)]
44 #[derive(Debug, derive_more::From)]
45 pub enum RepoError {
45 pub enum RepoError {
46 NotFound {
46 NotFound {
47 at: PathBuf,
47 at: PathBuf,
48 },
48 },
49 #[from]
49 #[from]
50 ConfigParseError(ConfigParseError),
50 ConfigParseError(ConfigParseError),
51 #[from]
51 #[from]
52 Other(HgError),
52 Other(HgError),
53 }
53 }
54
54
55 impl From<ConfigError> for RepoError {
55 impl From<ConfigError> for RepoError {
56 fn from(error: ConfigError) -> Self {
56 fn from(error: ConfigError) -> Self {
57 match error {
57 match error {
58 ConfigError::Parse(error) => error.into(),
58 ConfigError::Parse(error) => error.into(),
59 ConfigError::Other(error) => error.into(),
59 ConfigError::Other(error) => error.into(),
60 }
60 }
61 }
61 }
62 }
62 }
63
63
64 impl Repo {
64 impl Repo {
65 /// tries to find nearest repository root in current working directory or
65 /// tries to find nearest repository root in current working directory or
66 /// its ancestors
66 /// its ancestors
67 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
67 pub fn find_repo_root() -> Result<PathBuf, RepoError> {
68 let current_directory = crate::utils::current_dir()?;
68 let current_directory = crate::utils::current_dir()?;
69 // ancestors() is inclusive: it first yields `current_directory`
69 // ancestors() is inclusive: it first yields `current_directory`
70 // as-is.
70 // as-is.
71 for ancestor in current_directory.ancestors() {
71 for ancestor in current_directory.ancestors() {
72 if is_dir(ancestor.join(".hg"))? {
72 if is_dir(ancestor.join(".hg"))? {
73 return Ok(ancestor.to_path_buf());
73 return Ok(ancestor.to_path_buf());
74 }
74 }
75 }
75 }
76 Err(RepoError::NotFound {
76 Err(RepoError::NotFound {
77 at: current_directory,
77 at: current_directory,
78 })
78 })
79 }
79 }
80
80
81 /// Find a repository, either at the given path (which must contain a `.hg`
81 /// Find a repository, either at the given path (which must contain a `.hg`
82 /// sub-directory) or by searching the current directory and its
82 /// sub-directory) or by searching the current directory and its
83 /// ancestors.
83 /// ancestors.
84 ///
84 ///
85 /// A method with two very different "modes" like this usually a code smell
85 /// A method with two very different "modes" like this usually a code smell
86 /// to make two methods instead, but in this case an `Option` is what rhg
86 /// to make two methods instead, but in this case an `Option` is what rhg
87 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
87 /// sub-commands get from Clap for the `-R` / `--repository` CLI argument.
88 /// Having two methods would just move that `if` to almost all callers.
88 /// Having two methods would just move that `if` to almost all callers.
89 pub fn find(
89 pub fn find(
90 config: &Config,
90 config: &Config,
91 explicit_path: Option<PathBuf>,
91 explicit_path: Option<PathBuf>,
92 ) -> Result<Self, RepoError> {
92 ) -> Result<Self, RepoError> {
93 if let Some(root) = explicit_path {
93 if let Some(root) = explicit_path {
94 if is_dir(root.join(".hg"))? {
94 if is_dir(root.join(".hg"))? {
95 Self::new_at_path(root, config)
95 Self::new_at_path(root, config)
96 } else if is_file(&root)? {
96 } else if is_file(&root)? {
97 Err(HgError::unsupported("bundle repository").into())
97 Err(HgError::unsupported("bundle repository").into())
98 } else {
98 } else {
99 Err(RepoError::NotFound { at: root })
99 Err(RepoError::NotFound { at: root })
100 }
100 }
101 } else {
101 } else {
102 let root = Self::find_repo_root()?;
102 let root = Self::find_repo_root()?;
103 Self::new_at_path(root, config)
103 Self::new_at_path(root, config)
104 }
104 }
105 }
105 }
106
106
107 /// To be called after checking that `.hg` is a sub-directory
107 /// To be called after checking that `.hg` is a sub-directory
108 fn new_at_path(
108 fn new_at_path(
109 working_directory: PathBuf,
109 working_directory: PathBuf,
110 config: &Config,
110 config: &Config,
111 ) -> Result<Self, RepoError> {
111 ) -> Result<Self, RepoError> {
112 let dot_hg = working_directory.join(".hg");
112 let dot_hg = working_directory.join(".hg");
113
113
114 let mut repo_config_files =
114 let mut repo_config_files =
115 vec![dot_hg.join("hgrc"), dot_hg.join("hgrc-not-shared")];
115 vec![dot_hg.join("hgrc"), dot_hg.join("hgrc-not-shared")];
116
116
117 let hg_vfs = Vfs { base: &dot_hg };
117 let hg_vfs = Vfs { base: &dot_hg };
118 let mut reqs = requirements::load_if_exists(hg_vfs)?;
118 let mut reqs = requirements::load_if_exists(hg_vfs)?;
119 let relative =
119 let relative =
120 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
120 reqs.contains(requirements::RELATIVE_SHARED_REQUIREMENT);
121 let shared =
121 let shared =
122 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
122 reqs.contains(requirements::SHARED_REQUIREMENT) || relative;
123
123
124 // From `mercurial/localrepo.py`:
124 // From `mercurial/localrepo.py`:
125 //
125 //
126 // if .hg/requires contains the sharesafe requirement, it means
126 // if .hg/requires contains the sharesafe requirement, it means
127 // there exists a `.hg/store/requires` too and we should read it
127 // there exists a `.hg/store/requires` too and we should read it
128 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
128 // NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
129 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
129 // is present. We never write SHARESAFE_REQUIREMENT for a repo if store
130 // is not present, refer checkrequirementscompat() for that
130 // is not present, refer checkrequirementscompat() for that
131 //
131 //
132 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
132 // However, if SHARESAFE_REQUIREMENT is not present, it means that the
133 // repository was shared the old way. We check the share source
133 // repository was shared the old way. We check the share source
134 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
134 // .hg/requires for SHARESAFE_REQUIREMENT to detect whether the
135 // current repository needs to be reshared
135 // current repository needs to be reshared
136 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
136 let share_safe = reqs.contains(requirements::SHARESAFE_REQUIREMENT);
137
137
138 let store_path;
138 let store_path;
139 if !shared {
139 if !shared {
140 store_path = dot_hg.join("store");
140 store_path = dot_hg.join("store");
141 } else {
141 } else {
142 let bytes = hg_vfs.read("sharedpath")?;
142 let bytes = hg_vfs.read("sharedpath")?;
143 let mut shared_path =
143 let mut shared_path =
144 get_path_from_bytes(bytes.trim_end_matches(|b| b == b'\n'))
144 get_path_from_bytes(bytes.trim_end_matches(|b| b == b'\n'))
145 .to_owned();
145 .to_owned();
146 if relative {
146 if relative {
147 shared_path = dot_hg.join(shared_path)
147 shared_path = dot_hg.join(shared_path)
148 }
148 }
149 if !is_dir(&shared_path)? {
149 if !is_dir(&shared_path)? {
150 return Err(HgError::corrupted(format!(
150 return Err(HgError::corrupted(format!(
151 ".hg/sharedpath points to nonexistent directory {}",
151 ".hg/sharedpath points to nonexistent directory {}",
152 shared_path.display()
152 shared_path.display()
153 ))
153 ))
154 .into());
154 .into());
155 }
155 }
156
156
157 store_path = shared_path.join("store");
157 store_path = shared_path.join("store");
158
158
159 let source_is_share_safe =
159 let source_is_share_safe =
160 requirements::load(Vfs { base: &shared_path })?
160 requirements::load(Vfs { base: &shared_path })?
161 .contains(requirements::SHARESAFE_REQUIREMENT);
161 .contains(requirements::SHARESAFE_REQUIREMENT);
162
162
163 if share_safe != source_is_share_safe {
163 if share_safe != source_is_share_safe {
164 return Err(HgError::unsupported("share-safe mismatch").into());
164 return Err(HgError::unsupported("share-safe mismatch").into());
165 }
165 }
166
166
167 if share_safe {
167 if share_safe {
168 repo_config_files.insert(0, shared_path.join("hgrc"))
168 repo_config_files.insert(0, shared_path.join("hgrc"))
169 }
169 }
170 }
170 }
171 if share_safe {
171 if share_safe {
172 reqs.extend(requirements::load(Vfs { base: &store_path })?);
172 reqs.extend(requirements::load(Vfs { base: &store_path })?);
173 }
173 }
174
174
175 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
175 let repo_config = if std::env::var_os("HGRCSKIPREPO").is_none() {
176 config.combine_with_repo(&repo_config_files)?
176 config.combine_with_repo(&repo_config_files)?
177 } else {
177 } else {
178 config.clone()
178 config.clone()
179 };
179 };
180
180
181 let repo = Self {
181 let repo = Self {
182 requirements: reqs,
182 requirements: reqs,
183 working_directory,
183 working_directory,
184 store: store_path,
184 store: store_path,
185 dot_hg,
185 dot_hg,
186 config: repo_config,
186 config: repo_config,
187 dirstate_parents: LazyCell::new(),
187 dirstate_parents: LazyCell::new(),
188 dirstate_map: LazyCell::new(),
188 dirstate_map: LazyCell::new(),
189 changelog: LazyCell::new(),
189 changelog: LazyCell::new(),
190 manifestlog: LazyCell::new(),
190 manifestlog: LazyCell::new(),
191 };
191 };
192
192
193 requirements::check(&repo)?;
193 requirements::check(&repo)?;
194
194
195 Ok(repo)
195 Ok(repo)
196 }
196 }
197
197
198 pub fn working_directory_path(&self) -> &Path {
198 pub fn working_directory_path(&self) -> &Path {
199 &self.working_directory
199 &self.working_directory
200 }
200 }
201
201
202 pub fn requirements(&self) -> &HashSet<String> {
202 pub fn requirements(&self) -> &HashSet<String> {
203 &self.requirements
203 &self.requirements
204 }
204 }
205
205
206 pub fn config(&self) -> &Config {
206 pub fn config(&self) -> &Config {
207 &self.config
207 &self.config
208 }
208 }
209
209
210 /// For accessing repository files (in `.hg`), except for the store
210 /// For accessing repository files (in `.hg`), except for the store
211 /// (`.hg/store`).
211 /// (`.hg/store`).
212 pub fn hg_vfs(&self) -> Vfs<'_> {
212 pub fn hg_vfs(&self) -> Vfs<'_> {
213 Vfs { base: &self.dot_hg }
213 Vfs { base: &self.dot_hg }
214 }
214 }
215
215
216 /// For accessing repository store files (in `.hg/store`)
216 /// For accessing repository store files (in `.hg/store`)
217 pub fn store_vfs(&self) -> Vfs<'_> {
217 pub fn store_vfs(&self) -> Vfs<'_> {
218 Vfs { base: &self.store }
218 Vfs { base: &self.store }
219 }
219 }
220
220
221 /// For accessing the working copy
221 /// For accessing the working copy
222 pub fn working_directory_vfs(&self) -> Vfs<'_> {
222 pub fn working_directory_vfs(&self) -> Vfs<'_> {
223 Vfs {
223 Vfs {
224 base: &self.working_directory,
224 base: &self.working_directory,
225 }
225 }
226 }
226 }
227
227
228 pub fn try_with_wlock_no_wait<R>(
228 pub fn try_with_wlock_no_wait<R>(
229 &self,
229 &self,
230 f: impl FnOnce() -> R,
230 f: impl FnOnce() -> R,
231 ) -> Result<R, LockError> {
231 ) -> Result<R, LockError> {
232 try_with_lock_no_wait(self.hg_vfs(), "wlock", f)
232 try_with_lock_no_wait(self.hg_vfs(), "wlock", f)
233 }
233 }
234
234
235 /// Whether this repo should use dirstate-v2.
235 /// Whether this repo should use dirstate-v2.
236 /// The presence of `dirstate-v2` in the requirements does not mean that
236 /// The presence of `dirstate-v2` in the requirements does not mean that
237 /// the on-disk dirstate is necessarily in version 2. In most cases,
237 /// the on-disk dirstate is necessarily in version 2. In most cases,
238 /// a dirstate-v2 file will indeed be found, but in rare cases (like the
238 /// a dirstate-v2 file will indeed be found, but in rare cases (like the
239 /// upgrade mechanism being cut short), the on-disk version will be a
239 /// upgrade mechanism being cut short), the on-disk version will be a
240 /// v1 file.
240 /// v1 file.
241 /// Semantically, having a requirement only means that a client should be
241 /// Semantically, having a requirement only means that a client cannot
242 /// able to understand the repo *if* it uses the requirement, but not that
242 /// properly understand or properly update the repo if it lacks the support
243 /// the requirement is actually used.
243 /// for the required feature, but not that that feature is actually used
244 /// in all occasions.
244 pub fn use_dirstate_v2(&self) -> bool {
245 pub fn use_dirstate_v2(&self) -> bool {
245 self.requirements
246 self.requirements
246 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
247 .contains(requirements::DIRSTATE_V2_REQUIREMENT)
247 }
248 }
248
249
249 pub fn has_sparse(&self) -> bool {
250 pub fn has_sparse(&self) -> bool {
250 self.requirements.contains(requirements::SPARSE_REQUIREMENT)
251 self.requirements.contains(requirements::SPARSE_REQUIREMENT)
251 }
252 }
252
253
253 pub fn has_narrow(&self) -> bool {
254 pub fn has_narrow(&self) -> bool {
254 self.requirements.contains(requirements::NARROW_REQUIREMENT)
255 self.requirements.contains(requirements::NARROW_REQUIREMENT)
255 }
256 }
256
257
257 pub fn has_nodemap(&self) -> bool {
258 pub fn has_nodemap(&self) -> bool {
258 self.requirements
259 self.requirements
259 .contains(requirements::NODEMAP_REQUIREMENT)
260 .contains(requirements::NODEMAP_REQUIREMENT)
260 }
261 }
261
262
262 fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> {
263 fn dirstate_file_contents(&self) -> Result<Vec<u8>, HgError> {
263 Ok(self
264 Ok(self
264 .hg_vfs()
265 .hg_vfs()
265 .read("dirstate")
266 .read("dirstate")
266 .io_not_found_as_none()?
267 .io_not_found_as_none()?
267 .unwrap_or_default())
268 .unwrap_or_default())
268 }
269 }
269
270
270 fn dirstate_identity(&self) -> Result<Option<u64>, HgError> {
271 fn dirstate_identity(&self) -> Result<Option<u64>, HgError> {
271 use std::os::unix::fs::MetadataExt;
272 use std::os::unix::fs::MetadataExt;
272 Ok(self
273 Ok(self
273 .hg_vfs()
274 .hg_vfs()
274 .symlink_metadata("dirstate")
275 .symlink_metadata("dirstate")
275 .io_not_found_as_none()?
276 .io_not_found_as_none()?
276 .map(|meta| meta.ino()))
277 .map(|meta| meta.ino()))
277 }
278 }
278
279
279 pub fn dirstate_parents(&self) -> Result<DirstateParents, HgError> {
280 pub fn dirstate_parents(&self) -> Result<DirstateParents, HgError> {
280 Ok(*self
281 Ok(*self
281 .dirstate_parents
282 .dirstate_parents
282 .get_or_init(|| self.read_dirstate_parents())?)
283 .get_or_init(|| self.read_dirstate_parents())?)
283 }
284 }
284
285
285 fn read_dirstate_parents(&self) -> Result<DirstateParents, HgError> {
286 fn read_dirstate_parents(&self) -> Result<DirstateParents, HgError> {
286 let dirstate = self.dirstate_file_contents()?;
287 let dirstate = self.dirstate_file_contents()?;
287 let parents = if dirstate.is_empty() {
288 let parents = if dirstate.is_empty() {
288 DirstateParents::NULL
289 DirstateParents::NULL
289 } else if self.use_dirstate_v2() {
290 } else if self.use_dirstate_v2() {
290 let docket =
291 let docket_res =
291 crate::dirstate_tree::on_disk::read_docket(&dirstate)?;
292 crate::dirstate_tree::on_disk::read_docket(&dirstate);
292 docket.parents()
293 match docket_res {
294 Ok(docket) => docket.parents(),
295 Err(_) => {
296 log::info!(
297 "Parsing dirstate docket failed, \
298 falling back to dirstate-v1"
299 );
300 *crate::dirstate::parsers::parse_dirstate_parents(
301 &dirstate,
302 )?
303 }
304 }
293 } else {
305 } else {
294 *crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
306 *crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?
295 };
307 };
296 self.dirstate_parents.set(parents);
308 self.dirstate_parents.set(parents);
297 Ok(parents)
309 Ok(parents)
298 }
310 }
299
311
300 /// Returns the information read from the dirstate docket necessary to
312 /// Returns the information read from the dirstate docket necessary to
301 /// check if the data file has been updated/deleted by another process
313 /// check if the data file has been updated/deleted by another process
302 /// since we last read the dirstate.
314 /// since we last read the dirstate.
303 /// Namely, the inode, data file uuid and the data size.
315 /// Namely, the inode, data file uuid and the data size.
304 fn get_dirstate_data_file_integrity(
316 fn get_dirstate_data_file_integrity(
305 &self,
317 &self,
306 ) -> Result<DirstateMapIdentity, HgError> {
318 ) -> Result<DirstateMapIdentity, HgError> {
307 assert!(
319 assert!(
308 self.use_dirstate_v2(),
320 self.use_dirstate_v2(),
309 "accessing dirstate data file ID without dirstate-v2"
321 "accessing dirstate data file ID without dirstate-v2"
310 );
322 );
311 // Get the identity before the contents since we could have a race
323 // Get the identity before the contents since we could have a race
312 // between the two. Having an identity that is too old is fine, but
324 // between the two. Having an identity that is too old is fine, but
313 // one that is younger than the content change is bad.
325 // one that is younger than the content change is bad.
314 let identity = self.dirstate_identity()?;
326 let identity = self.dirstate_identity()?;
315 let dirstate = self.dirstate_file_contents()?;
327 let dirstate = self.dirstate_file_contents()?;
316 if dirstate.is_empty() {
328 if dirstate.is_empty() {
317 self.dirstate_parents.set(DirstateParents::NULL);
329 self.dirstate_parents.set(DirstateParents::NULL);
318 Ok((identity, None, 0))
330 Ok((identity, None, 0))
319 } else {
331 } else {
320 let docket =
332 let docket_res =
321 crate::dirstate_tree::on_disk::read_docket(&dirstate)?;
333 crate::dirstate_tree::on_disk::read_docket(&dirstate);
334 match docket_res {
335 Ok(docket) => {
322 self.dirstate_parents.set(docket.parents());
336 self.dirstate_parents.set(docket.parents());
323 Ok((identity, Some(docket.uuid.to_owned()), docket.data_size()))
337 Ok((
338 identity,
339 Some(docket.uuid.to_owned()),
340 docket.data_size(),
341 ))
342 }
343 Err(_) => {
344 log::info!(
345 "Parsing dirstate docket failed, \
346 falling back to dirstate-v1"
347 );
348 let parents =
349 *crate::dirstate::parsers::parse_dirstate_parents(
350 &dirstate,
351 )?;
352 self.dirstate_parents.set(parents);
353 Ok((identity, None, 0))
354 }
355 }
324 }
356 }
325 }
357 }
326
358
327 fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> {
359 fn new_dirstate_map(&self) -> Result<OwningDirstateMap, DirstateError> {
328 if self.use_dirstate_v2() {
360 if self.use_dirstate_v2() {
329 // The v2 dirstate is split into a docket and a data file.
361 // The v2 dirstate is split into a docket and a data file.
330 // Since we don't always take the `wlock` to read it
362 // Since we don't always take the `wlock` to read it
331 // (like in `hg status`), it is susceptible to races.
363 // (like in `hg status`), it is susceptible to races.
332 // A simple retry method should be enough since full rewrites
364 // A simple retry method should be enough since full rewrites
333 // only happen when too much garbage data is present and
365 // only happen when too much garbage data is present and
334 // this race is unlikely.
366 // this race is unlikely.
335 let mut tries = 0;
367 let mut tries = 0;
336
368
337 while tries < V2_MAX_READ_ATTEMPTS {
369 while tries < V2_MAX_READ_ATTEMPTS {
338 tries += 1;
370 tries += 1;
339 match self.read_docket_and_data_file() {
371 match self.read_docket_and_data_file() {
340 Ok(m) => {
372 Ok(m) => {
341 return Ok(m);
373 return Ok(m);
342 }
374 }
343 Err(e) => match e {
375 Err(e) => match e {
344 DirstateError::Common(HgError::RaceDetected(
376 DirstateError::Common(HgError::RaceDetected(
345 context,
377 context,
346 )) => {
378 )) => {
347 log::info!(
379 log::info!(
348 "dirstate read race detected {} (retry {}/{})",
380 "dirstate read race detected {} (retry {}/{})",
349 context,
381 context,
350 tries,
382 tries,
351 V2_MAX_READ_ATTEMPTS,
383 V2_MAX_READ_ATTEMPTS,
352 );
384 );
353 continue;
385 continue;
354 }
386 }
355 _ => return Err(e),
387 _ => {
388 log::info!(
389 "Reading dirstate v2 failed, \
390 falling back to v1"
391 );
392 return self.new_dirstate_map_v1();
393 }
356 },
394 },
357 }
395 }
358 }
396 }
359 let error = HgError::abort(
397 let error = HgError::abort(
360 format!("dirstate read race happened {tries} times in a row"),
398 format!("dirstate read race happened {tries} times in a row"),
361 255,
399 255,
362 None,
400 None,
363 );
401 );
364 Err(DirstateError::Common(error))
402 Err(DirstateError::Common(error))
365 } else {
403 } else {
366 debug_wait_for_file_or_print(
404 self.new_dirstate_map_v1()
367 self.config(),
405 }
368 "dirstate.pre-read-file",
406 }
369 );
407
408 fn new_dirstate_map_v1(&self) -> Result<OwningDirstateMap, DirstateError> {
409 debug_wait_for_file_or_print(self.config(), "dirstate.pre-read-file");
370 let identity = self.dirstate_identity()?;
410 let identity = self.dirstate_identity()?;
371 let dirstate_file_contents = self.dirstate_file_contents()?;
411 let dirstate_file_contents = self.dirstate_file_contents()?;
372 if dirstate_file_contents.is_empty() {
412 if dirstate_file_contents.is_empty() {
373 self.dirstate_parents.set(DirstateParents::NULL);
413 self.dirstate_parents.set(DirstateParents::NULL);
374 Ok(OwningDirstateMap::new_empty(Vec::new()))
414 Ok(OwningDirstateMap::new_empty(Vec::new()))
375 } else {
415 } else {
376 let (map, parents) = OwningDirstateMap::new_v1(
416 let (map, parents) =
377 dirstate_file_contents,
417 OwningDirstateMap::new_v1(dirstate_file_contents, identity)?;
378 identity,
379 )?;
380 self.dirstate_parents.set(parents);
418 self.dirstate_parents.set(parents);
381 Ok(map)
419 Ok(map)
382 }
420 }
383 }
421 }
384 }
385
422
386 fn read_docket_and_data_file(
423 fn read_docket_and_data_file(
387 &self,
424 &self,
388 ) -> Result<OwningDirstateMap, DirstateError> {
425 ) -> Result<OwningDirstateMap, DirstateError> {
389 debug_wait_for_file_or_print(self.config(), "dirstate.pre-read-file");
426 debug_wait_for_file_or_print(self.config(), "dirstate.pre-read-file");
390 let dirstate_file_contents = self.dirstate_file_contents()?;
427 let dirstate_file_contents = self.dirstate_file_contents()?;
391 let identity = self.dirstate_identity()?;
428 let identity = self.dirstate_identity()?;
392 if dirstate_file_contents.is_empty() {
429 if dirstate_file_contents.is_empty() {
393 self.dirstate_parents.set(DirstateParents::NULL);
430 self.dirstate_parents.set(DirstateParents::NULL);
394 return Ok(OwningDirstateMap::new_empty(Vec::new()));
431 return Ok(OwningDirstateMap::new_empty(Vec::new()));
395 }
432 }
396 let docket = crate::dirstate_tree::on_disk::read_docket(
433 let docket = crate::dirstate_tree::on_disk::read_docket(
397 &dirstate_file_contents,
434 &dirstate_file_contents,
398 )?;
435 )?;
399 debug_wait_for_file_or_print(
436 debug_wait_for_file_or_print(
400 self.config(),
437 self.config(),
401 "dirstate.post-docket-read-file",
438 "dirstate.post-docket-read-file",
402 );
439 );
403 self.dirstate_parents.set(docket.parents());
440 self.dirstate_parents.set(docket.parents());
404 let uuid = docket.uuid.to_owned();
441 let uuid = docket.uuid.to_owned();
405 let data_size = docket.data_size();
442 let data_size = docket.data_size();
406
443
407 let context = "between reading dirstate docket and data file";
444 let context = "between reading dirstate docket and data file";
408 let race_error = HgError::RaceDetected(context.into());
445 let race_error = HgError::RaceDetected(context.into());
409 let metadata = docket.tree_metadata();
446 let metadata = docket.tree_metadata();
410
447
411 let mut map = if crate::vfs::is_on_nfs_mount(docket.data_filename()) {
448 let mut map = if crate::vfs::is_on_nfs_mount(docket.data_filename()) {
412 // Don't mmap on NFS to prevent `SIGBUS` error on deletion
449 // Don't mmap on NFS to prevent `SIGBUS` error on deletion
413 let contents = self.hg_vfs().read(docket.data_filename());
450 let contents = self.hg_vfs().read(docket.data_filename());
414 let contents = match contents {
451 let contents = match contents {
415 Ok(c) => c,
452 Ok(c) => c,
416 Err(HgError::IoError { error, context }) => {
453 Err(HgError::IoError { error, context }) => {
417 match error.raw_os_error().expect("real os error") {
454 match error.raw_os_error().expect("real os error") {
418 // 2 = ENOENT, No such file or directory
455 // 2 = ENOENT, No such file or directory
419 // 116 = ESTALE, Stale NFS file handle
456 // 116 = ESTALE, Stale NFS file handle
420 //
457 //
421 // TODO match on `error.kind()` when
458 // TODO match on `error.kind()` when
422 // `ErrorKind::StaleNetworkFileHandle` is stable.
459 // `ErrorKind::StaleNetworkFileHandle` is stable.
423 2 | 116 => {
460 2 | 116 => {
424 // Race where the data file was deleted right after
461 // Race where the data file was deleted right after
425 // we read the docket, try again
462 // we read the docket, try again
426 return Err(race_error.into());
463 return Err(race_error.into());
427 }
464 }
428 _ => {
465 _ => {
429 return Err(
466 return Err(
430 HgError::IoError { error, context }.into()
467 HgError::IoError { error, context }.into()
431 )
468 )
432 }
469 }
433 }
470 }
434 }
471 }
435 Err(e) => return Err(e.into()),
472 Err(e) => return Err(e.into()),
436 };
473 };
437 OwningDirstateMap::new_v2(
474 OwningDirstateMap::new_v2(
438 contents, data_size, metadata, uuid, identity,
475 contents, data_size, metadata, uuid, identity,
439 )
476 )
440 } else {
477 } else {
441 match self
478 match self
442 .hg_vfs()
479 .hg_vfs()
443 .mmap_open(docket.data_filename())
480 .mmap_open(docket.data_filename())
444 .io_not_found_as_none()
481 .io_not_found_as_none()
445 {
482 {
446 Ok(Some(data_mmap)) => OwningDirstateMap::new_v2(
483 Ok(Some(data_mmap)) => OwningDirstateMap::new_v2(
447 data_mmap, data_size, metadata, uuid, identity,
484 data_mmap, data_size, metadata, uuid, identity,
448 ),
485 ),
449 Ok(None) => {
486 Ok(None) => {
450 // Race where the data file was deleted right after we
487 // Race where the data file was deleted right after we
451 // read the docket, try again
488 // read the docket, try again
452 return Err(race_error.into());
489 return Err(race_error.into());
453 }
490 }
454 Err(e) => return Err(e.into()),
491 Err(e) => return Err(e.into()),
455 }
492 }
456 }?;
493 }?;
457
494
458 let write_mode_config = self
495 let write_mode_config = self
459 .config()
496 .config()
460 .get_str(b"devel", b"dirstate.v2.data_update_mode")
497 .get_str(b"devel", b"dirstate.v2.data_update_mode")
461 .unwrap_or(Some("auto"))
498 .unwrap_or(Some("auto"))
462 .unwrap_or("auto"); // don't bother for devel options
499 .unwrap_or("auto"); // don't bother for devel options
463 let write_mode = match write_mode_config {
500 let write_mode = match write_mode_config {
464 "auto" => DirstateMapWriteMode::Auto,
501 "auto" => DirstateMapWriteMode::Auto,
465 "force-new" => DirstateMapWriteMode::ForceNewDataFile,
502 "force-new" => DirstateMapWriteMode::ForceNewDataFile,
466 "force-append" => DirstateMapWriteMode::ForceAppend,
503 "force-append" => DirstateMapWriteMode::ForceAppend,
467 _ => DirstateMapWriteMode::Auto,
504 _ => DirstateMapWriteMode::Auto,
468 };
505 };
469
506
470 map.with_dmap_mut(|m| m.set_write_mode(write_mode));
507 map.with_dmap_mut(|m| m.set_write_mode(write_mode));
471
508
472 Ok(map)
509 Ok(map)
473 }
510 }
474
511
475 pub fn dirstate_map(
512 pub fn dirstate_map(
476 &self,
513 &self,
477 ) -> Result<Ref<OwningDirstateMap>, DirstateError> {
514 ) -> Result<Ref<OwningDirstateMap>, DirstateError> {
478 self.dirstate_map.get_or_init(|| self.new_dirstate_map())
515 self.dirstate_map.get_or_init(|| self.new_dirstate_map())
479 }
516 }
480
517
481 pub fn dirstate_map_mut(
518 pub fn dirstate_map_mut(
482 &self,
519 &self,
483 ) -> Result<RefMut<OwningDirstateMap>, DirstateError> {
520 ) -> Result<RefMut<OwningDirstateMap>, DirstateError> {
484 self.dirstate_map
521 self.dirstate_map
485 .get_mut_or_init(|| self.new_dirstate_map())
522 .get_mut_or_init(|| self.new_dirstate_map())
486 }
523 }
487
524
488 fn new_changelog(&self) -> Result<Changelog, HgError> {
525 fn new_changelog(&self) -> Result<Changelog, HgError> {
489 Changelog::open(&self.store_vfs(), self.has_nodemap())
526 Changelog::open(&self.store_vfs(), self.has_nodemap())
490 }
527 }
491
528
492 pub fn changelog(&self) -> Result<Ref<Changelog>, HgError> {
529 pub fn changelog(&self) -> Result<Ref<Changelog>, HgError> {
493 self.changelog.get_or_init(|| self.new_changelog())
530 self.changelog.get_or_init(|| self.new_changelog())
494 }
531 }
495
532
496 pub fn changelog_mut(&self) -> Result<RefMut<Changelog>, HgError> {
533 pub fn changelog_mut(&self) -> Result<RefMut<Changelog>, HgError> {
497 self.changelog.get_mut_or_init(|| self.new_changelog())
534 self.changelog.get_mut_or_init(|| self.new_changelog())
498 }
535 }
499
536
500 fn new_manifestlog(&self) -> Result<Manifestlog, HgError> {
537 fn new_manifestlog(&self) -> Result<Manifestlog, HgError> {
501 Manifestlog::open(&self.store_vfs(), self.has_nodemap())
538 Manifestlog::open(&self.store_vfs(), self.has_nodemap())
502 }
539 }
503
540
504 pub fn manifestlog(&self) -> Result<Ref<Manifestlog>, HgError> {
541 pub fn manifestlog(&self) -> Result<Ref<Manifestlog>, HgError> {
505 self.manifestlog.get_or_init(|| self.new_manifestlog())
542 self.manifestlog.get_or_init(|| self.new_manifestlog())
506 }
543 }
507
544
508 pub fn manifestlog_mut(&self) -> Result<RefMut<Manifestlog>, HgError> {
545 pub fn manifestlog_mut(&self) -> Result<RefMut<Manifestlog>, HgError> {
509 self.manifestlog.get_mut_or_init(|| self.new_manifestlog())
546 self.manifestlog.get_mut_or_init(|| self.new_manifestlog())
510 }
547 }
511
548
512 /// Returns the manifest of the *changeset* with the given node ID
549 /// Returns the manifest of the *changeset* with the given node ID
513 pub fn manifest_for_node(
550 pub fn manifest_for_node(
514 &self,
551 &self,
515 node: impl Into<NodePrefix>,
552 node: impl Into<NodePrefix>,
516 ) -> Result<Manifest, RevlogError> {
553 ) -> Result<Manifest, RevlogError> {
517 self.manifestlog()?.data_for_node(
554 self.manifestlog()?.data_for_node(
518 self.changelog()?
555 self.changelog()?
519 .data_for_node(node.into())?
556 .data_for_node(node.into())?
520 .manifest_node()?
557 .manifest_node()?
521 .into(),
558 .into(),
522 )
559 )
523 }
560 }
524
561
525 /// Returns the manifest of the *changeset* with the given revision number
562 /// Returns the manifest of the *changeset* with the given revision number
526 pub fn manifest_for_rev(
563 pub fn manifest_for_rev(
527 &self,
564 &self,
528 revision: Revision,
565 revision: Revision,
529 ) -> Result<Manifest, RevlogError> {
566 ) -> Result<Manifest, RevlogError> {
530 self.manifestlog()?.data_for_node(
567 self.manifestlog()?.data_for_node(
531 self.changelog()?
568 self.changelog()?
532 .data_for_rev(revision)?
569 .data_for_rev(revision)?
533 .manifest_node()?
570 .manifest_node()?
534 .into(),
571 .into(),
535 )
572 )
536 }
573 }
537
574
538 pub fn has_subrepos(&self) -> Result<bool, DirstateError> {
575 pub fn has_subrepos(&self) -> Result<bool, DirstateError> {
539 if let Some(entry) = self.dirstate_map()?.get(HgPath::new(".hgsub"))? {
576 if let Some(entry) = self.dirstate_map()?.get(HgPath::new(".hgsub"))? {
540 Ok(entry.tracked())
577 Ok(entry.tracked())
541 } else {
578 } else {
542 Ok(false)
579 Ok(false)
543 }
580 }
544 }
581 }
545
582
546 pub fn filelog(&self, path: &HgPath) -> Result<Filelog, HgError> {
583 pub fn filelog(&self, path: &HgPath) -> Result<Filelog, HgError> {
547 Filelog::open(self, path)
584 Filelog::open(self, path)
548 }
585 }
549
586
550 /// Write to disk any updates that were made through `dirstate_map_mut`.
587 /// Write to disk any updates that were made through `dirstate_map_mut`.
551 ///
588 ///
552 /// The "wlock" must be held while calling this.
589 /// The "wlock" must be held while calling this.
553 /// See for example `try_with_wlock_no_wait`.
590 /// See for example `try_with_wlock_no_wait`.
554 ///
591 ///
555 /// TODO: have a `WritableRepo` type only accessible while holding the
592 /// TODO: have a `WritableRepo` type only accessible while holding the
556 /// lock?
593 /// lock?
557 pub fn write_dirstate(&self) -> Result<(), DirstateError> {
594 pub fn write_dirstate(&self) -> Result<(), DirstateError> {
558 let map = self.dirstate_map()?;
595 let map = self.dirstate_map()?;
559 // TODO: Maintain a `DirstateMap::dirty` flag, and return early here if
596 // TODO: Maintain a `DirstateMap::dirty` flag, and return early here if
560 // it’s unset
597 // it’s unset
561 let parents = self.dirstate_parents()?;
598 let parents = self.dirstate_parents()?;
562 let (packed_dirstate, old_uuid_to_remove) = if self.use_dirstate_v2() {
599 let (packed_dirstate, old_uuid_to_remove) = if self.use_dirstate_v2() {
563 let (identity, uuid, data_size) =
600 let (identity, uuid, data_size) =
564 self.get_dirstate_data_file_integrity()?;
601 self.get_dirstate_data_file_integrity()?;
565 let identity_changed = identity != map.old_identity();
602 let identity_changed = identity != map.old_identity();
566 let uuid_changed = uuid.as_deref() != map.old_uuid();
603 let uuid_changed = uuid.as_deref() != map.old_uuid();
567 let data_length_changed = data_size != map.old_data_size();
604 let data_length_changed = data_size != map.old_data_size();
568
605
569 if identity_changed || uuid_changed || data_length_changed {
606 if identity_changed || uuid_changed || data_length_changed {
570 // If any of identity, uuid or length have changed since
607 // If any of identity, uuid or length have changed since
571 // last disk read, don't write.
608 // last disk read, don't write.
572 // This is fine because either we're in a command that doesn't
609 // This is fine because either we're in a command that doesn't
573 // write anything too important (like `hg status`), or we're in
610 // write anything too important (like `hg status`), or we're in
574 // `hg add` and we're supposed to have taken the lock before
611 // `hg add` and we're supposed to have taken the lock before
575 // reading anyway.
612 // reading anyway.
576 //
613 //
577 // TODO complain loudly if we've changed anything important
614 // TODO complain loudly if we've changed anything important
578 // without taking the lock.
615 // without taking the lock.
579 // (see `hg help config.format.use-dirstate-tracked-hint`)
616 // (see `hg help config.format.use-dirstate-tracked-hint`)
580 log::debug!(
617 log::debug!(
581 "dirstate has changed since last read, not updating."
618 "dirstate has changed since last read, not updating."
582 );
619 );
583 return Ok(());
620 return Ok(());
584 }
621 }
585
622
586 let uuid_opt = map.old_uuid();
623 let uuid_opt = map.old_uuid();
587 let write_mode = if uuid_opt.is_some() {
624 let write_mode = if uuid_opt.is_some() {
588 DirstateMapWriteMode::Auto
625 DirstateMapWriteMode::Auto
589 } else {
626 } else {
590 DirstateMapWriteMode::ForceNewDataFile
627 DirstateMapWriteMode::ForceNewDataFile
591 };
628 };
592 let (data, tree_metadata, append, old_data_size) =
629 let (data, tree_metadata, append, old_data_size) =
593 map.pack_v2(write_mode)?;
630 map.pack_v2(write_mode)?;
594
631
595 // Reuse the uuid, or generate a new one, keeping the old for
632 // Reuse the uuid, or generate a new one, keeping the old for
596 // deletion.
633 // deletion.
597 let (uuid, old_uuid) = match uuid_opt {
634 let (uuid, old_uuid) = match uuid_opt {
598 Some(uuid) => {
635 Some(uuid) => {
599 let as_str = std::str::from_utf8(uuid)
636 let as_str = std::str::from_utf8(uuid)
600 .map_err(|_| {
637 .map_err(|_| {
601 HgError::corrupted(
638 HgError::corrupted(
602 "non-UTF-8 dirstate data file ID",
639 "non-UTF-8 dirstate data file ID",
603 )
640 )
604 })?
641 })?
605 .to_owned();
642 .to_owned();
606 if append {
643 if append {
607 (as_str, None)
644 (as_str, None)
608 } else {
645 } else {
609 (DirstateDocket::new_uid(), Some(as_str))
646 (DirstateDocket::new_uid(), Some(as_str))
610 }
647 }
611 }
648 }
612 None => (DirstateDocket::new_uid(), None),
649 None => (DirstateDocket::new_uid(), None),
613 };
650 };
614
651
615 let data_filename = format!("dirstate.{}", uuid);
652 let data_filename = format!("dirstate.{}", uuid);
616 let data_filename = self.hg_vfs().join(data_filename);
653 let data_filename = self.hg_vfs().join(data_filename);
617 let mut options = std::fs::OpenOptions::new();
654 let mut options = std::fs::OpenOptions::new();
618 options.write(true);
655 options.write(true);
619
656
620 // Why are we not using the O_APPEND flag when appending?
657 // Why are we not using the O_APPEND flag when appending?
621 //
658 //
622 // - O_APPEND makes it trickier to deal with garbage at the end of
659 // - O_APPEND makes it trickier to deal with garbage at the end of
623 // the file, left by a previous uncommitted transaction. By
660 // the file, left by a previous uncommitted transaction. By
624 // starting the write at [old_data_size] we make sure we erase
661 // starting the write at [old_data_size] we make sure we erase
625 // all such garbage.
662 // all such garbage.
626 //
663 //
627 // - O_APPEND requires to special-case 0-byte writes, whereas we
664 // - O_APPEND requires to special-case 0-byte writes, whereas we
628 // don't need that.
665 // don't need that.
629 //
666 //
630 // - Some OSes have bugs in implementation O_APPEND:
667 // - Some OSes have bugs in implementation O_APPEND:
631 // revlog.py talks about a Solaris bug, but we also saw some ZFS
668 // revlog.py talks about a Solaris bug, but we also saw some ZFS
632 // bug: https://github.com/openzfs/zfs/pull/3124,
669 // bug: https://github.com/openzfs/zfs/pull/3124,
633 // https://github.com/openzfs/zfs/issues/13370
670 // https://github.com/openzfs/zfs/issues/13370
634 //
671 //
635 if !append {
672 if !append {
636 log::trace!("creating a new dirstate data file");
673 log::trace!("creating a new dirstate data file");
637 options.create_new(true);
674 options.create_new(true);
638 } else {
675 } else {
639 log::trace!("appending to the dirstate data file");
676 log::trace!("appending to the dirstate data file");
640 }
677 }
641
678
642 let data_size = (|| {
679 let data_size = (|| {
643 // TODO: loop and try another random ID if !append and this
680 // TODO: loop and try another random ID if !append and this
644 // returns `ErrorKind::AlreadyExists`? Collision chance of two
681 // returns `ErrorKind::AlreadyExists`? Collision chance of two
645 // random IDs is one in 2**32
682 // random IDs is one in 2**32
646 let mut file = options.open(&data_filename)?;
683 let mut file = options.open(&data_filename)?;
647 if append {
684 if append {
648 file.seek(SeekFrom::Start(old_data_size as u64))?;
685 file.seek(SeekFrom::Start(old_data_size as u64))?;
649 }
686 }
650 file.write_all(&data)?;
687 file.write_all(&data)?;
651 file.flush()?;
688 file.flush()?;
652 file.seek(SeekFrom::Current(0))
689 file.seek(SeekFrom::Current(0))
653 })()
690 })()
654 .when_writing_file(&data_filename)?;
691 .when_writing_file(&data_filename)?;
655
692
656 let packed_dirstate = DirstateDocket::serialize(
693 let packed_dirstate = DirstateDocket::serialize(
657 parents,
694 parents,
658 tree_metadata,
695 tree_metadata,
659 data_size,
696 data_size,
660 uuid.as_bytes(),
697 uuid.as_bytes(),
661 )
698 )
662 .map_err(|_: std::num::TryFromIntError| {
699 .map_err(|_: std::num::TryFromIntError| {
663 HgError::corrupted("overflow in dirstate docket serialization")
700 HgError::corrupted("overflow in dirstate docket serialization")
664 })?;
701 })?;
665
702
666 (packed_dirstate, old_uuid)
703 (packed_dirstate, old_uuid)
667 } else {
704 } else {
668 let identity = self.dirstate_identity()?;
705 let identity = self.dirstate_identity()?;
669 if identity != map.old_identity() {
706 if identity != map.old_identity() {
670 // If identity changed since last disk read, don't write.
707 // If identity changed since last disk read, don't write.
671 // This is fine because either we're in a command that doesn't
708 // This is fine because either we're in a command that doesn't
672 // write anything too important (like `hg status`), or we're in
709 // write anything too important (like `hg status`), or we're in
673 // `hg add` and we're supposed to have taken the lock before
710 // `hg add` and we're supposed to have taken the lock before
674 // reading anyway.
711 // reading anyway.
675 //
712 //
676 // TODO complain loudly if we've changed anything important
713 // TODO complain loudly if we've changed anything important
677 // without taking the lock.
714 // without taking the lock.
678 // (see `hg help config.format.use-dirstate-tracked-hint`)
715 // (see `hg help config.format.use-dirstate-tracked-hint`)
679 log::debug!(
716 log::debug!(
680 "dirstate has changed since last read, not updating."
717 "dirstate has changed since last read, not updating."
681 );
718 );
682 return Ok(());
719 return Ok(());
683 }
720 }
684 (map.pack_v1(parents)?, None)
721 (map.pack_v1(parents)?, None)
685 };
722 };
686
723
687 let vfs = self.hg_vfs();
724 let vfs = self.hg_vfs();
688 vfs.atomic_write("dirstate", &packed_dirstate)?;
725 vfs.atomic_write("dirstate", &packed_dirstate)?;
689 if let Some(uuid) = old_uuid_to_remove {
726 if let Some(uuid) = old_uuid_to_remove {
690 // Remove the old data file after the new docket pointing to the
727 // Remove the old data file after the new docket pointing to the
691 // new data file was written.
728 // new data file was written.
692 vfs.remove_file(format!("dirstate.{}", uuid))?;
729 vfs.remove_file(format!("dirstate.{}", uuid))?;
693 }
730 }
694 Ok(())
731 Ok(())
695 }
732 }
696 }
733 }
697
734
698 /// Lazily-initialized component of `Repo` with interior mutability
735 /// Lazily-initialized component of `Repo` with interior mutability
699 ///
736 ///
700 /// This differs from `OnceCell` in that the value can still be "deinitialized"
737 /// This differs from `OnceCell` in that the value can still be "deinitialized"
701 /// later by setting its inner `Option` to `None`. It also takes the
738 /// later by setting its inner `Option` to `None`. It also takes the
702 /// initialization function as an argument when the value is requested, not
739 /// initialization function as an argument when the value is requested, not
703 /// when the instance is created.
740 /// when the instance is created.
704 struct LazyCell<T> {
741 struct LazyCell<T> {
705 value: RefCell<Option<T>>,
742 value: RefCell<Option<T>>,
706 }
743 }
707
744
708 impl<T> LazyCell<T> {
745 impl<T> LazyCell<T> {
709 fn new() -> Self {
746 fn new() -> Self {
710 Self {
747 Self {
711 value: RefCell::new(None),
748 value: RefCell::new(None),
712 }
749 }
713 }
750 }
714
751
715 fn set(&self, value: T) {
752 fn set(&self, value: T) {
716 *self.value.borrow_mut() = Some(value)
753 *self.value.borrow_mut() = Some(value)
717 }
754 }
718
755
719 fn get_or_init<E>(
756 fn get_or_init<E>(
720 &self,
757 &self,
721 init: impl Fn() -> Result<T, E>,
758 init: impl Fn() -> Result<T, E>,
722 ) -> Result<Ref<T>, E> {
759 ) -> Result<Ref<T>, E> {
723 let mut borrowed = self.value.borrow();
760 let mut borrowed = self.value.borrow();
724 if borrowed.is_none() {
761 if borrowed.is_none() {
725 drop(borrowed);
762 drop(borrowed);
726 // Only use `borrow_mut` if it is really needed to avoid panic in
763 // Only use `borrow_mut` if it is really needed to avoid panic in
727 // case there is another outstanding borrow but mutation is not
764 // case there is another outstanding borrow but mutation is not
728 // needed.
765 // needed.
729 *self.value.borrow_mut() = Some(init()?);
766 *self.value.borrow_mut() = Some(init()?);
730 borrowed = self.value.borrow()
767 borrowed = self.value.borrow()
731 }
768 }
732 Ok(Ref::map(borrowed, |option| option.as_ref().unwrap()))
769 Ok(Ref::map(borrowed, |option| option.as_ref().unwrap()))
733 }
770 }
734
771
735 fn get_mut_or_init<E>(
772 fn get_mut_or_init<E>(
736 &self,
773 &self,
737 init: impl Fn() -> Result<T, E>,
774 init: impl Fn() -> Result<T, E>,
738 ) -> Result<RefMut<T>, E> {
775 ) -> Result<RefMut<T>, E> {
739 let mut borrowed = self.value.borrow_mut();
776 let mut borrowed = self.value.borrow_mut();
740 if borrowed.is_none() {
777 if borrowed.is_none() {
741 *borrowed = Some(init()?);
778 *borrowed = Some(init()?);
742 }
779 }
743 Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap()))
780 Ok(RefMut::map(borrowed, |option| option.as_mut().unwrap()))
744 }
781 }
745 }
782 }
@@ -1,43 +1,49 b''
1 $ cat >> $HGRCPATH << EOF
1 $ cat >> $HGRCPATH << EOF
2 > [storage]
2 > [storage]
3 > dirstate-v2.slow-path=allow
3 > dirstate-v2.slow-path=allow
4 > EOF
4 > EOF
5
5
6 Set up a v1 repo
6 Set up a v1 repo
7
7
8 $ hg init repo
8 $ hg init repo
9 $ cd repo
9 $ cd repo
10 $ echo a > a
10 $ echo a > a
11 $ hg add a
11 $ hg add a
12 $ hg commit -m a
12 $ hg commit -m a
13 $ hg debugrequires | grep dirstate
13 $ hg debugrequires | grep dirstate
14 [1]
14 [1]
15 $ ls -1 .hg/dirstate*
15 $ ls -1 .hg/dirstate*
16 .hg/dirstate
16 .hg/dirstate
17
17
18 Copy v1 dirstate
18 Copy v1 dirstate
19 $ cp .hg/dirstate $TESTTMP/dirstate-v1-backup
19 $ cp .hg/dirstate $TESTTMP/dirstate-v1-backup
20
20
21 Upgrade it to v2
21 Upgrade it to v2
22
22
23 $ hg debugupgraderepo -q --config format.use-dirstate-v2=1 --run | grep added
23 $ hg debugupgraderepo -q --config format.use-dirstate-v2=1 --run | egrep 'added:|removed:'
24 added: dirstate-v2
24 added: dirstate-v2
25 $ hg debugrequires | grep dirstate
25 $ hg debugrequires | grep dirstate
26 dirstate-v2
26 dirstate-v2
27 $ ls -1 .hg/dirstate*
27 $ ls -1 .hg/dirstate*
28 .hg/dirstate
28 .hg/dirstate
29 .hg/dirstate.* (glob)
29 .hg/dirstate.* (glob)
30
30
31 Manually reset to dirstate v1 to simulate an incomplete dirstate-v2 upgrade
31 Manually reset to dirstate v1 to simulate an incomplete dirstate-v2 upgrade
32
32
33 $ rm .hg/dirstate*
33 $ rm .hg/dirstate*
34 $ cp $TESTTMP/dirstate-v1-backup .hg/dirstate
34 $ cp $TESTTMP/dirstate-v1-backup .hg/dirstate
35
35
36 There should be no errors, but a v2 dirstate should be written back to disk
36 There should be no errors, but a v2 dirstate should be written back to disk
37 $ hg st
37 $ hg st
38 abort: dirstate-v2 parse error: when reading docket, Expected at least * bytes, got * (glob) (known-bad-output !)
39 [255]
40 $ ls -1 .hg/dirstate*
38 $ ls -1 .hg/dirstate*
41 .hg/dirstate
39 .hg/dirstate
42 .hg/dirstate.* (glob) (missing-correct-output !)
40 .hg/dirstate.* (glob)
41
42 Corrupt the dirstate to see how the errors show up to the user
43 $ echo "I ate your data" > .hg/dirstate
43
44
45 $ hg st
46 abort: working directory state appears damaged! (no-rhg !)
47 (falling back to dirstate-v1 from v2 also failed) (no-rhg !)
48 abort: Too little data for dirstate. (rhg !)
49 [255]
General Comments 0
You need to be logged in to leave comments. Login now