##// END OF EJS Templates
rhg: fallback when encountering ellipsis revisions...
Raphaël Gomès -
r50454:7787174f stable
parent child Browse files
Show More
@@ -1,639 +1,644 b''
1 1 use std::borrow::Cow;
2 2 use std::convert::TryFrom;
3 3 use std::io::Read;
4 4 use std::ops::Deref;
5 5 use std::path::Path;
6 6
7 7 use flate2::read::ZlibDecoder;
8 8 use sha1::{Digest, Sha1};
9 9 use zstd;
10 10
11 11 use super::index::Index;
12 12 use super::node::{NodePrefix, NODE_BYTES_LENGTH, NULL_NODE};
13 13 use super::nodemap;
14 14 use super::nodemap::{NodeMap, NodeMapError};
15 15 use super::nodemap_docket::NodeMapDocket;
16 16 use super::patch;
17 17 use crate::errors::HgError;
18 18 use crate::revlog::Revision;
19 19 use crate::vfs::Vfs;
20 20 use crate::{Node, NULL_REVISION};
21 21
22 22 const REVISION_FLAG_CENSORED: u16 = 1 << 15;
23 23 const REVISION_FLAG_ELLIPSIS: u16 = 1 << 14;
24 24 const REVISION_FLAG_EXTSTORED: u16 = 1 << 13;
25 25 const REVISION_FLAG_HASCOPIESINFO: u16 = 1 << 12;
26 26
27 27 // Keep this in sync with REVIDX_KNOWN_FLAGS in
28 28 // mercurial/revlogutils/flagutil.py
29 29 const REVIDX_KNOWN_FLAGS: u16 = REVISION_FLAG_CENSORED
30 30 | REVISION_FLAG_ELLIPSIS
31 31 | REVISION_FLAG_EXTSTORED
32 32 | REVISION_FLAG_HASCOPIESINFO;
33 33
34 34 const NULL_REVLOG_ENTRY_FLAGS: u16 = 0;
35 35
36 36 #[derive(Debug, derive_more::From)]
37 37 pub enum RevlogError {
38 38 InvalidRevision,
39 39 /// Working directory is not supported
40 40 WDirUnsupported,
41 41 /// Found more than one entry whose ID match the requested prefix
42 42 AmbiguousPrefix,
43 43 #[from]
44 44 Other(HgError),
45 45 }
46 46
47 47 impl From<NodeMapError> for RevlogError {
48 48 fn from(error: NodeMapError) -> Self {
49 49 match error {
50 50 NodeMapError::MultipleResults => RevlogError::AmbiguousPrefix,
51 51 NodeMapError::RevisionNotInIndex(rev) => RevlogError::corrupted(
52 52 format!("nodemap point to revision {} not in index", rev),
53 53 ),
54 54 }
55 55 }
56 56 }
57 57
58 58 fn corrupted<S: AsRef<str>>(context: S) -> HgError {
59 59 HgError::corrupted(format!("corrupted revlog, {}", context.as_ref()))
60 60 }
61 61
62 62 impl RevlogError {
63 63 fn corrupted<S: AsRef<str>>(context: S) -> Self {
64 64 RevlogError::Other(corrupted(context))
65 65 }
66 66 }
67 67
68 68 /// Read only implementation of revlog.
69 69 pub struct Revlog {
70 70 /// When index and data are not interleaved: bytes of the revlog index.
71 71 /// When index and data are interleaved: bytes of the revlog index and
72 72 /// data.
73 73 index: Index,
74 74 /// When index and data are not interleaved: bytes of the revlog data
75 75 data_bytes: Option<Box<dyn Deref<Target = [u8]> + Send>>,
76 76 /// When present on disk: the persistent nodemap for this revlog
77 77 nodemap: Option<nodemap::NodeTree>,
78 78 }
79 79
80 80 impl Revlog {
81 81 /// Open a revlog index file.
82 82 ///
83 83 /// It will also open the associated data file if index and data are not
84 84 /// interleaved.
85 85 pub fn open(
86 86 store_vfs: &Vfs,
87 87 index_path: impl AsRef<Path>,
88 88 data_path: Option<&Path>,
89 89 use_nodemap: bool,
90 90 ) -> Result<Self, HgError> {
91 91 let index_path = index_path.as_ref();
92 92 let index = {
93 93 match store_vfs.mmap_open_opt(&index_path)? {
94 94 None => Index::new(Box::new(vec![])),
95 95 Some(index_mmap) => {
96 96 let index = Index::new(Box::new(index_mmap))?;
97 97 Ok(index)
98 98 }
99 99 }
100 100 }?;
101 101
102 102 let default_data_path = index_path.with_extension("d");
103 103
104 104 // type annotation required
105 105 // won't recognize Mmap as Deref<Target = [u8]>
106 106 let data_bytes: Option<Box<dyn Deref<Target = [u8]> + Send>> =
107 107 if index.is_inline() {
108 108 None
109 109 } else {
110 110 let data_path = data_path.unwrap_or(&default_data_path);
111 111 let data_mmap = store_vfs.mmap_open(data_path)?;
112 112 Some(Box::new(data_mmap))
113 113 };
114 114
115 115 let nodemap = if index.is_inline() {
116 116 None
117 117 } else if !use_nodemap {
118 118 None
119 119 } else {
120 120 NodeMapDocket::read_from_file(store_vfs, index_path)?.map(
121 121 |(docket, data)| {
122 122 nodemap::NodeTree::load_bytes(
123 123 Box::new(data),
124 124 docket.data_length,
125 125 )
126 126 },
127 127 )
128 128 };
129 129
130 130 Ok(Revlog {
131 131 index,
132 132 data_bytes,
133 133 nodemap,
134 134 })
135 135 }
136 136
137 137 /// Return number of entries of the `Revlog`.
138 138 pub fn len(&self) -> usize {
139 139 self.index.len()
140 140 }
141 141
142 142 /// Returns `true` if the `Revlog` has zero `entries`.
143 143 pub fn is_empty(&self) -> bool {
144 144 self.index.is_empty()
145 145 }
146 146
147 147 /// Returns the node ID for the given revision number, if it exists in this
148 148 /// revlog
149 149 pub fn node_from_rev(&self, rev: Revision) -> Option<&Node> {
150 150 if rev == NULL_REVISION {
151 151 return Some(&NULL_NODE);
152 152 }
153 153 Some(self.index.get_entry(rev)?.hash())
154 154 }
155 155
156 156 /// Return the revision number for the given node ID, if it exists in this
157 157 /// revlog
158 158 pub fn rev_from_node(
159 159 &self,
160 160 node: NodePrefix,
161 161 ) -> Result<Revision, RevlogError> {
162 162 if node.is_prefix_of(&NULL_NODE) {
163 163 return Ok(NULL_REVISION);
164 164 }
165 165
166 166 if let Some(nodemap) = &self.nodemap {
167 167 return nodemap
168 168 .find_bin(&self.index, node)?
169 169 .ok_or(RevlogError::InvalidRevision);
170 170 }
171 171
172 172 // Fallback to linear scan when a persistent nodemap is not present.
173 173 // This happens when the persistent-nodemap experimental feature is not
174 174 // enabled, or for small revlogs.
175 175 //
176 176 // TODO: consider building a non-persistent nodemap in memory to
177 177 // optimize these cases.
178 178 let mut found_by_prefix = None;
179 179 for rev in (0..self.len() as Revision).rev() {
180 180 let index_entry =
181 181 self.index.get_entry(rev).ok_or(HgError::corrupted(
182 182 "revlog references a revision not in the index",
183 183 ))?;
184 184 if node == *index_entry.hash() {
185 185 return Ok(rev);
186 186 }
187 187 if node.is_prefix_of(index_entry.hash()) {
188 188 if found_by_prefix.is_some() {
189 189 return Err(RevlogError::AmbiguousPrefix);
190 190 }
191 191 found_by_prefix = Some(rev)
192 192 }
193 193 }
194 194 found_by_prefix.ok_or(RevlogError::InvalidRevision)
195 195 }
196 196
197 197 /// Returns whether the given revision exists in this revlog.
198 198 pub fn has_rev(&self, rev: Revision) -> bool {
199 199 self.index.get_entry(rev).is_some()
200 200 }
201 201
202 202 /// Return the full data associated to a revision.
203 203 ///
204 204 /// All entries required to build the final data out of deltas will be
205 205 /// retrieved as needed, and the deltas will be applied to the inital
206 206 /// snapshot to rebuild the final data.
207 207 pub fn get_rev_data(
208 208 &self,
209 209 rev: Revision,
210 210 ) -> Result<Cow<[u8]>, RevlogError> {
211 211 if rev == NULL_REVISION {
212 212 return Ok(Cow::Borrowed(&[]));
213 213 };
214 214 Ok(self.get_entry(rev)?.data()?)
215 215 }
216 216
217 217 /// Check the hash of some given data against the recorded hash.
218 218 pub fn check_hash(
219 219 &self,
220 220 p1: Revision,
221 221 p2: Revision,
222 222 expected: &[u8],
223 223 data: &[u8],
224 224 ) -> bool {
225 225 let e1 = self.index.get_entry(p1);
226 226 let h1 = match e1 {
227 227 Some(ref entry) => entry.hash(),
228 228 None => &NULL_NODE,
229 229 };
230 230 let e2 = self.index.get_entry(p2);
231 231 let h2 = match e2 {
232 232 Some(ref entry) => entry.hash(),
233 233 None => &NULL_NODE,
234 234 };
235 235
236 236 &hash(data, h1.as_bytes(), h2.as_bytes()) == expected
237 237 }
238 238
239 239 /// Build the full data of a revision out its snapshot
240 240 /// and its deltas.
241 241 fn build_data_from_deltas(
242 242 snapshot: RevlogEntry,
243 243 deltas: &[RevlogEntry],
244 244 ) -> Result<Vec<u8>, HgError> {
245 245 let snapshot = snapshot.data_chunk()?;
246 246 let deltas = deltas
247 247 .iter()
248 248 .rev()
249 249 .map(RevlogEntry::data_chunk)
250 250 .collect::<Result<Vec<_>, _>>()?;
251 251 let patches: Vec<_> =
252 252 deltas.iter().map(|d| patch::PatchList::new(d)).collect();
253 253 let patch = patch::fold_patch_lists(&patches);
254 254 Ok(patch.apply(&snapshot))
255 255 }
256 256
257 257 /// Return the revlog data.
258 258 fn data(&self) -> &[u8] {
259 259 match self.data_bytes {
260 260 Some(ref data_bytes) => &data_bytes,
261 261 None => panic!(
262 262 "forgot to load the data or trying to access inline data"
263 263 ),
264 264 }
265 265 }
266 266
267 267 pub fn make_null_entry(&self) -> RevlogEntry {
268 268 RevlogEntry {
269 269 revlog: self,
270 270 rev: NULL_REVISION,
271 271 bytes: b"",
272 272 compressed_len: 0,
273 273 uncompressed_len: 0,
274 274 base_rev_or_base_of_delta_chain: None,
275 275 p1: NULL_REVISION,
276 276 p2: NULL_REVISION,
277 277 flags: NULL_REVLOG_ENTRY_FLAGS,
278 278 hash: NULL_NODE,
279 279 }
280 280 }
281 281
282 282 /// Get an entry of the revlog.
283 283 pub fn get_entry(
284 284 &self,
285 285 rev: Revision,
286 286 ) -> Result<RevlogEntry, RevlogError> {
287 287 if rev == NULL_REVISION {
288 288 return Ok(self.make_null_entry());
289 289 }
290 290 let index_entry = self
291 291 .index
292 292 .get_entry(rev)
293 293 .ok_or(RevlogError::InvalidRevision)?;
294 294 let start = index_entry.offset();
295 295 let end = start + index_entry.compressed_len() as usize;
296 296 let data = if self.index.is_inline() {
297 297 self.index.data(start, end)
298 298 } else {
299 299 &self.data()[start..end]
300 300 };
301 301 let entry = RevlogEntry {
302 302 revlog: self,
303 303 rev,
304 304 bytes: data,
305 305 compressed_len: index_entry.compressed_len(),
306 306 uncompressed_len: index_entry.uncompressed_len(),
307 307 base_rev_or_base_of_delta_chain: if index_entry
308 308 .base_revision_or_base_of_delta_chain()
309 309 == rev
310 310 {
311 311 None
312 312 } else {
313 313 Some(index_entry.base_revision_or_base_of_delta_chain())
314 314 },
315 315 p1: index_entry.p1(),
316 316 p2: index_entry.p2(),
317 317 flags: index_entry.flags(),
318 318 hash: *index_entry.hash(),
319 319 };
320 320 Ok(entry)
321 321 }
322 322
323 323 /// when resolving internal references within revlog, any errors
324 324 /// should be reported as corruption, instead of e.g. "invalid revision"
325 325 fn get_entry_internal(
326 326 &self,
327 327 rev: Revision,
328 328 ) -> Result<RevlogEntry, HgError> {
329 329 self.get_entry(rev)
330 330 .map_err(|_| corrupted(format!("revision {} out of range", rev)))
331 331 }
332 332 }
333 333
334 334 /// The revlog entry's bytes and the necessary informations to extract
335 335 /// the entry's data.
336 336 #[derive(Clone)]
337 337 pub struct RevlogEntry<'a> {
338 338 revlog: &'a Revlog,
339 339 rev: Revision,
340 340 bytes: &'a [u8],
341 341 compressed_len: u32,
342 342 uncompressed_len: i32,
343 343 base_rev_or_base_of_delta_chain: Option<Revision>,
344 344 p1: Revision,
345 345 p2: Revision,
346 346 flags: u16,
347 347 hash: Node,
348 348 }
349 349
350 350 impl<'a> RevlogEntry<'a> {
351 351 pub fn revision(&self) -> Revision {
352 352 self.rev
353 353 }
354 354
355 355 pub fn node(&self) -> &Node {
356 356 &self.hash
357 357 }
358 358
359 359 pub fn uncompressed_len(&self) -> Option<u32> {
360 360 u32::try_from(self.uncompressed_len).ok()
361 361 }
362 362
363 363 pub fn has_p1(&self) -> bool {
364 364 self.p1 != NULL_REVISION
365 365 }
366 366
367 367 pub fn p1_entry(&self) -> Result<Option<RevlogEntry>, RevlogError> {
368 368 if self.p1 == NULL_REVISION {
369 369 Ok(None)
370 370 } else {
371 371 Ok(Some(self.revlog.get_entry(self.p1)?))
372 372 }
373 373 }
374 374
375 375 pub fn p2_entry(&self) -> Result<Option<RevlogEntry>, RevlogError> {
376 376 if self.p2 == NULL_REVISION {
377 377 Ok(None)
378 378 } else {
379 379 Ok(Some(self.revlog.get_entry(self.p2)?))
380 380 }
381 381 }
382 382
383 383 pub fn p1(&self) -> Option<Revision> {
384 384 if self.p1 == NULL_REVISION {
385 385 None
386 386 } else {
387 387 Some(self.p1)
388 388 }
389 389 }
390 390
391 391 pub fn p2(&self) -> Option<Revision> {
392 392 if self.p2 == NULL_REVISION {
393 393 None
394 394 } else {
395 395 Some(self.p2)
396 396 }
397 397 }
398 398
399 399 pub fn is_censored(&self) -> bool {
400 400 (self.flags & REVISION_FLAG_CENSORED) != 0
401 401 }
402 402
403 403 pub fn has_length_affecting_flag_processor(&self) -> bool {
404 404 // Relevant Python code: revlog.size()
405 405 // note: ELLIPSIS is known to not change the content
406 406 (self.flags & (REVIDX_KNOWN_FLAGS ^ REVISION_FLAG_ELLIPSIS)) != 0
407 407 }
408 408
409 409 /// The data for this entry, after resolving deltas if any.
410 410 pub fn rawdata(&self) -> Result<Cow<'a, [u8]>, HgError> {
411 411 let mut entry = self.clone();
412 412 let mut delta_chain = vec![];
413 413
414 414 // The meaning of `base_rev_or_base_of_delta_chain` depends on
415 415 // generaldelta. See the doc on `ENTRY_DELTA_BASE` in
416 416 // `mercurial/revlogutils/constants.py` and the code in
417 417 // [_chaininfo] and in [index_deltachain].
418 418 let uses_generaldelta = self.revlog.index.uses_generaldelta();
419 419 while let Some(base_rev) = entry.base_rev_or_base_of_delta_chain {
420 420 let base_rev = if uses_generaldelta {
421 421 base_rev
422 422 } else {
423 423 entry.rev - 1
424 424 };
425 425 delta_chain.push(entry);
426 426 entry = self.revlog.get_entry_internal(base_rev)?;
427 427 }
428 428
429 429 let data = if delta_chain.is_empty() {
430 430 entry.data_chunk()?
431 431 } else {
432 432 Revlog::build_data_from_deltas(entry, &delta_chain)?.into()
433 433 };
434 434
435 435 Ok(data)
436 436 }
437 437
438 438 fn check_data(
439 439 &self,
440 440 data: Cow<'a, [u8]>,
441 441 ) -> Result<Cow<'a, [u8]>, HgError> {
442 442 if self.revlog.check_hash(
443 443 self.p1,
444 444 self.p2,
445 445 self.hash.as_bytes(),
446 446 &data,
447 447 ) {
448 448 Ok(data)
449 449 } else {
450 if (self.flags & REVISION_FLAG_ELLIPSIS) != 0 {
451 return Err(HgError::unsupported(
452 "ellipsis revisions are not supported by rhg",
453 ));
454 }
450 455 Err(corrupted(format!(
451 456 "hash check failed for revision {}",
452 457 self.rev
453 458 )))
454 459 }
455 460 }
456 461
457 462 pub fn data(&self) -> Result<Cow<'a, [u8]>, HgError> {
458 463 let data = self.rawdata()?;
459 464 if self.is_censored() {
460 465 return Err(HgError::CensoredNodeError);
461 466 }
462 467 self.check_data(data)
463 468 }
464 469
465 470 /// Extract the data contained in the entry.
466 471 /// This may be a delta. (See `is_delta`.)
467 472 fn data_chunk(&self) -> Result<Cow<'a, [u8]>, HgError> {
468 473 if self.bytes.is_empty() {
469 474 return Ok(Cow::Borrowed(&[]));
470 475 }
471 476 match self.bytes[0] {
472 477 // Revision data is the entirety of the entry, including this
473 478 // header.
474 479 b'\0' => Ok(Cow::Borrowed(self.bytes)),
475 480 // Raw revision data follows.
476 481 b'u' => Ok(Cow::Borrowed(&self.bytes[1..])),
477 482 // zlib (RFC 1950) data.
478 483 b'x' => Ok(Cow::Owned(self.uncompressed_zlib_data()?)),
479 484 // zstd data.
480 485 b'\x28' => Ok(Cow::Owned(self.uncompressed_zstd_data()?)),
481 486 // A proper new format should have had a repo/store requirement.
482 487 format_type => Err(corrupted(format!(
483 488 "unknown compression header '{}'",
484 489 format_type
485 490 ))),
486 491 }
487 492 }
488 493
489 494 fn uncompressed_zlib_data(&self) -> Result<Vec<u8>, HgError> {
490 495 let mut decoder = ZlibDecoder::new(self.bytes);
491 496 if self.is_delta() {
492 497 let mut buf = Vec::with_capacity(self.compressed_len as usize);
493 498 decoder
494 499 .read_to_end(&mut buf)
495 500 .map_err(|e| corrupted(e.to_string()))?;
496 501 Ok(buf)
497 502 } else {
498 503 let cap = self.uncompressed_len.max(0) as usize;
499 504 let mut buf = vec![0; cap];
500 505 decoder
501 506 .read_exact(&mut buf)
502 507 .map_err(|e| corrupted(e.to_string()))?;
503 508 Ok(buf)
504 509 }
505 510 }
506 511
507 512 fn uncompressed_zstd_data(&self) -> Result<Vec<u8>, HgError> {
508 513 if self.is_delta() {
509 514 let mut buf = Vec::with_capacity(self.compressed_len as usize);
510 515 zstd::stream::copy_decode(self.bytes, &mut buf)
511 516 .map_err(|e| corrupted(e.to_string()))?;
512 517 Ok(buf)
513 518 } else {
514 519 let cap = self.uncompressed_len.max(0) as usize;
515 520 let mut buf = vec![0; cap];
516 521 let len = zstd::block::decompress_to_buffer(self.bytes, &mut buf)
517 522 .map_err(|e| corrupted(e.to_string()))?;
518 523 if len != self.uncompressed_len as usize {
519 524 Err(corrupted("uncompressed length does not match"))
520 525 } else {
521 526 Ok(buf)
522 527 }
523 528 }
524 529 }
525 530
526 531 /// Tell if the entry is a snapshot or a delta
527 532 /// (influences on decompression).
528 533 fn is_delta(&self) -> bool {
529 534 self.base_rev_or_base_of_delta_chain.is_some()
530 535 }
531 536 }
532 537
533 538 /// Calculate the hash of a revision given its data and its parents.
534 539 fn hash(
535 540 data: &[u8],
536 541 p1_hash: &[u8],
537 542 p2_hash: &[u8],
538 543 ) -> [u8; NODE_BYTES_LENGTH] {
539 544 let mut hasher = Sha1::new();
540 545 let (a, b) = (p1_hash, p2_hash);
541 546 if a > b {
542 547 hasher.update(b);
543 548 hasher.update(a);
544 549 } else {
545 550 hasher.update(a);
546 551 hasher.update(b);
547 552 }
548 553 hasher.update(data);
549 554 *hasher.finalize().as_ref()
550 555 }
551 556
552 557 #[cfg(test)]
553 558 mod tests {
554 559 use super::*;
555 560 use crate::index::{IndexEntryBuilder, INDEX_ENTRY_SIZE};
556 561 use itertools::Itertools;
557 562
558 563 #[test]
559 564 fn test_empty() {
560 565 let temp = tempfile::tempdir().unwrap();
561 566 let vfs = Vfs { base: temp.path() };
562 567 std::fs::write(temp.path().join("foo.i"), b"").unwrap();
563 568 let revlog = Revlog::open(&vfs, "foo.i", None, false).unwrap();
564 569 assert!(revlog.is_empty());
565 570 assert_eq!(revlog.len(), 0);
566 571 assert!(revlog.get_entry(0).is_err());
567 572 assert!(!revlog.has_rev(0));
568 573 }
569 574
570 575 #[test]
571 576 fn test_inline() {
572 577 let temp = tempfile::tempdir().unwrap();
573 578 let vfs = Vfs { base: temp.path() };
574 579 let node0 = Node::from_hex("2ed2a3912a0b24502043eae84ee4b279c18b90dd")
575 580 .unwrap();
576 581 let node1 = Node::from_hex("b004912a8510032a0350a74daa2803dadfb00e12")
577 582 .unwrap();
578 583 let node2 = Node::from_hex("dd6ad206e907be60927b5a3117b97dffb2590582")
579 584 .unwrap();
580 585 let entry0_bytes = IndexEntryBuilder::new()
581 586 .is_first(true)
582 587 .with_version(1)
583 588 .with_inline(true)
584 589 .with_offset(INDEX_ENTRY_SIZE)
585 590 .with_node(node0)
586 591 .build();
587 592 let entry1_bytes = IndexEntryBuilder::new()
588 593 .with_offset(INDEX_ENTRY_SIZE)
589 594 .with_node(node1)
590 595 .build();
591 596 let entry2_bytes = IndexEntryBuilder::new()
592 597 .with_offset(INDEX_ENTRY_SIZE)
593 598 .with_p1(0)
594 599 .with_p2(1)
595 600 .with_node(node2)
596 601 .build();
597 602 let contents = vec![entry0_bytes, entry1_bytes, entry2_bytes]
598 603 .into_iter()
599 604 .flatten()
600 605 .collect_vec();
601 606 std::fs::write(temp.path().join("foo.i"), contents).unwrap();
602 607 let revlog = Revlog::open(&vfs, "foo.i", None, false).unwrap();
603 608
604 609 let entry0 = revlog.get_entry(0).ok().unwrap();
605 610 assert_eq!(entry0.revision(), 0);
606 611 assert_eq!(*entry0.node(), node0);
607 612 assert!(!entry0.has_p1());
608 613 assert_eq!(entry0.p1(), None);
609 614 assert_eq!(entry0.p2(), None);
610 615 let p1_entry = entry0.p1_entry().unwrap();
611 616 assert!(p1_entry.is_none());
612 617 let p2_entry = entry0.p2_entry().unwrap();
613 618 assert!(p2_entry.is_none());
614 619
615 620 let entry1 = revlog.get_entry(1).ok().unwrap();
616 621 assert_eq!(entry1.revision(), 1);
617 622 assert_eq!(*entry1.node(), node1);
618 623 assert!(!entry1.has_p1());
619 624 assert_eq!(entry1.p1(), None);
620 625 assert_eq!(entry1.p2(), None);
621 626 let p1_entry = entry1.p1_entry().unwrap();
622 627 assert!(p1_entry.is_none());
623 628 let p2_entry = entry1.p2_entry().unwrap();
624 629 assert!(p2_entry.is_none());
625 630
626 631 let entry2 = revlog.get_entry(2).ok().unwrap();
627 632 assert_eq!(entry2.revision(), 2);
628 633 assert_eq!(*entry2.node(), node2);
629 634 assert!(entry2.has_p1());
630 635 assert_eq!(entry2.p1(), Some(0));
631 636 assert_eq!(entry2.p2(), Some(1));
632 637 let p1_entry = entry2.p1_entry().unwrap();
633 638 assert!(p1_entry.is_some());
634 639 assert_eq!(p1_entry.unwrap().revision(), 0);
635 640 let p2_entry = entry2.p2_entry().unwrap();
636 641 assert!(p2_entry.is_some());
637 642 assert_eq!(p2_entry.unwrap().revision(), 1);
638 643 }
639 644 }
General Comments 0
You need to be logged in to leave comments. Login now