Show More
@@ -4,6 +4,7 b'' | |||||
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 ( | |
@@ -151,9 +152,15 b' class _dirstatemapcommon:' | |||||
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() | |
154 | self._docket = docketmod.DirstateDocket.parse( |
|
155 | try: | |
155 | self._readdirstatefile(), self._nodeconstants |
|
156 | self._docket = docketmod.DirstateDocket.parse( | |
156 | ) |
|
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 | return self._docket |
|
164 | return self._docket | |
158 |
|
165 | |||
159 | def _read_v2_data(self): |
|
166 | def _read_v2_data(self): | |
@@ -176,11 +183,23 b' class _dirstatemapcommon:' | |||||
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): | |
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 | 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) | |
@@ -194,7 +213,7 b' class _dirstatemapcommon:' | |||||
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') | |
@@ -211,28 +230,40 b' class _dirstatemapcommon:' | |||||
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: | |
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 | else: |
|
240 | else: | |
216 |
|
|
241 | self._v1_parents() | |
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 | ) |
|
|||
233 |
|
242 | |||
234 | return self._parents |
|
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 | class dirstatemap(_dirstatemapcommon): |
|
268 | class dirstatemap(_dirstatemapcommon): | |
238 | """Map encapsulating the dirstate's contents. |
|
269 | """Map encapsulating the dirstate's contents. | |
@@ -330,11 +361,17 b' class dirstatemap(_dirstatemapcommon):' | |||||
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: | ||
334 |
|
|
365 | self.docket | |
335 | return |
|
366 | except error.CorruptedDirstate: | |
336 | testing.wait_on_cfg(self._ui, b'dirstate.post-docket-read-file') |
|
367 | # fall back to dirstate-v1 if we fail to read v2 | |
337 |
|
|
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 | else: |
|
375 | else: | |
339 | self._set_identity() |
|
376 | self._set_identity() | |
340 | st = self._readdirstatefile() |
|
377 | st = self._readdirstatefile() | |
@@ -365,10 +402,17 b' class dirstatemap(_dirstatemapcommon):' | |||||
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: | |
368 | p = self.docket.parents |
|
405 | try: | |
369 |
|
|
406 | self.docket | |
370 | parse_dirstate = util.nogc(v2.parse_dirstate) |
|
407 | except error.CorruptedDirstate: | |
371 | parse_dirstate(self._map, self.copymap, st, meta) |
|
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 | 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) | |
@@ -597,38 +641,37 b' if rustmod is not None:' | |||||
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: | |
601 |
|
|
645 | self.docket | |
602 | self.identity.stat.st_ino |
|
646 | except error.CorruptedDirstate as e: | |
603 | if self.identity is not None |
|
647 | # fall back to dirstate-v1 if we fail to read v2 | |
604 | and self.identity.stat is not None |
|
648 | parents = self._v1_map(e) | |
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() |
|
|||
611 | else: |
|
649 | else: | |
612 |
|
|
650 | parents = self.docket.parents | |
613 | self._map = rustmod.DirstateMap.new_v2( |
|
651 | inode = ( | |
614 |
|
|
652 | self.identity.stat.st_ino | |
615 |
self. |
|
653 | if self.identity is not None | |
616 |
self.d |
|
654 | and self.identity.stat is not None | |
617 |
|
|
655 | else None | |
618 |
|
|
656 | ) | |
|
657 | testing.wait_on_cfg( | |||
|
658 | self._ui, b'dirstate.post-docket-read-file' | |||
619 | ) |
|
659 | ) | |
620 |
|
|
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 | else: |
|
673 | else: | |
622 |
self._ |
|
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) | |
@@ -638,6 +681,23 b' if rustmod is not None:' | |||||
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() | |
@@ -696,9 +756,15 b' if rustmod is not None:' | |||||
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: |
@@ -650,6 +650,13 b' class CorruptedState(Exception):' | |||||
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 |
@@ -238,9 +238,10 b' impl Repo {' | |||||
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 |
|
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 require |
|
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) | |
@@ -287,9 +288,20 b' impl Repo {' | |||||
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 |
|
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 | }; | |
@@ -317,10 +329,30 b' impl Repo {' | |||||
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); | |
322 | self.dirstate_parents.set(docket.parents()); |
|
334 | match docket_res { | |
323 | Ok((identity, Some(docket.uuid.to_owned()), docket.data_size())) |
|
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 | |||
@@ -352,7 +384,13 b' impl Repo {' | |||||
352 | ); |
|
384 | ); | |
353 | continue; |
|
385 | continue; | |
354 | } |
|
386 | } | |
355 |
_ => |
|
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 | } | |
@@ -363,23 +401,22 b' impl Repo {' | |||||
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 | ||
370 | let identity = self.dirstate_identity()?; |
|
408 | fn new_dirstate_map_v1(&self) -> Result<OwningDirstateMap, DirstateError> { | |
371 | let dirstate_file_contents = self.dirstate_file_contents()?; |
|
409 | debug_wait_for_file_or_print(self.config(), "dirstate.pre-read-file"); | |
372 | if dirstate_file_contents.is_empty() { |
|
410 | let identity = self.dirstate_identity()?; | |
373 | self.dirstate_parents.set(DirstateParents::NULL); |
|
411 | let dirstate_file_contents = self.dirstate_file_contents()?; | |
374 | Ok(OwningDirstateMap::new_empty(Vec::new())) |
|
412 | if dirstate_file_contents.is_empty() { | |
375 | } else { |
|
413 | self.dirstate_parents.set(DirstateParents::NULL); | |
376 |
|
|
414 | Ok(OwningDirstateMap::new_empty(Vec::new())) | |
377 | dirstate_file_contents, |
|
415 | } else { | |
378 | identity, |
|
416 | let (map, parents) = | |
379 | )?; |
|
417 | OwningDirstateMap::new_v1(dirstate_file_contents, identity)?; | |
380 |
|
|
418 | self.dirstate_parents.set(parents); | |
381 |
|
|
419 | Ok(map) | |
382 | } |
|
|||
383 | } |
|
420 | } | |
384 | } |
|
421 | } | |
385 |
|
422 |
@@ -20,7 +20,7 b' Copy v1 dirstate' | |||||
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 | |
@@ -35,9 +35,15 b' Manually reset to dirstate v1 to simulat' | |||||
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) |
|
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