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