1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
11
12use sley_config::{ConfigEntry, ConfigSection, GitConfig};
13use sley_core::{GitError, ObjectFormat, ObjectId, Result};
14use std::collections::{BTreeMap, HashMap};
15use std::env;
16use std::fs;
17use std::path::{Path, PathBuf};
18
19const REFTABLE_MAGIC: &[u8; 4] = b"REFT";
20const REFTABLE_DEFAULT_BLOCK_SIZE: u32 = 4096;
21const REFTABLE_MAX_BLOCK_SIZE: u32 = 0x00ff_ffff;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ReftableVersion {
25 V1,
26 V2,
27}
28
29impl ReftableVersion {
30 fn number(self) -> u8 {
31 match self {
32 Self::V1 => 1,
33 Self::V2 => 2,
34 }
35 }
36
37 fn header_len(self) -> usize {
38 match self {
39 Self::V1 => 24,
40 Self::V2 => 28,
41 }
42 }
43
44 fn footer_len(self) -> usize {
45 match self {
46 Self::V1 => 68,
47 Self::V2 => 72,
48 }
49 }
50
51 fn from_number(value: u8) -> Result<Self> {
52 match value {
53 1 => Ok(Self::V1),
54 2 => Ok(Self::V2),
55 other => Err(GitError::InvalidFormat(format!(
56 "unsupported reftable version {other}"
57 ))),
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct ReftableHeader {
64 pub version: ReftableVersion,
65 pub block_size: u32,
66 pub min_update_index: u64,
67 pub max_update_index: u64,
68 pub object_format: ObjectFormat,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum ReftableRefValue {
73 Deletion,
74 Direct(ObjectId),
75 Peeled { target: ObjectId, peeled: ObjectId },
76 Symbolic(String),
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct ReftableRefRecord {
81 pub name: String,
82 pub update_index: u64,
83 pub value: ReftableRefValue,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum ReftableLogValue {
91 Deletion,
94 Update(ReftableLogUpdate),
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ReftableLogUpdate {
103 pub old_oid: ObjectId,
104 pub new_oid: ObjectId,
105 pub name: String,
106 pub email: String,
107 pub time: u64,
109 pub tz_offset: i16,
112 pub message: String,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct ReftableLogRecord {
122 pub refname: String,
123 pub update_index: u64,
124 pub value: ReftableLogValue,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct ReftableWriteOptions {
133 pub block_size: u32,
136 pub restart_interval: u16,
138 pub index_objects: bool,
141}
142
143impl Default for ReftableWriteOptions {
144 fn default() -> Self {
145 Self {
146 block_size: REFTABLE_DEFAULT_BLOCK_SIZE,
147 restart_interval: 16,
148 index_objects: true,
149 }
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct Reftable {
155 pub header: ReftableHeader,
156 pub refs: Vec<ReftableRefRecord>,
157 pub logs: Vec<ReftableLogRecord>,
160}
161
162impl Reftable {
163 pub fn parse(bytes: &[u8]) -> Result<Self> {
164 let header = parse_reftable_header(bytes)?;
165 let footer = parse_reftable_footer(bytes, header.version)?;
166 if footer.version != header.version
167 || footer.block_size != header.block_size
168 || footer.min_update_index != header.min_update_index
169 || footer.max_update_index != header.max_update_index
170 || footer.object_format != header.object_format
171 {
172 return Err(GitError::InvalidFormat(
173 "reftable footer header does not match file header".into(),
174 ));
175 }
176 let footer_start = bytes.len() - header.version.footer_len();
177 if footer.obj_id_len > header.object_format.raw_len() as u8 {
178 return Err(GitError::InvalidFormat(
179 "reftable object id abbreviation length exceeds hash length".into(),
180 ));
181 }
182 let ref_end = [
183 footer.ref_index_position,
184 footer.obj_position,
185 footer.obj_index_position,
186 footer.log_position,
187 footer.log_index_position,
188 footer_start as u64,
189 ]
190 .into_iter()
191 .filter(|position| *position != 0)
192 .min()
193 .unwrap_or(footer_start as u64) as usize;
194 let mut refs = Vec::new();
195 let mut offset = header.version.header_len();
196 while offset < ref_end {
197 if bytes[offset] == 0 {
198 offset += 1;
199 continue;
200 }
201 let block_type = bytes[offset];
202 if block_type != b'r' {
203 break;
204 }
205 let block_len = read_u24(bytes, offset + 1)? as usize;
206 let block_end = if offset == header.version.header_len() {
207 block_len
208 } else {
209 offset
210 .checked_add(block_len)
211 .ok_or_else(|| GitError::InvalidFormat("reftable block overflow".into()))?
212 };
213 if block_end > ref_end || block_end > bytes.len() {
214 return Err(GitError::InvalidFormat(
215 "reftable ref block extends past section".into(),
216 ));
217 }
218 refs.extend(parse_reftable_ref_block(
219 &bytes[offset..block_end],
220 offset,
221 header,
222 )?);
223 offset = block_end;
224 }
225 let logs = parse_reftable_log_section(bytes, &footer, footer_start, header)?;
226 Ok(Self { header, refs, logs })
227 }
228
229 pub fn write_ref_only(
230 format: ObjectFormat,
231 min_update_index: u64,
232 max_update_index: u64,
233 refs: &[ReftableRefRecord],
234 ) -> Result<Vec<u8>> {
235 Self::write(format, min_update_index, max_update_index, refs, &[])
236 }
237
238 pub fn write(
246 format: ObjectFormat,
247 min_update_index: u64,
248 max_update_index: u64,
249 refs: &[ReftableRefRecord],
250 logs: &[ReftableLogRecord],
251 ) -> Result<Vec<u8>> {
252 Self::write_with_options(
253 format,
254 min_update_index,
255 max_update_index,
256 refs,
257 logs,
258 ReftableWriteOptions::default(),
259 )
260 }
261
262 pub fn write_with_options(
264 format: ObjectFormat,
265 min_update_index: u64,
266 max_update_index: u64,
267 refs: &[ReftableRefRecord],
268 logs: &[ReftableLogRecord],
269 mut options: ReftableWriteOptions,
270 ) -> Result<Vec<u8>> {
271 if options.block_size == 0 {
272 options.block_size = REFTABLE_DEFAULT_BLOCK_SIZE;
273 }
274 if options.restart_interval == 0 {
275 options.restart_interval = 16;
276 }
277 if options.block_size > REFTABLE_MAX_BLOCK_SIZE {
278 return Err(GitError::InvalidFormat(
279 "reftable block size exceeds maximum".into(),
280 ));
281 }
282 let version = match format {
283 ObjectFormat::Sha1 => ReftableVersion::V1,
284 ObjectFormat::Sha256 => ReftableVersion::V2,
285 };
286 let header = ReftableHeader {
287 version,
288 block_size: options.block_size,
289 min_update_index,
290 max_update_index,
291 object_format: format,
292 };
293 write_reftable_with_options(header, refs, logs, options)
294 }
295}
296
297#[derive(Debug, Clone)]
298struct ReftableWriterRecord {
299 key: Vec<u8>,
300 value_type: u8,
301 value: Vec<u8>,
302 object_ids: Vec<ObjectId>,
303}
304
305#[derive(Debug)]
306struct ReftableWriterBlock {
307 bytes: Vec<u8>,
308 last_key: Vec<u8>,
309 record_indices: Vec<usize>,
310 offset: u64,
311}
312
313fn write_reftable_with_options(
314 header: ReftableHeader,
315 refs: &[ReftableRefRecord],
316 logs: &[ReftableLogRecord],
317 options: ReftableWriteOptions,
318) -> Result<Vec<u8>> {
319 let mut ref_records = refs
320 .iter()
321 .map(|record| reftable_ref_writer_record(header, record))
322 .collect::<Result<Vec<_>>>()?;
323 ref_records.sort_by(|left, right| left.key.cmp(&right.key));
324
325 let mut log_records = logs
326 .iter()
327 .map(|record| reftable_log_writer_record(header.object_format, record))
328 .collect::<Result<Vec<_>>>()?;
329 log_records.sort_by(|left, right| left.key.cmp(&right.key));
330
331 let mut out = write_reftable_header(header)?;
332 let mut ref_index_position = 0;
333 let mut obj_position = 0;
334 let mut obj_id_len = 0;
335 let mut obj_index_position = 0;
336 let mut log_position = 0;
337 let mut log_index_position = 0;
338
339 let first_header_len = header.version.header_len();
340 let mut ref_blocks = if ref_records.is_empty() {
341 Vec::new()
342 } else {
343 build_reftable_blocks(
344 b'r',
345 &ref_records,
346 options.block_size,
347 first_header_len,
348 options.restart_interval,
349 false,
350 )?
351 };
352 if !ref_blocks.is_empty() {
353 append_first_reftable_section(&mut out, &mut ref_blocks, options.block_size)?;
354 }
355
356 if ref_blocks.len() > 3 {
357 ref_index_position = append_reftable_index_levels(&mut out, &ref_blocks, options, true)?;
358
359 if options.index_objects {
360 let object_records =
361 reftable_object_writer_records(&ref_records, &ref_blocks, header.object_format);
362 if !object_records.is_empty() {
363 obj_id_len = object_records[0].key.len() as u8;
364 let mut object_blocks = build_reftable_blocks(
365 b'o',
366 &object_records,
367 options.block_size,
368 0,
369 options.restart_interval,
370 false,
371 )?;
372 align_reftable_output(&mut out, options.block_size);
373 obj_position = out.len() as u64;
374 append_reftable_blocks(&mut out, &mut object_blocks, options.block_size, false);
375 if object_blocks.len() > 3 {
376 obj_index_position =
377 append_reftable_index_levels(&mut out, &object_blocks, options, true)?;
378 }
379 }
380 }
381 }
382
383 let first_log_header = if ref_blocks.is_empty() {
384 first_header_len
385 } else {
386 0
387 };
388 let mut log_blocks = if log_records.is_empty() {
389 Vec::new()
390 } else {
391 build_reftable_blocks(
392 b'g',
393 &log_records,
394 options.block_size,
395 first_log_header,
396 options.restart_interval,
397 true,
398 )?
399 };
400 if !log_blocks.is_empty() {
401 log_position = if first_log_header == 0 {
402 out.len() as u64
403 } else {
404 0
409 };
410 if first_log_header == 0 {
411 append_reftable_blocks(&mut out, &mut log_blocks, options.block_size, true);
412 } else {
413 append_first_reftable_section(&mut out, &mut log_blocks, options.block_size)?;
414 }
415 if log_blocks.len() > 3 {
416 log_index_position =
417 append_reftable_index_levels(&mut out, &log_blocks, options, false)?;
418 }
419 }
420
421 out.extend_from_slice(&write_reftable_footer(
422 header,
423 ref_index_position,
424 obj_position,
425 obj_id_len,
426 obj_index_position,
427 log_position,
428 log_index_position,
429 )?);
430 Ok(out)
431}
432
433fn reftable_ref_writer_record(
434 header: ReftableHeader,
435 record: &ReftableRefRecord,
436) -> Result<ReftableWriterRecord> {
437 if record.update_index < header.min_update_index
438 || record.update_index > header.max_update_index
439 {
440 return Err(GitError::InvalidFormat(format!(
441 "reftable ref {} update index {} outside header bounds",
442 record.name, record.update_index
443 )));
444 }
445 let mut value = Vec::new();
446 write_reftable_varint(&mut value, record.update_index - header.min_update_index);
447 let (value_type, object_ids) = match &record.value {
448 ReftableRefValue::Deletion => (0, Vec::new()),
449 ReftableRefValue::Direct(oid) => {
450 if oid.format() != header.object_format {
451 return Err(GitError::InvalidFormat(
452 "reftable direct ref object format mismatch".into(),
453 ));
454 }
455 value.extend_from_slice(oid.as_bytes());
456 (1, vec![*oid])
457 }
458 ReftableRefValue::Peeled { target, peeled } => {
459 if target.format() != header.object_format || peeled.format() != header.object_format {
460 return Err(GitError::InvalidFormat(
461 "reftable peeled ref object format mismatch".into(),
462 ));
463 }
464 value.extend_from_slice(target.as_bytes());
465 value.extend_from_slice(peeled.as_bytes());
466 (2, vec![*target, *peeled])
467 }
468 ReftableRefValue::Symbolic(target) => {
469 write_reftable_string(&mut value, target.as_bytes());
470 (3, Vec::new())
471 }
472 };
473 Ok(ReftableWriterRecord {
474 key: record.name.as_bytes().to_vec(),
475 value_type,
476 value,
477 object_ids,
478 })
479}
480
481fn reftable_log_writer_record(
482 format: ObjectFormat,
483 record: &ReftableLogRecord,
484) -> Result<ReftableWriterRecord> {
485 let mut value = Vec::new();
486 let value_type = match &record.value {
487 ReftableLogValue::Deletion => 0,
488 ReftableLogValue::Update(update) => {
489 write_reftable_log_value(&mut value, format, update)?;
490 1
491 }
492 };
493 Ok(ReftableWriterRecord {
494 key: reftable_log_key(&record.refname, record.update_index),
495 value_type,
496 value,
497 object_ids: Vec::new(),
498 })
499}
500
501fn build_reftable_blocks(
502 block_type: u8,
503 records: &[ReftableWriterRecord],
504 block_size: u32,
505 first_header_len: usize,
506 restart_interval: u16,
507 compress: bool,
508) -> Result<Vec<ReftableWriterBlock>> {
509 let block_size = block_size as usize;
510 let mut blocks = Vec::new();
511 let mut next_record = 0;
512 let mut header_len = first_header_len;
513 while next_record < records.len() {
514 let mut encoded = Vec::new();
515 let mut restart_offsets = Vec::new();
516 let mut record_indices = Vec::new();
517 let mut previous_key = Vec::new();
518 let block_begin = next_record;
519 while next_record < records.len() {
520 let record = &records[next_record];
521 let force_restart = record_indices.len() % usize::from(restart_interval) == 0;
522 let prefix_len = if force_restart {
523 0
524 } else {
525 common_prefix_len(&previous_key, &record.key)
526 };
527 let mut entry = Vec::new();
528 write_reftable_varint(&mut entry, prefix_len as u64);
529 let suffix = &record.key[prefix_len..];
530 write_reftable_varint(
531 &mut entry,
532 ((suffix.len() as u64) << 3) | u64::from(record.value_type),
533 );
534 entry.extend_from_slice(suffix);
535 entry.extend_from_slice(&record.value);
536 let is_restart = prefix_len == 0;
537 let restart_count = restart_offsets.len() + usize::from(is_restart);
538 let raw_len = header_len + 4 + encoded.len() + entry.len() + restart_count * 3 + 2;
539 if raw_len > block_size {
540 if record_indices.is_empty() {
541 return Err(GitError::InvalidFormat("entry too large".into()));
542 }
543 break;
544 }
545 if is_restart {
546 restart_offsets.push((header_len + 4 + encoded.len()) as u32);
547 }
548 encoded.extend_from_slice(&entry);
549 record_indices.push(next_record);
550 previous_key.clone_from(&record.key);
551 next_record += 1;
552 }
553
554 if next_record == block_begin {
555 return Err(GitError::InvalidFormat("entry too large".into()));
556 }
557 let last_key = records[next_record - 1].key.clone();
558 let mut body = Vec::new();
559 body.push(block_type);
560 body.extend_from_slice(&[0, 0, 0]);
561 body.extend_from_slice(&encoded);
562 for offset in &restart_offsets {
563 write_u24(&mut body, *offset)?;
564 }
565 let restart_count = u16::try_from(restart_offsets.len())
566 .map_err(|_| GitError::InvalidFormat("too many reftable restart offsets".into()))?;
567 body.extend_from_slice(&restart_count.to_be_bytes());
568 let raw_len = header_len + body.len();
569 write_u24_at(&mut body, 1, raw_len as u32)?;
570 if compress {
571 let compressed = deflate_zlib(&body[4..])?;
572 body.truncate(4);
573 body.extend_from_slice(&compressed);
574 }
575 blocks.push(ReftableWriterBlock {
576 bytes: body,
577 last_key,
578 record_indices,
579 offset: 0,
580 });
581 header_len = 0;
582 }
583 Ok(blocks)
584}
585
586fn append_first_reftable_section(
587 out: &mut Vec<u8>,
588 blocks: &mut [ReftableWriterBlock],
589 block_size: u32,
590) -> Result<()> {
591 for (index, block) in blocks.iter_mut().enumerate() {
592 if index == 0 {
593 block.offset = 0;
594 } else {
595 align_reftable_output(out, block_size);
596 block.offset = out.len() as u64;
597 }
598 out.extend_from_slice(&block.bytes);
599 }
600 Ok(())
601}
602
603fn append_reftable_blocks(
604 out: &mut Vec<u8>,
605 blocks: &mut [ReftableWriterBlock],
606 block_size: u32,
607 unpadded: bool,
608) {
609 for (index, block) in blocks.iter_mut().enumerate() {
610 if index != 0 && !unpadded {
611 align_reftable_output(out, block_size);
612 }
613 block.offset = out.len() as u64;
614 out.extend_from_slice(&block.bytes);
615 }
616}
617
618fn align_reftable_output(out: &mut Vec<u8>, block_size: u32) {
619 let block_size = block_size as usize;
620 let remainder = out.len() % block_size;
621 if remainder != 0 {
622 out.resize(out.len() + block_size - remainder, 0);
623 }
624}
625
626fn reftable_index_writer_records(blocks: &[ReftableWriterBlock]) -> Vec<ReftableWriterRecord> {
627 blocks
628 .iter()
629 .map(|block| {
630 let mut value = Vec::new();
631 write_reftable_varint(&mut value, block.offset);
632 ReftableWriterRecord {
633 key: block.last_key.clone(),
634 value_type: 0,
635 value,
636 object_ids: Vec::new(),
637 }
638 })
639 .collect()
640}
641
642fn append_reftable_index_levels(
643 out: &mut Vec<u8>,
644 indexed_blocks: &[ReftableWriterBlock],
645 options: ReftableWriteOptions,
646 align_first: bool,
647) -> Result<u64> {
648 let mut records = reftable_index_writer_records(indexed_blocks);
649 let mut first_level = true;
650 loop {
651 if (first_level && align_first) || !first_level {
652 align_reftable_output(out, options.block_size);
653 }
654 let highest_position = out.len() as u64;
655 let mut blocks = build_reftable_blocks(
656 b'i',
657 &records,
658 options.block_size,
659 0,
660 options.restart_interval,
661 false,
662 )?;
663 append_reftable_blocks(out, &mut blocks, options.block_size, false);
664 if blocks.len() <= 3 {
665 return Ok(highest_position);
666 }
667 records = reftable_index_writer_records(&blocks);
668 first_level = false;
669 }
670}
671
672fn reftable_object_writer_records(
673 records: &[ReftableWriterRecord],
674 blocks: &[ReftableWriterBlock],
675 format: ObjectFormat,
676) -> Vec<ReftableWriterRecord> {
677 let mut offsets = BTreeMap::<Vec<u8>, Vec<u64>>::new();
678 for block in blocks {
679 for index in &block.record_indices {
680 for oid in &records[*index].object_ids {
681 let entry = offsets.entry(oid.as_bytes().to_vec()).or_default();
682 if entry.last() != Some(&block.offset) {
683 entry.push(block.offset);
684 }
685 }
686 }
687 }
688 if offsets.is_empty() {
689 return Vec::new();
690 }
691 let keys = offsets.keys().cloned().collect::<Vec<_>>();
692 let common = keys
693 .windows(2)
694 .map(|pair| common_prefix_len(&pair[0], &pair[1]))
695 .max()
696 .unwrap_or(1);
697 let prefix_len = (common + 1).max(2).min(format.raw_len());
698 offsets
699 .into_iter()
700 .map(|(oid, block_offsets)| {
701 let count = block_offsets.len();
702 let value_type = if (1..8).contains(&count) {
703 count as u8
704 } else {
705 0
706 };
707 let mut value = Vec::new();
708 if value_type == 0 {
709 write_reftable_varint(&mut value, count as u64);
710 }
711 if let Some(first) = block_offsets.first() {
712 write_reftable_varint(&mut value, *first);
713 for pair in block_offsets.windows(2) {
714 write_reftable_varint(&mut value, pair[1] - pair[0]);
715 }
716 }
717 ReftableWriterRecord {
718 key: oid[..prefix_len].to_vec(),
719 value_type,
720 value,
721 object_ids: Vec::new(),
722 }
723 })
724 .collect()
725}
726
727#[derive(Debug, Clone, Copy)]
728struct ReftableFooter {
729 version: ReftableVersion,
730 block_size: u32,
731 min_update_index: u64,
732 max_update_index: u64,
733 object_format: ObjectFormat,
734 ref_index_position: u64,
735 obj_position: u64,
736 obj_id_len: u8,
737 obj_index_position: u64,
738 log_position: u64,
739 log_index_position: u64,
740}
741
742fn parse_reftable_header(bytes: &[u8]) -> Result<ReftableHeader> {
743 if bytes.len() < 24 {
744 return Err(GitError::InvalidFormat("truncated reftable header".into()));
745 }
746 if &bytes[..4] != REFTABLE_MAGIC {
747 return Err(GitError::InvalidFormat("missing reftable magic".into()));
748 }
749 let version = ReftableVersion::from_number(bytes[4])?;
750 if bytes.len() < version.header_len() {
751 return Err(GitError::InvalidFormat("truncated reftable header".into()));
752 }
753 let block_size = read_u24(bytes, 5)?;
754 let min_update_index = read_u64(bytes, 8)?;
755 let max_update_index = read_u64(bytes, 16)?;
756 let object_format = match version {
757 ReftableVersion::V1 => ObjectFormat::Sha1,
758 ReftableVersion::V2 => match bytes.get(24..28) {
759 Some(b"sha1") => ObjectFormat::Sha1,
760 Some(b"s256") => ObjectFormat::Sha256,
761 Some(value) => {
762 return Err(GitError::InvalidFormat(format!(
763 "unsupported reftable hash id {}",
764 String::from_utf8_lossy(value)
765 )));
766 }
767 None => return Err(GitError::InvalidFormat("truncated reftable hash id".into())),
768 },
769 };
770 Ok(ReftableHeader {
771 version,
772 block_size,
773 min_update_index,
774 max_update_index,
775 object_format,
776 })
777}
778
779fn write_reftable_header(header: ReftableHeader) -> Result<Vec<u8>> {
780 let mut out = Vec::with_capacity(header.version.header_len());
781 out.extend_from_slice(REFTABLE_MAGIC);
782 out.push(header.version.number());
783 write_u24(&mut out, header.block_size)?;
784 out.extend_from_slice(&header.min_update_index.to_be_bytes());
785 out.extend_from_slice(&header.max_update_index.to_be_bytes());
786 if header.version == ReftableVersion::V2 {
787 out.extend_from_slice(match header.object_format {
788 ObjectFormat::Sha1 => b"sha1",
789 ObjectFormat::Sha256 => b"s256",
790 });
791 }
792 Ok(out)
793}
794
795fn parse_reftable_footer(bytes: &[u8], version: ReftableVersion) -> Result<ReftableFooter> {
796 let footer_len = version.footer_len();
797 if bytes.len() < footer_len {
798 return Err(GitError::InvalidFormat("truncated reftable footer".into()));
799 }
800 let start = bytes.len() - footer_len;
801 let crc_start = bytes.len() - 4;
802 let expected = read_u32(bytes, crc_start)?;
803 let actual = crc32(&bytes[start..crc_start]);
804 if expected != actual {
805 return Err(GitError::InvalidFormat(format!(
806 "reftable footer crc mismatch: expected {expected:08x}, got {actual:08x}"
807 )));
808 }
809 let header = parse_reftable_header(&bytes[start..])?;
810 let mut offset = start + version.header_len();
811 let ref_index_position = read_u64(bytes, offset)?;
812 offset += 8;
813 let obj_position_and_len = read_u64(bytes, offset)?;
814 offset += 8;
815 let obj_index_position = read_u64(bytes, offset)?;
816 offset += 8;
817 let log_position = read_u64(bytes, offset)?;
818 offset += 8;
819 let log_index_position = read_u64(bytes, offset)?;
820 Ok(ReftableFooter {
821 version: header.version,
822 block_size: header.block_size,
823 min_update_index: header.min_update_index,
824 max_update_index: header.max_update_index,
825 object_format: header.object_format,
826 ref_index_position,
827 obj_position: obj_position_and_len >> 5,
828 obj_id_len: (obj_position_and_len & 0x1f) as u8,
829 obj_index_position,
830 log_position,
831 log_index_position,
832 })
833}
834
835fn write_reftable_footer(
836 header: ReftableHeader,
837 ref_index_position: u64,
838 obj_position: u64,
839 obj_id_len: u8,
840 obj_index_position: u64,
841 log_position: u64,
842 log_index_position: u64,
843) -> Result<Vec<u8>> {
844 if obj_id_len > 31 {
845 return Err(GitError::InvalidFormat(
846 "reftable object id abbreviation length exceeds 31".into(),
847 ));
848 }
849 let mut out = write_reftable_header(header)?;
850 out.extend_from_slice(&ref_index_position.to_be_bytes());
851 out.extend_from_slice(&((obj_position << 5) | u64::from(obj_id_len)).to_be_bytes());
852 out.extend_from_slice(&obj_index_position.to_be_bytes());
853 out.extend_from_slice(&log_position.to_be_bytes());
854 out.extend_from_slice(&log_index_position.to_be_bytes());
855 let crc = crc32(&out);
856 out.extend_from_slice(&crc.to_be_bytes());
857 Ok(out)
858}
859
860fn parse_reftable_ref_block(
861 block: &[u8],
862 block_start: usize,
863 header: ReftableHeader,
864) -> Result<Vec<ReftableRefRecord>> {
865 if block.len() < 6 || block[0] != b'r' {
866 return Err(GitError::InvalidFormat("invalid reftable ref block".into()));
867 }
868 let restart_count = read_u16(block, block.len() - 2)? as usize;
869 if restart_count == 0 {
870 return Err(GitError::InvalidFormat(
871 "reftable ref block has no restart offsets".into(),
872 ));
873 }
874 let restart_table_start = block
875 .len()
876 .checked_sub(2 + restart_count * 3)
877 .ok_or_else(|| GitError::InvalidFormat("truncated reftable restart table".into()))?;
878 let mut restart_offsets = Vec::with_capacity(restart_count);
879 for idx in 0..restart_count {
880 restart_offsets.push(read_u24(block, restart_table_start + idx * 3)? as usize);
881 }
882 if restart_offsets.windows(2).any(|pair| pair[0] > pair[1]) {
883 return Err(GitError::InvalidFormat(
884 "reftable restart offsets are not sorted".into(),
885 ));
886 }
887 let mut offset = 4;
888 let mut previous_name = Vec::new();
889 let mut records = Vec::new();
890 while offset < restart_table_start {
891 let record_offset = block_start + offset;
892 let restart = restart_offsets.contains(&record_offset);
893 let record = parse_reftable_ref_record(
894 block,
895 &mut offset,
896 restart_table_start,
897 header,
898 &previous_name,
899 restart,
900 )?;
901 previous_name = record.name.as_bytes().to_vec();
902 records.push(record);
903 }
904 if offset != restart_table_start {
905 return Err(GitError::InvalidFormat(
906 "reftable ref block ended inside record".into(),
907 ));
908 }
909 Ok(records)
910}
911
912fn parse_reftable_ref_record(
913 block: &[u8],
914 offset: &mut usize,
915 end: usize,
916 header: ReftableHeader,
917 previous_name: &[u8],
918 restart: bool,
919) -> Result<ReftableRefRecord> {
920 let prefix_len = read_reftable_varint(block, offset, end)? as usize;
921 if prefix_len > previous_name.len() {
922 return Err(GitError::InvalidFormat(
923 "reftable ref prefix exceeds previous name".into(),
924 ));
925 }
926 if restart && prefix_len != 0 {
927 return Err(GitError::InvalidFormat(
928 "reftable restart record uses prefix compression".into(),
929 ));
930 }
931 let suffix_len_and_type = read_reftable_varint(block, offset, end)?;
932 let suffix_len = (suffix_len_and_type >> 3) as usize;
933 let value_type = (suffix_len_and_type & 0x7) as u8;
934 let suffix_end = offset
935 .checked_add(suffix_len)
936 .ok_or_else(|| GitError::InvalidFormat("reftable suffix overflow".into()))?;
937 if suffix_end > end {
938 return Err(GitError::InvalidFormat("truncated reftable suffix".into()));
939 }
940 let mut name = previous_name[..prefix_len].to_vec();
941 name.extend_from_slice(&block[*offset..suffix_end]);
942 *offset = suffix_end;
943 let update_index_delta = read_reftable_varint(block, offset, end)?;
944 let update_index = header
945 .min_update_index
946 .checked_add(update_index_delta)
947 .ok_or_else(|| GitError::InvalidFormat("reftable update index overflow".into()))?;
948 let value = match value_type {
949 0 => ReftableRefValue::Deletion,
950 1 => ReftableRefValue::Direct(read_reftable_oid(block, offset, end, header.object_format)?),
951 2 => ReftableRefValue::Peeled {
952 target: read_reftable_oid(block, offset, end, header.object_format)?,
953 peeled: read_reftable_oid(block, offset, end, header.object_format)?,
954 },
955 3 => {
956 let target_len = read_reftable_varint(block, offset, end)? as usize;
957 let target_end = offset.checked_add(target_len).ok_or_else(|| {
958 GitError::InvalidFormat("reftable symbolic target overflow".into())
959 })?;
960 if target_end > end {
961 return Err(GitError::InvalidFormat(
962 "truncated reftable symbolic target".into(),
963 ));
964 }
965 let target = std::str::from_utf8(&block[*offset..target_end])
966 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
967 .to_string();
968 *offset = target_end;
969 ReftableRefValue::Symbolic(target)
970 }
971 other => {
972 return Err(GitError::InvalidFormat(format!(
973 "unsupported reftable ref value type {other}"
974 )));
975 }
976 };
977 let name = std::str::from_utf8(&name)
978 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
979 .to_string();
980 Ok(ReftableRefRecord {
981 name,
982 update_index,
983 value,
984 })
985}
986
987fn reftable_log_key(refname: &str, update_index: u64) -> Vec<u8> {
991 let mut key = Vec::with_capacity(refname.len() + 9);
992 key.extend_from_slice(refname.as_bytes());
993 key.push(0);
994 let ts = u64::MAX - update_index;
995 key.extend_from_slice(&ts.to_be_bytes());
996 key
997}
998
999fn write_reftable_log_value(
1000 out: &mut Vec<u8>,
1001 format: ObjectFormat,
1002 update: &ReftableLogUpdate,
1003) -> Result<()> {
1004 if update.old_oid.format() != format || update.new_oid.format() != format {
1005 return Err(GitError::InvalidFormat(
1006 "reftable log object format mismatch".into(),
1007 ));
1008 }
1009 out.extend_from_slice(update.old_oid.as_bytes());
1010 out.extend_from_slice(update.new_oid.as_bytes());
1011 write_reftable_string(out, update.name.as_bytes());
1012 write_reftable_string(out, update.email.as_bytes());
1013 write_reftable_varint(out, update.time);
1014 out.extend_from_slice(&update.tz_offset.to_be_bytes());
1015 write_reftable_string(out, update.message.as_bytes());
1016 Ok(())
1017}
1018
1019fn write_reftable_string(out: &mut Vec<u8>, bytes: &[u8]) {
1021 write_reftable_varint(out, bytes.len() as u64);
1022 out.extend_from_slice(bytes);
1023}
1024
1025fn read_reftable_string(block: &[u8], offset: &mut usize, end: usize) -> Result<String> {
1026 let len = read_reftable_varint(block, offset, end)? as usize;
1027 let str_end = offset
1028 .checked_add(len)
1029 .ok_or_else(|| GitError::InvalidFormat("reftable string overflow".into()))?;
1030 if str_end > end {
1031 return Err(GitError::InvalidFormat("truncated reftable string".into()));
1032 }
1033 let value = std::str::from_utf8(&block[*offset..str_end])
1034 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
1035 .to_string();
1036 *offset = str_end;
1037 Ok(value)
1038}
1039
1040fn common_prefix_len(left: &[u8], right: &[u8]) -> usize {
1041 left.iter()
1042 .zip(right.iter())
1043 .take_while(|(a, b)| a == b)
1044 .count()
1045}
1046
1047fn parse_reftable_log_section(
1051 bytes: &[u8],
1052 footer: &ReftableFooter,
1053 footer_start: usize,
1054 header: ReftableHeader,
1055) -> Result<Vec<ReftableLogRecord>> {
1056 let first_block = header.version.header_len();
1057 let log_position = if footer.log_position != 0 {
1058 footer.log_position as usize
1059 } else if bytes.get(first_block) == Some(&b'g') {
1060 first_block
1061 } else {
1062 return Ok(Vec::new());
1063 };
1064 let log_end = [footer.log_index_position, footer_start as u64]
1065 .into_iter()
1066 .filter(|position| *position != 0)
1067 .min()
1068 .unwrap_or(footer_start as u64) as usize;
1069 let mut logs = Vec::new();
1070 let mut offset = log_position;
1071 while offset < log_end {
1072 if bytes[offset] == 0 {
1073 offset += 1;
1074 continue;
1075 }
1076 if bytes[offset] != b'g' {
1077 break;
1078 }
1079 let uncompressed_len = read_u24(bytes, offset + 1)? as usize;
1080 let compressed = &bytes[offset + 4..log_end];
1084 let (inflated, consumed) = inflate_zlib(compressed, uncompressed_len.saturating_sub(4))?;
1085 let mut block = Vec::with_capacity(uncompressed_len);
1087 block.extend_from_slice(&bytes[offset..offset + 4]);
1088 block.extend_from_slice(&inflated);
1089 logs.extend(parse_reftable_log_block(&block, header)?);
1090 offset += 4 + consumed;
1091 }
1092 Ok(logs)
1093}
1094
1095fn parse_reftable_log_block(
1096 block: &[u8],
1097 header: ReftableHeader,
1098) -> Result<Vec<ReftableLogRecord>> {
1099 if block.len() < 6 || block[0] != b'g' {
1100 return Err(GitError::InvalidFormat("invalid reftable log block".into()));
1101 }
1102 let restart_count = read_u16(block, block.len() - 2)? as usize;
1103 if restart_count == 0 {
1104 return Err(GitError::InvalidFormat(
1105 "reftable log block has no restart offsets".into(),
1106 ));
1107 }
1108 let restart_table_start = block
1109 .len()
1110 .checked_sub(2 + restart_count * 3)
1111 .ok_or_else(|| GitError::InvalidFormat("truncated reftable log restart table".into()))?;
1112 let mut offset = 4;
1113 let mut previous_key: Vec<u8> = Vec::new();
1114 let mut records = Vec::new();
1115 while offset < restart_table_start {
1116 let record = parse_reftable_log_record(
1117 block,
1118 &mut offset,
1119 restart_table_start,
1120 header,
1121 &mut previous_key,
1122 )?;
1123 records.push(record);
1124 }
1125 if offset != restart_table_start {
1126 return Err(GitError::InvalidFormat(
1127 "reftable log block ended inside record".into(),
1128 ));
1129 }
1130 Ok(records)
1131}
1132
1133fn parse_reftable_log_record(
1134 block: &[u8],
1135 offset: &mut usize,
1136 end: usize,
1137 header: ReftableHeader,
1138 previous_key: &mut Vec<u8>,
1139) -> Result<ReftableLogRecord> {
1140 let prefix_len = read_reftable_varint(block, offset, end)? as usize;
1141 if prefix_len > previous_key.len() {
1142 return Err(GitError::InvalidFormat(
1143 "reftable log prefix exceeds previous key".into(),
1144 ));
1145 }
1146 let suffix_len_and_type = read_reftable_varint(block, offset, end)?;
1147 let suffix_len = (suffix_len_and_type >> 3) as usize;
1148 let value_type = (suffix_len_and_type & 0x7) as u8;
1149 let suffix_end = offset
1150 .checked_add(suffix_len)
1151 .ok_or_else(|| GitError::InvalidFormat("reftable log suffix overflow".into()))?;
1152 if suffix_end > end {
1153 return Err(GitError::InvalidFormat(
1154 "truncated reftable log suffix".into(),
1155 ));
1156 }
1157 let mut key = previous_key[..prefix_len].to_vec();
1158 key.extend_from_slice(&block[*offset..suffix_end]);
1159 *offset = suffix_end;
1160 if key.len() < 9 || key[key.len() - 9] != 0 {
1163 return Err(GitError::InvalidFormat("malformed reftable log key".into()));
1164 }
1165 let refname = std::str::from_utf8(&key[..key.len() - 9])
1166 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
1167 .to_string();
1168 let ts_bytes: [u8; 8] = key[key.len() - 8..]
1169 .try_into()
1170 .map_err(|_| GitError::InvalidFormat("truncated reftable log timestamp".into()))?;
1171 let update_index = u64::MAX - u64::from_be_bytes(ts_bytes);
1172 *previous_key = key;
1173 let value = match value_type {
1174 0 => ReftableLogValue::Deletion,
1175 1 => {
1176 let old_oid = read_reftable_oid(block, offset, end, header.object_format)?;
1177 let new_oid = read_reftable_oid(block, offset, end, header.object_format)?;
1178 let name = read_reftable_string(block, offset, end)?;
1179 let email = read_reftable_string(block, offset, end)?;
1180 let time = read_reftable_varint(block, offset, end)?;
1181 let tz_offset = i16::from_be_bytes([
1182 *block
1183 .get(*offset)
1184 .ok_or_else(|| GitError::InvalidFormat("truncated reftable log tz".into()))?,
1185 *block
1186 .get(*offset + 1)
1187 .ok_or_else(|| GitError::InvalidFormat("truncated reftable log tz".into()))?,
1188 ]);
1189 *offset += 2;
1190 let message = read_reftable_string(block, offset, end)?;
1191 ReftableLogValue::Update(ReftableLogUpdate {
1192 old_oid,
1193 new_oid,
1194 name,
1195 email,
1196 time,
1197 tz_offset,
1198 message,
1199 })
1200 }
1201 other => {
1202 return Err(GitError::InvalidFormat(format!(
1203 "unsupported reftable log value type {other}"
1204 )));
1205 }
1206 };
1207 Ok(ReftableLogRecord {
1208 refname,
1209 update_index,
1210 value,
1211 })
1212}
1213
1214fn deflate_zlib(data: &[u8]) -> Result<Vec<u8>> {
1217 use flate2::{Compression, write::ZlibEncoder};
1218 use std::io::Write;
1219 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::new(9));
1220 encoder
1221 .write_all(data)
1222 .map_err(|err| GitError::InvalidFormat(format!("reftable log deflate failed: {err}")))?;
1223 encoder
1224 .finish()
1225 .map_err(|err| GitError::InvalidFormat(format!("reftable log deflate failed: {err}")))
1226}
1227
1228fn inflate_zlib(data: &[u8], expected_len: usize) -> Result<(Vec<u8>, usize)> {
1232 use flate2::Decompress;
1233 use flate2::FlushDecompress;
1234 let mut decoder = Decompress::new(true);
1235 let mut out = Vec::with_capacity(expected_len);
1236 decoder
1237 .decompress_vec(data, &mut out, FlushDecompress::Finish)
1238 .map_err(|err| GitError::InvalidFormat(format!("reftable log inflate failed: {err}")))?;
1239 Ok((out, decoder.total_in() as usize))
1240}
1241
1242fn read_reftable_oid(
1243 block: &[u8],
1244 offset: &mut usize,
1245 end: usize,
1246 format: ObjectFormat,
1247) -> Result<ObjectId> {
1248 let oid_end = offset
1249 .checked_add(format.raw_len())
1250 .ok_or_else(|| GitError::InvalidFormat("reftable object id overflow".into()))?;
1251 if oid_end > end {
1252 return Err(GitError::InvalidFormat(
1253 "truncated reftable object id".into(),
1254 ));
1255 }
1256 let oid = ObjectId::from_raw(format, &block[*offset..oid_end])?;
1257 *offset = oid_end;
1258 Ok(oid)
1259}
1260
1261fn read_reftable_varint(bytes: &[u8], offset: &mut usize, end: usize) -> Result<u64> {
1262 if *offset >= end {
1263 return Err(GitError::InvalidFormat("truncated reftable varint".into()));
1264 }
1265 let mut value = u64::from(bytes[*offset] & 0x7f);
1266 while bytes[*offset] & 0x80 != 0 {
1267 *offset += 1;
1268 if *offset >= end {
1269 return Err(GitError::InvalidFormat("truncated reftable varint".into()));
1270 }
1271 value = value
1272 .checked_add(1)
1273 .and_then(|value| value.checked_shl(7))
1274 .ok_or_else(|| GitError::InvalidFormat("reftable varint overflow".into()))?
1275 | u64::from(bytes[*offset] & 0x7f);
1276 }
1277 *offset += 1;
1278 Ok(value)
1279}
1280
1281fn write_reftable_varint(out: &mut Vec<u8>, mut value: u64) {
1282 let mut bytes = [0u8; 10];
1283 let mut pos = bytes.len() - 1;
1284 bytes[pos] = (value & 0x7f) as u8;
1285 while value > 0x7f {
1286 value = (value >> 7) - 1;
1287 pos -= 1;
1288 bytes[pos] = ((value & 0x7f) as u8) | 0x80;
1289 }
1290 out.extend_from_slice(&bytes[pos..]);
1291}
1292
1293fn read_u16(bytes: &[u8], offset: usize) -> Result<u16> {
1294 let raw = bytes
1295 .get(offset..offset + 2)
1296 .ok_or_else(|| GitError::InvalidFormat("truncated uint16".into()))?;
1297 Ok(u16::from_be_bytes([raw[0], raw[1]]))
1298}
1299
1300fn read_u24(bytes: &[u8], offset: usize) -> Result<u32> {
1301 let raw = bytes
1302 .get(offset..offset + 3)
1303 .ok_or_else(|| GitError::InvalidFormat("truncated uint24".into()))?;
1304 Ok((u32::from(raw[0]) << 16) | (u32::from(raw[1]) << 8) | u32::from(raw[2]))
1305}
1306
1307fn write_u24(out: &mut Vec<u8>, value: u32) -> Result<()> {
1308 if value > REFTABLE_MAX_BLOCK_SIZE {
1309 return Err(GitError::InvalidFormat(format!(
1310 "uint24 value {value} exceeds maximum"
1311 )));
1312 }
1313 out.push((value >> 16) as u8);
1314 out.push((value >> 8) as u8);
1315 out.push(value as u8);
1316 Ok(())
1317}
1318
1319fn write_u24_at(out: &mut [u8], offset: usize, value: u32) -> Result<()> {
1320 if value > REFTABLE_MAX_BLOCK_SIZE {
1321 return Err(GitError::InvalidFormat(format!(
1322 "uint24 value {value} exceeds maximum"
1323 )));
1324 }
1325 let target = out
1326 .get_mut(offset..offset + 3)
1327 .ok_or_else(|| GitError::InvalidFormat("uint24 write is out of bounds".into()))?;
1328 target[0] = (value >> 16) as u8;
1329 target[1] = (value >> 8) as u8;
1330 target[2] = value as u8;
1331 Ok(())
1332}
1333
1334fn read_u32(bytes: &[u8], offset: usize) -> Result<u32> {
1335 let raw = bytes
1336 .get(offset..offset + 4)
1337 .ok_or_else(|| GitError::InvalidFormat("truncated uint32".into()))?;
1338 Ok(u32::from_be_bytes([raw[0], raw[1], raw[2], raw[3]]))
1339}
1340
1341fn read_u64(bytes: &[u8], offset: usize) -> Result<u64> {
1342 let raw = bytes
1343 .get(offset..offset + 8)
1344 .ok_or_else(|| GitError::InvalidFormat("truncated uint64".into()))?;
1345 Ok(u64::from_be_bytes([
1346 raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
1347 ]))
1348}
1349
1350fn crc32(bytes: &[u8]) -> u32 {
1351 let mut crc = 0xffff_ffffu32;
1352 for byte in bytes {
1353 crc ^= u32::from(*byte);
1354 for _ in 0..8 {
1355 let mask = 0u32.wrapping_sub(crc & 1);
1356 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
1357 }
1358 }
1359 !crc
1360}
1361
1362#[derive(Debug, Clone, PartialEq, Eq)]
1363pub struct CommitGraph {
1364 pub version: u8,
1365 pub format: ObjectFormat,
1366 pub base_graph_count: u8,
1367 pub fanout: [u32; 256],
1368 pub commits: Vec<CommitGraphEntry>,
1369 pub chunks: Vec<CommitGraphChunk>,
1370 pub base_graphs: Vec<ObjectId>,
1371 pub bloom_filters: Option<CommitGraphBloomFilters>,
1372 pub checksum: ObjectId,
1373}
1374
1375#[derive(Debug, Clone, PartialEq, Eq)]
1376pub struct CommitGraphEntry {
1377 pub oid: ObjectId,
1378 pub tree: ObjectId,
1379 pub parents: Vec<u32>,
1380 pub generation: u32,
1381 pub commit_time: u64,
1382 pub corrected_commit_date_offset: Option<u64>,
1383}
1384
1385impl CommitGraphEntry {
1386 pub fn parent_indices(&self) -> impl ExactSizeIterator<Item = u32> + '_ {
1389 self.parents.iter().copied()
1390 }
1391
1392 pub fn first_parent_index(&self) -> Option<u32> {
1393 self.parents.first().copied()
1394 }
1395}
1396
1397pub struct CommitGraphParentOids<'a> {
1399 graph: &'a CommitGraph,
1400 parents: std::slice::Iter<'a, u32>,
1401}
1402
1403impl Iterator for CommitGraphParentOids<'_> {
1404 type Item = ObjectId;
1405
1406 fn next(&mut self) -> Option<Self::Item> {
1407 let parent = *self.parents.next()?;
1408 let idx = usize::try_from(parent).ok()?;
1409 self.graph.commits.get(idx).map(|entry| entry.oid)
1410 }
1411
1412 fn size_hint(&self) -> (usize, Option<usize>) {
1413 self.parents.size_hint()
1414 }
1415}
1416
1417impl ExactSizeIterator for CommitGraphParentOids<'_> {}
1418
1419#[derive(Debug, Clone, PartialEq, Eq)]
1420pub struct CommitGraphChunk {
1421 pub id: [u8; 4],
1422 pub offset: u64,
1423 pub len: u64,
1424}
1425
1426#[derive(Debug, Clone, PartialEq, Eq)]
1427pub struct CommitGraphBloomFilters {
1428 pub hash_version: u32,
1429 pub hash_count: u32,
1430 pub bits_per_entry: u32,
1431 pub filters: Vec<Vec<u8>>,
1432}
1433
1434#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1435pub struct CommitGraphBloomSettings {
1436 pub hash_version: u32,
1437 pub hash_count: u32,
1438 pub bits_per_entry: u32,
1439 pub max_changed_paths: usize,
1440}
1441
1442pub const DEFAULT_COMMIT_GRAPH_BLOOM_SETTINGS: CommitGraphBloomSettings =
1443 CommitGraphBloomSettings {
1444 hash_version: 1,
1445 hash_count: 7,
1446 bits_per_entry: 10,
1447 max_changed_paths: 512,
1448 };
1449
1450#[derive(Debug, Clone, PartialEq, Eq)]
1451pub struct CommitGraphWriteEntry {
1452 pub oid: ObjectId,
1453 pub tree: ObjectId,
1454 pub parents: Vec<ObjectId>,
1455 pub generation: u32,
1456 pub commit_time: u64,
1457 pub bloom_filter: Option<Vec<u8>>,
1458}
1459
1460impl CommitGraph {
1461 pub fn write(format: ObjectFormat, entries: &[CommitGraphWriteEntry]) -> Result<Vec<u8>> {
1462 Self::write_with_bloom_settings(format, entries, DEFAULT_COMMIT_GRAPH_BLOOM_SETTINGS)
1463 }
1464
1465 pub fn write_with_bloom_settings(
1466 format: ObjectFormat,
1467 entries: &[CommitGraphWriteEntry],
1468 bloom_settings: CommitGraphBloomSettings,
1469 ) -> Result<Vec<u8>> {
1470 Self::write_with_options(format, entries, bloom_settings, true)
1473 }
1474
1475 pub fn write_with_options(
1480 format: ObjectFormat,
1481 entries: &[CommitGraphWriteEntry],
1482 bloom_settings: CommitGraphBloomSettings,
1483 write_generation_data: bool,
1484 ) -> Result<Vec<u8>> {
1485 validate_commit_graph_bloom_settings(bloom_settings)?;
1486 let mut entries = entries.to_vec();
1487 entries.sort_by(|left, right| left.oid.as_bytes().cmp(right.oid.as_bytes()));
1488 validate_commit_graph_write_entries(format, &entries)?;
1489 let object_ids = entries.iter().map(|entry| entry.oid).collect::<Vec<_>>();
1490 let (cdat, edge) = write_commit_graph_commit_data(&entries)?;
1491 let mut chunks = vec![
1492 (*b"OIDF", write_commit_graph_fanout(&object_ids)?),
1493 (*b"OIDL", write_commit_graph_oid_lookup(&object_ids)),
1494 (*b"CDAT", cdat),
1495 ];
1496 if write_generation_data {
1497 chunks.push((*b"GDA2", write_commit_graph_generation_data(&entries)?));
1498 let gdo2 = write_commit_graph_generation_overflow(&entries)?;
1499 if !gdo2.is_empty() {
1500 chunks.push((*b"GDO2", gdo2));
1501 }
1502 }
1503 if !edge.is_empty() {
1504 chunks.push((*b"EDGE", edge));
1505 }
1506 let bloom = write_commit_graph_bloom_filters(&entries, bloom_settings);
1507 if let Some((bidx, bdat)) = bloom {
1508 chunks.push((*b"BIDX", bidx));
1509 chunks.push((*b"BDAT", bdat));
1510 }
1511 write_commit_graph_chunks(format, 0, &chunks)
1512 }
1513
1514 pub fn write_with_base_options(
1521 format: ObjectFormat,
1522 entries: &[CommitGraphWriteEntry],
1523 bloom_settings: CommitGraphBloomSettings,
1524 write_generation_data: bool,
1525 base_graphs: &[ObjectId],
1526 base_oids: &[ObjectId],
1527 ) -> Result<Vec<u8>> {
1528 validate_commit_graph_bloom_settings(bloom_settings)?;
1529 if base_graphs.len() > u8::MAX as usize {
1530 return Err(GitError::InvalidFormat(
1531 "too many commit-graph base layers".into(),
1532 ));
1533 }
1534 for oid in base_graphs.iter().chain(base_oids.iter()) {
1535 if oid.format() != format {
1536 return Err(GitError::InvalidObjectId(
1537 "commit-graph base format does not match graph format".into(),
1538 ));
1539 }
1540 }
1541 let mut entries = entries.to_vec();
1542 entries.sort_by(|left, right| left.oid.as_bytes().cmp(right.oid.as_bytes()));
1543 validate_commit_graph_write_entries(format, &entries)?;
1544 let object_ids = entries.iter().map(|entry| entry.oid).collect::<Vec<_>>();
1545 let parent_positions = commit_graph_parent_positions(&entries, base_oids)?;
1546 let (cdat, edge) =
1547 write_commit_graph_commit_data_with_positions(&entries, &parent_positions)?;
1548 let mut chunks = vec![
1549 (*b"OIDF", write_commit_graph_fanout(&object_ids)?),
1550 (*b"OIDL", write_commit_graph_oid_lookup(&object_ids)),
1551 (*b"CDAT", cdat),
1552 ];
1553 if write_generation_data {
1554 if base_oids.is_empty() {
1555 chunks.push((*b"GDA2", write_commit_graph_generation_data(&entries)?));
1556 let gdo2 = write_commit_graph_generation_overflow(&entries)?;
1557 if !gdo2.is_empty() {
1558 chunks.push((*b"GDO2", gdo2));
1559 }
1560 } else {
1561 chunks.push((*b"GDA2", vec![0; entries.len() * 4]));
1562 }
1563 }
1564 if !edge.is_empty() {
1565 chunks.push((*b"EDGE", edge));
1566 }
1567 let bloom = write_commit_graph_bloom_filters(&entries, bloom_settings);
1568 if let Some((bidx, bdat)) = bloom {
1569 chunks.push((*b"BIDX", bidx));
1570 chunks.push((*b"BDAT", bdat));
1571 }
1572 if !base_graphs.is_empty() {
1573 let mut base = Vec::with_capacity(base_graphs.len() * format.raw_len());
1574 for oid in base_graphs {
1575 base.extend_from_slice(oid.as_bytes());
1576 }
1577 chunks.push((*b"BASE", base));
1578 }
1579 write_commit_graph_chunks(format, base_graphs.len() as u8, &chunks)
1580 }
1581
1582 pub fn parse(bytes: &[u8], format: ObjectFormat) -> Result<Self> {
1583 let hash_len = format.raw_len();
1584 if bytes.len() < 8 + 12 + hash_len {
1585 return Err(GitError::InvalidFormat(
1586 "commit-graph file too short".into(),
1587 ));
1588 }
1589 if &bytes[..4] != b"CGPH" {
1590 return Err(GitError::InvalidFormat(
1591 "missing commit-graph signature".into(),
1592 ));
1593 }
1594 let version = bytes[4];
1595 if version != 1 {
1596 return Err(GitError::Unsupported(format!(
1597 "commit-graph version {version}"
1598 )));
1599 }
1600 let hash_id = bytes[5];
1601 if u32::from(hash_id) != hash_function_id(format) {
1602 return Err(GitError::InvalidFormat(format!(
1603 "commit-graph hash id {hash_id} does not match {}",
1604 format.name()
1605 )));
1606 }
1607 let chunk_count = bytes[6] as usize;
1608 let base_graph_count = bytes[7];
1609 let lookup_len = (chunk_count + 1)
1610 .checked_mul(12)
1611 .ok_or_else(|| GitError::InvalidFormat("commit-graph lookup overflow".into()))?;
1612 let data_start = 8usize
1613 .checked_add(lookup_len)
1614 .ok_or_else(|| GitError::InvalidFormat("commit-graph lookup overflow".into()))?;
1615 let checksum_offset = bytes.len() - hash_len;
1616 if data_start > checksum_offset {
1617 return Err(GitError::InvalidFormat(
1618 "truncated commit-graph chunk lookup".into(),
1619 ));
1620 }
1621
1622 let actual_checksum = sley_core::digest_bytes(format, &bytes[..checksum_offset])?;
1623 let checksum = ObjectId::from_raw(format, &bytes[checksum_offset..])?;
1624 if actual_checksum != checksum {
1625 return Err(GitError::InvalidFormat(format!(
1626 "commit-graph checksum mismatch: expected {checksum}, got {actual_checksum}"
1627 )));
1628 }
1629
1630 let mut lookup = Vec::with_capacity(chunk_count + 1);
1631 let mut offset = 8usize;
1632 for _ in 0..=chunk_count {
1633 let id = [
1634 bytes[offset],
1635 bytes[offset + 1],
1636 bytes[offset + 2],
1637 bytes[offset + 3],
1638 ];
1639 let chunk_offset = u64_be(&bytes[offset + 4..offset + 12]);
1640 lookup.push((id, chunk_offset));
1641 offset += 12;
1642 }
1643 let Some((terminator_id, terminator_offset)) = lookup.last().copied() else {
1644 return Err(GitError::InvalidFormat(
1645 "commit-graph chunk lookup is empty".into(),
1646 ));
1647 };
1648 if terminator_id != [0, 0, 0, 0] {
1649 return Err(GitError::InvalidFormat(
1650 "commit-graph chunk lookup missing terminator".into(),
1651 ));
1652 }
1653 if terminator_offset != checksum_offset as u64 {
1654 return Err(GitError::InvalidFormat(
1655 "commit-graph terminator does not point at checksum".into(),
1656 ));
1657 }
1658
1659 let mut chunks = Vec::with_capacity(chunk_count);
1660 let mut previous_offset = data_start as u64;
1661 for pair in lookup.windows(2) {
1662 let (id, chunk_offset) = pair[0];
1663 let (_next_id, next_offset) = pair[1];
1664 if id == [0, 0, 0, 0] {
1665 return Err(GitError::InvalidFormat(
1666 "commit-graph chunk id is zero before terminator".into(),
1667 ));
1668 }
1669 if chunks.iter().any(|chunk: &CommitGraphChunk| chunk.id == id) {
1670 return Err(GitError::InvalidFormat(
1671 "commit-graph chunk id is duplicated".into(),
1672 ));
1673 }
1674 if chunk_offset < data_start as u64 || chunk_offset < previous_offset {
1675 return Err(GitError::InvalidFormat(
1676 "commit-graph chunk offsets are not monotonic".into(),
1677 ));
1678 }
1679 if next_offset < chunk_offset || next_offset > checksum_offset as u64 {
1680 return Err(GitError::InvalidFormat(
1681 "commit-graph chunk length is invalid".into(),
1682 ));
1683 }
1684 chunks.push(CommitGraphChunk {
1685 id,
1686 offset: chunk_offset,
1687 len: next_offset - chunk_offset,
1688 });
1689 previous_offset = chunk_offset;
1690 }
1691
1692 let (fanout, commit_count) = parse_commit_graph_fanout(bytes, &chunks)?;
1693 let oids = parse_commit_graph_oids(bytes, &chunks, format, commit_count, &fanout)?;
1694 let mut commits =
1695 parse_commit_graph_commit_data(bytes, &chunks, format, oids, base_graph_count)?;
1696 apply_commit_graph_generation_data(bytes, &chunks, &mut commits)?;
1697 let bloom_filters = parse_commit_graph_bloom_filters(bytes, &chunks, commits.len())?;
1698 let base_graphs =
1699 parse_commit_graph_base_graphs(bytes, &chunks, format, base_graph_count as usize)?;
1700
1701 Ok(Self {
1702 version,
1703 format,
1704 base_graph_count,
1705 fanout,
1706 commits,
1707 chunks,
1708 base_graphs,
1709 bloom_filters,
1710 checksum,
1711 })
1712 }
1713
1714 pub fn find(&self, oid: &ObjectId) -> Option<&CommitGraphEntry> {
1715 self.commits
1716 .binary_search_by(|entry| entry.oid.as_bytes().cmp(oid.as_bytes()))
1717 .ok()
1718 .map(|idx| &self.commits[idx])
1719 }
1720
1721 pub fn parent_oids<'a>(
1727 &'a self,
1728 entry: &'a CommitGraphEntry,
1729 ) -> Result<CommitGraphParentOids<'a>> {
1730 for parent in entry.parent_indices() {
1731 let idx = usize::try_from(parent).map_err(|_| {
1732 GitError::InvalidFormat("commit-graph parent index overflow".into())
1733 })?;
1734 if idx >= self.commits.len() {
1735 return Err(GitError::InvalidFormat(
1736 "commit-graph parent points past commit table".into(),
1737 ));
1738 }
1739 }
1740 Ok(CommitGraphParentOids {
1741 graph: self,
1742 parents: entry.parents.iter(),
1743 })
1744 }
1745}
1746
1747impl CommitGraphBloomFilters {
1748 pub fn filter_for_commit(&self, commit_index: usize) -> Option<&[u8]> {
1749 self.filters.get(commit_index).map(Vec::as_slice)
1750 }
1751
1752 pub fn contains_path(&self, commit_index: usize, path: &[u8]) -> Option<bool> {
1753 let filter = self.filter_for_commit(commit_index)?;
1754 Some(commit_graph_bloom_filter_contains(
1755 filter,
1756 path,
1757 CommitGraphBloomSettings {
1758 hash_version: self.hash_version,
1759 hash_count: self.hash_count,
1760 bits_per_entry: self.bits_per_entry,
1761 max_changed_paths: DEFAULT_COMMIT_GRAPH_BLOOM_SETTINGS.max_changed_paths,
1762 },
1763 ))
1764 }
1765}
1766
1767fn validate_commit_graph_bloom_settings(settings: CommitGraphBloomSettings) -> Result<()> {
1768 match settings.hash_version {
1769 1 | 2 => Ok(()),
1770 other => Err(GitError::Unsupported(format!(
1771 "commit-graph changed-path Bloom filter hash version {other}"
1772 ))),
1773 }
1774}
1775
1776fn validate_commit_graph_write_entries(
1777 format: ObjectFormat,
1778 entries: &[CommitGraphWriteEntry],
1779) -> Result<()> {
1780 let mut previous_oid: Option<&ObjectId> = None;
1781 for entry in entries {
1782 if entry.oid.format() != format
1783 || entry.tree.format() != format
1784 || entry.parents.iter().any(|parent| parent.format() != format)
1785 {
1786 return Err(GitError::InvalidObjectId(
1787 "commit-graph entry format does not match graph format".into(),
1788 ));
1789 }
1790 if let Some(previous) = previous_oid
1791 && previous.as_bytes() == entry.oid.as_bytes()
1792 {
1793 return Err(GitError::InvalidFormat(
1794 "commit-graph contains duplicate object ids".into(),
1795 ));
1796 }
1797 if entry.generation >= (1 << 30) {
1798 return Err(GitError::InvalidFormat(
1799 "commit-graph generation is too large".into(),
1800 ));
1801 }
1802 previous_oid = Some(&entry.oid);
1803 }
1804 Ok(())
1805}
1806
1807fn write_commit_graph_fanout(object_ids: &[ObjectId]) -> Result<Vec<u8>> {
1808 let mut counts = [0u32; 256];
1809 for oid in object_ids {
1810 let first = oid.as_bytes()[0] as usize;
1811 counts[first] = counts[first]
1812 .checked_add(1)
1813 .ok_or_else(|| GitError::InvalidFormat("commit-graph fanout overflow".into()))?;
1814 }
1815 let mut running = 0u32;
1816 let mut out = Vec::with_capacity(256 * 4);
1817 for count in counts {
1818 running = running
1819 .checked_add(count)
1820 .ok_or_else(|| GitError::InvalidFormat("commit-graph fanout overflow".into()))?;
1821 out.extend_from_slice(&running.to_be_bytes());
1822 }
1823 Ok(out)
1824}
1825
1826fn write_commit_graph_oid_lookup(object_ids: &[ObjectId]) -> Vec<u8> {
1827 let mut out = Vec::new();
1828 for oid in object_ids {
1829 out.extend_from_slice(oid.as_bytes());
1830 }
1831 out
1832}
1833
1834fn write_commit_graph_commit_data(entries: &[CommitGraphWriteEntry]) -> Result<(Vec<u8>, Vec<u8>)> {
1835 let parent_positions = commit_graph_parent_positions(entries, &[])?;
1836 write_commit_graph_commit_data_with_positions(entries, &parent_positions)
1837}
1838
1839fn commit_graph_parent_positions(
1840 entries: &[CommitGraphWriteEntry],
1841 base_oids: &[ObjectId],
1842) -> Result<HashMap<ObjectId, u32>> {
1843 let mut positions = HashMap::with_capacity(base_oids.len() + entries.len());
1844 for (idx, oid) in base_oids.iter().enumerate() {
1845 let idx = u32::try_from(idx)
1846 .map_err(|_| GitError::InvalidFormat("commit-graph base position overflow".into()))?;
1847 positions.entry(*oid).or_insert(idx);
1848 }
1849 let base_len = u32::try_from(base_oids.len())
1850 .map_err(|_| GitError::InvalidFormat("commit-graph base position overflow".into()))?;
1851 for (idx, entry) in entries.iter().enumerate() {
1852 let idx = u32::try_from(idx)
1853 .map_err(|_| GitError::InvalidFormat("commit-graph position overflow".into()))?;
1854 let pos = base_len
1855 .checked_add(idx)
1856 .ok_or_else(|| GitError::InvalidFormat("commit-graph position overflow".into()))?;
1857 if positions.insert(entry.oid, pos).is_some() {
1858 return Err(GitError::InvalidFormat(
1859 "commit-graph contains duplicate object ids".into(),
1860 ));
1861 }
1862 }
1863 Ok(positions)
1864}
1865
1866fn write_commit_graph_commit_data_with_positions(
1867 entries: &[CommitGraphWriteEntry],
1868 parent_positions_by_oid: &HashMap<ObjectId, u32>,
1869) -> Result<(Vec<u8>, Vec<u8>)> {
1870 let mut cdat = Vec::new();
1871 let mut edge = Vec::new();
1872 for entry in entries {
1873 cdat.extend_from_slice(entry.tree.as_bytes());
1874 let parent_positions = entry
1875 .parents
1876 .iter()
1877 .map(|parent| {
1878 parent_positions_by_oid.get(parent).copied().ok_or_else(|| {
1879 GitError::InvalidFormat(format!(
1880 "commit-graph parent {parent} is missing from graph"
1881 ))
1882 })
1883 })
1884 .collect::<Result<Vec<_>>>()?;
1885 let first_parent = parent_positions
1886 .first()
1887 .copied()
1888 .unwrap_or(COMMIT_GRAPH_PARENT_NONE);
1889 let second_parent = match parent_positions.len() {
1890 0 | 1 => COMMIT_GRAPH_PARENT_NONE,
1891 2 => parent_positions[1],
1892 _ => {
1893 let edge_start = u32::try_from(edge.len() / 4).map_err(|_| {
1894 GitError::InvalidFormat("commit-graph EDGE chunk overflow".into())
1895 })?;
1896 for (idx, parent) in parent_positions[1..].iter().enumerate() {
1897 let mut value = *parent;
1898 if idx == parent_positions.len() - 2 {
1899 value |= COMMIT_GRAPH_EXTRA_EDGE;
1900 }
1901 edge.extend_from_slice(&value.to_be_bytes());
1902 }
1903 COMMIT_GRAPH_EXTRA_EDGE | edge_start
1904 }
1905 };
1906 cdat.extend_from_slice(&first_parent.to_be_bytes());
1907 cdat.extend_from_slice(&second_parent.to_be_bytes());
1908 let generation_and_time_high =
1909 (entry.generation << 2) | (((entry.commit_time >> 32) as u32) & 0x3);
1910 cdat.extend_from_slice(&generation_and_time_high.to_be_bytes());
1911 cdat.extend_from_slice(&(entry.commit_time as u32).to_be_bytes());
1912 }
1913 Ok((cdat, edge))
1914}
1915
1916fn write_commit_graph_generation_data(entries: &[CommitGraphWriteEntry]) -> Result<Vec<u8>> {
1917 let corrected_dates = corrected_commit_dates(entries)?;
1918 let mut overflow_index = 0u32;
1919 let mut out = Vec::with_capacity(entries.len() * 4);
1920 for (entry, corrected) in entries.iter().zip(corrected_dates) {
1921 let offset = corrected.saturating_sub(entry.commit_time);
1922 if offset > COMMIT_GRAPH_GENERATION_DATA_MAX {
1923 out.extend_from_slice(
1924 &(COMMIT_GRAPH_GENERATION_DATA_OVERFLOW | overflow_index).to_be_bytes(),
1925 );
1926 overflow_index = overflow_index.checked_add(1).ok_or_else(|| {
1927 GitError::InvalidFormat("commit-graph GDO2 chunk overflow".into())
1928 })?;
1929 } else {
1930 out.extend_from_slice(&(offset as u32).to_be_bytes());
1931 }
1932 }
1933 Ok(out)
1934}
1935
1936fn write_commit_graph_generation_overflow(entries: &[CommitGraphWriteEntry]) -> Result<Vec<u8>> {
1937 let corrected_dates = corrected_commit_dates(entries)?;
1938 let mut out = Vec::new();
1939 for (entry, corrected) in entries.iter().zip(corrected_dates) {
1940 let offset = corrected.saturating_sub(entry.commit_time);
1941 if offset > COMMIT_GRAPH_GENERATION_DATA_MAX {
1942 out.extend_from_slice(&offset.to_be_bytes());
1943 }
1944 }
1945 Ok(out)
1946}
1947
1948fn corrected_commit_dates(entries: &[CommitGraphWriteEntry]) -> Result<Vec<u64>> {
1949 let resolve_parent = |parent: &ObjectId| -> Result<usize> {
1956 entries
1957 .binary_search_by(|entry| entry.oid.as_bytes().cmp(parent.as_bytes()))
1958 .map_err(|_| {
1959 GitError::InvalidFormat(format!(
1960 "commit-graph parent {parent} is missing from graph"
1961 ))
1962 })
1963 };
1964
1965 let mut cache: Vec<Option<u64>> = vec![None; entries.len()];
1966 let mut stack: Vec<usize> = Vec::new();
1967 for start in 0..entries.len() {
1968 if cache[start].is_some() {
1969 continue;
1970 }
1971 stack.push(start);
1972 while let Some(&idx) = stack.last() {
1973 if cache[idx].is_some() {
1974 stack.pop();
1975 continue;
1976 }
1977 let entry = &entries[idx];
1978 let mut corrected = entry.commit_time;
1979 let mut ready = true;
1980 for parent in &entry.parents {
1981 let parent_idx = resolve_parent(parent)?;
1982 match cache[parent_idx] {
1983 Some(value) => corrected = corrected.max(value.saturating_add(1)),
1984 None => {
1985 stack.push(parent_idx);
1988 ready = false;
1989 }
1990 }
1991 }
1992 if ready {
1993 cache[idx] = Some(corrected);
1994 stack.pop();
1995 }
1996 }
1997 }
1998 cache
1999 .into_iter()
2000 .map(|value| {
2001 value.ok_or_else(|| {
2002 GitError::InvalidFormat("commit-graph corrected date missing".into())
2003 })
2004 })
2005 .collect()
2006}
2007
2008fn write_commit_graph_bloom_filters(
2009 entries: &[CommitGraphWriteEntry],
2010 settings: CommitGraphBloomSettings,
2011) -> Option<(Vec<u8>, Vec<u8>)> {
2012 if entries.iter().all(|entry| entry.bloom_filter.is_none()) {
2013 return None;
2014 }
2015 let mut bidx = Vec::with_capacity(entries.len() * 4);
2016 let mut bdat = Vec::new();
2017 bdat.extend_from_slice(&settings.hash_version.to_be_bytes());
2018 bdat.extend_from_slice(&settings.hash_count.to_be_bytes());
2019 bdat.extend_from_slice(&settings.bits_per_entry.to_be_bytes());
2020 let mut offset = 0u32;
2021 for entry in entries {
2022 if let Some(filter) = &entry.bloom_filter {
2023 offset = offset.checked_add(filter.len() as u32)?;
2024 bdat.extend_from_slice(filter);
2025 }
2026 bidx.extend_from_slice(&offset.to_be_bytes());
2027 }
2028 Some((bidx, bdat))
2029}
2030
2031pub fn commit_graph_bloom_filter_for_paths<I, P>(
2032 paths: I,
2033 settings: CommitGraphBloomSettings,
2034) -> Vec<u8>
2035where
2036 I: IntoIterator<Item = P>,
2037 P: AsRef<[u8]>,
2038{
2039 let mut unique = std::collections::BTreeSet::new();
2040 for path in paths {
2041 let path = path.as_ref();
2042 if path.is_empty() {
2043 continue;
2044 }
2045 unique.insert(path.to_vec());
2046 for idx in path
2047 .iter()
2048 .enumerate()
2049 .filter_map(|(idx, byte)| (*byte == b'/').then_some(idx))
2050 {
2051 if idx > 0 {
2052 unique.insert(path[..idx].to_vec());
2053 }
2054 }
2055 }
2056 if unique.len() > settings.max_changed_paths {
2057 return vec![0xff];
2058 }
2059 let filter_len = (unique.len() as u64 * u64::from(settings.bits_per_entry)).div_ceil(8);
2060 let mut filter = vec![0u8; usize::try_from(filter_len).unwrap_or(usize::MAX).max(1)];
2061 for path in unique {
2062 add_commit_graph_bloom_key(&mut filter, &path, settings);
2063 }
2064 filter
2065}
2066
2067pub fn commit_graph_bloom_filter_contains(
2068 filter: &[u8],
2069 path: &[u8],
2070 settings: CommitGraphBloomSettings,
2071) -> bool {
2072 if filter.is_empty() {
2073 return false;
2074 }
2075 commit_graph_bloom_key_hashes(path, settings)
2076 .into_iter()
2077 .all(|hash| {
2078 let bit = u64::from(hash) % ((filter.len() as u64) * 8);
2079 let byte = (bit / 8) as usize;
2080 let mask = 1u8 << (bit & 7);
2081 filter.get(byte).is_some_and(|value| value & mask != 0)
2082 })
2083}
2084
2085fn add_commit_graph_bloom_key(filter: &mut [u8], path: &[u8], settings: CommitGraphBloomSettings) {
2086 if filter.is_empty() {
2087 return;
2088 }
2089 for hash in commit_graph_bloom_key_hashes(path, settings) {
2090 let bit = u64::from(hash) % ((filter.len() as u64) * 8);
2091 let byte = (bit / 8) as usize;
2092 let mask = 1u8 << (bit & 7);
2093 if let Some(value) = filter.get_mut(byte) {
2094 *value |= mask;
2095 }
2096 }
2097}
2098
2099fn commit_graph_bloom_key_hashes(path: &[u8], settings: CommitGraphBloomSettings) -> Vec<u32> {
2100 let seed0 = 0x293a_e76f;
2101 let seed1 = 0x7e64_6e2c;
2102 let hash0 = commit_graph_bloom_murmur3_seeded(seed0, path, settings.hash_version);
2103 let hash1 = commit_graph_bloom_murmur3_seeded(seed1, path, settings.hash_version);
2104 (0..settings.hash_count)
2105 .map(|idx| hash0.wrapping_add(idx.wrapping_mul(hash1)))
2106 .collect()
2107}
2108
2109fn commit_graph_bloom_murmur3_seeded(seed: u32, data: &[u8], version: u32) -> u32 {
2110 let c1 = 0xcc9e_2d51u32;
2111 let c2 = 0x1b87_3593u32;
2112 let r1 = 15;
2113 let r2 = 13;
2114 let m = 5u32;
2115 let n = 0xe654_6b64u32;
2116 let mut seed = seed;
2117 for chunk in data.chunks_exact(4) {
2118 let mut k = if version == 2 {
2119 u32::from(chunk[0])
2120 | (u32::from(chunk[1]) << 8)
2121 | (u32::from(chunk[2]) << 16)
2122 | (u32::from(chunk[3]) << 24)
2123 } else {
2124 (chunk[0] as i8 as i32 as u32)
2125 | ((chunk[1] as i8 as i32 as u32) << 8)
2126 | ((chunk[2] as i8 as i32 as u32) << 16)
2127 | ((chunk[3] as i8 as i32 as u32) << 24)
2128 };
2129 k = k.wrapping_mul(c1);
2130 k = k.rotate_left(r1);
2131 k = k.wrapping_mul(c2);
2132 seed ^= k;
2133 seed = seed.rotate_left(r2).wrapping_mul(m).wrapping_add(n);
2134 }
2135
2136 let tail = data.chunks_exact(4).remainder();
2137 let mut k1 = 0u32;
2138 let tail_byte = |idx: usize| -> u32 {
2139 if version == 2 {
2140 u32::from(tail[idx])
2141 } else {
2142 tail[idx] as i8 as i32 as u32
2143 }
2144 };
2145 match tail.len() {
2146 3 => {
2147 k1 ^= tail_byte(2) << 16;
2148 k1 ^= tail_byte(1) << 8;
2149 k1 ^= tail_byte(0);
2150 }
2151 2 => {
2152 k1 ^= tail_byte(1) << 8;
2153 k1 ^= tail_byte(0);
2154 }
2155 1 => {
2156 k1 ^= tail_byte(0);
2157 }
2158 _ => {}
2159 }
2160 if !tail.is_empty() {
2161 k1 = k1.wrapping_mul(c1);
2162 k1 = k1.rotate_left(r1);
2163 k1 = k1.wrapping_mul(c2);
2164 seed ^= k1;
2165 }
2166
2167 seed ^= data.len() as u32;
2168 seed ^= seed >> 16;
2169 seed = seed.wrapping_mul(0x85eb_ca6b);
2170 seed ^= seed >> 13;
2171 seed = seed.wrapping_mul(0xc2b2_ae35);
2172 seed ^ (seed >> 16)
2173}
2174
2175fn write_commit_graph_chunks(
2176 format: ObjectFormat,
2177 base_graph_count: u8,
2178 chunks: &[([u8; 4], Vec<u8>)],
2179) -> Result<Vec<u8>> {
2180 if chunks.len() > u8::MAX as usize {
2181 return Err(GitError::InvalidFormat(
2182 "too many commit-graph chunks".into(),
2183 ));
2184 }
2185 let lookup_len = (chunks.len() + 1)
2186 .checked_mul(12)
2187 .ok_or_else(|| GitError::InvalidFormat("commit-graph lookup overflow".into()))?;
2188 let mut out = Vec::new();
2189 out.extend_from_slice(b"CGPH");
2190 out.push(1);
2191 out.push(hash_function_id(format) as u8);
2192 out.push(chunks.len() as u8);
2193 out.push(base_graph_count);
2194 let mut chunk_offset = (8usize)
2195 .checked_add(lookup_len)
2196 .ok_or_else(|| GitError::InvalidFormat("commit-graph lookup overflow".into()))?
2197 as u64;
2198 for (id, data) in chunks {
2199 out.extend_from_slice(id);
2200 out.extend_from_slice(&chunk_offset.to_be_bytes());
2201 chunk_offset = chunk_offset
2202 .checked_add(data.len() as u64)
2203 .ok_or_else(|| GitError::InvalidFormat("commit-graph size overflow".into()))?;
2204 }
2205 out.extend_from_slice(&[0, 0, 0, 0]);
2206 out.extend_from_slice(&chunk_offset.to_be_bytes());
2207 for (_id, data) in chunks {
2208 out.extend_from_slice(data);
2209 }
2210 let checksum = sley_core::digest_bytes(format, &out)?;
2211 out.extend_from_slice(checksum.as_bytes());
2212 Ok(out)
2213}
2214
2215const COMMIT_GRAPH_PARENT_NONE: u32 = 0x7000_0000;
2216const COMMIT_GRAPH_EXTRA_EDGE: u32 = 0x8000_0000;
2217const COMMIT_GRAPH_EXTRA_EDGE_MASK: u32 = 0x7fff_ffff;
2218const COMMIT_GRAPH_GENERATION_DATA_OVERFLOW: u32 = 0x8000_0000;
2219const COMMIT_GRAPH_GENERATION_DATA_MAX: u64 = 0x7fff_ffff;
2220
2221fn parse_commit_graph_fanout(
2222 bytes: &[u8],
2223 chunks: &[CommitGraphChunk],
2224) -> Result<([u32; 256], usize)> {
2225 let data = commit_graph_chunk_data(bytes, chunks, *b"OIDF", true)?
2226 .ok_or_else(|| GitError::InvalidFormat("commit-graph missing OIDF chunk".into()))?;
2227 if data.len() != 256 * 4 {
2228 return Err(GitError::InvalidFormat(
2229 "commit-graph OIDF chunk has invalid length".into(),
2230 ));
2231 }
2232 let mut fanout = [0u32; 256];
2233 let mut previous = 0u32;
2234 for (idx, slot) in fanout.iter_mut().enumerate() {
2235 let start = idx * 4;
2236 *slot = u32_be(&data[start..start + 4]);
2237 if *slot < previous {
2238 return Err(GitError::InvalidFormat(
2239 "commit-graph OIDF fanout is not monotonic".into(),
2240 ));
2241 }
2242 previous = *slot;
2243 }
2244 Ok((fanout, fanout[255] as usize))
2245}
2246
2247fn parse_commit_graph_oids(
2248 bytes: &[u8],
2249 chunks: &[CommitGraphChunk],
2250 format: ObjectFormat,
2251 commit_count: usize,
2252 fanout: &[u32; 256],
2253) -> Result<Vec<ObjectId>> {
2254 let data = commit_graph_chunk_data(bytes, chunks, *b"OIDL", true)?
2255 .ok_or_else(|| GitError::InvalidFormat("commit-graph missing OIDL chunk".into()))?;
2256 let expected_len = commit_count
2257 .checked_mul(format.raw_len())
2258 .ok_or_else(|| GitError::InvalidFormat("commit-graph OIDL chunk overflow".into()))?;
2259 if data.len() != expected_len {
2260 return Err(GitError::InvalidFormat(
2261 "commit-graph OIDL chunk has invalid length".into(),
2262 ));
2263 }
2264
2265 let mut oids = Vec::with_capacity(commit_count);
2266 let mut counts = [0u32; 256];
2267 let mut previous_oid: Option<ObjectId> = None;
2268 for idx in 0..commit_count {
2269 let start = idx * format.raw_len();
2270 let oid = ObjectId::from_raw(format, &data[start..start + format.raw_len()])?;
2271 if let Some(previous) = &previous_oid
2272 && previous.as_bytes() >= oid.as_bytes()
2273 {
2274 return Err(GitError::InvalidFormat(
2275 "commit-graph OIDL object ids are not strictly sorted".into(),
2276 ));
2277 }
2278 counts[oid.as_bytes()[0] as usize] = counts[oid.as_bytes()[0] as usize]
2279 .checked_add(1)
2280 .ok_or_else(|| GitError::InvalidFormat("commit-graph fanout overflow".into()))?;
2281 previous_oid = Some(oid);
2282 oids.push(oid);
2283 }
2284
2285 let mut running = 0u32;
2286 for (idx, count) in counts.iter().enumerate() {
2287 running = running
2288 .checked_add(*count)
2289 .ok_or_else(|| GitError::InvalidFormat("commit-graph fanout overflow".into()))?;
2290 if fanout[idx] != running {
2291 return Err(GitError::InvalidFormat(
2292 "commit-graph OIDF fanout does not match OIDL".into(),
2293 ));
2294 }
2295 }
2296 Ok(oids)
2297}
2298
2299fn parse_commit_graph_commit_data(
2300 bytes: &[u8],
2301 chunks: &[CommitGraphChunk],
2302 format: ObjectFormat,
2303 oids: Vec<ObjectId>,
2304 base_graph_count: u8,
2305) -> Result<Vec<CommitGraphEntry>> {
2306 let data = commit_graph_chunk_data(bytes, chunks, *b"CDAT", true)?
2307 .ok_or_else(|| GitError::InvalidFormat("commit-graph missing CDAT chunk".into()))?;
2308 let entry_len = format
2309 .raw_len()
2310 .checked_add(16)
2311 .ok_or_else(|| GitError::InvalidFormat("commit-graph CDAT entry overflow".into()))?;
2312 let expected_len = oids
2313 .len()
2314 .checked_mul(entry_len)
2315 .ok_or_else(|| GitError::InvalidFormat("commit-graph CDAT chunk overflow".into()))?;
2316 if data.len() != expected_len {
2317 return Err(GitError::InvalidFormat(
2318 "commit-graph CDAT chunk has invalid length".into(),
2319 ));
2320 }
2321 let extra_edges = commit_graph_chunk_data(bytes, chunks, *b"EDGE", false)?;
2322 if let Some(extra_edges) = extra_edges
2323 && extra_edges.len() % 4 != 0
2324 {
2325 return Err(GitError::InvalidFormat(
2326 "commit-graph EDGE chunk has invalid length".into(),
2327 ));
2328 }
2329
2330 let commit_count = oids.len();
2331 let parent_limit = if base_graph_count == 0 {
2332 commit_count
2333 } else {
2334 usize::MAX
2335 };
2336 let mut entries = Vec::with_capacity(commit_count);
2337 for (idx, oid) in oids.into_iter().enumerate() {
2338 let start = idx * entry_len;
2339 let tree = ObjectId::from_raw(format, &data[start..start + format.raw_len()])?;
2340 let parent_one = u32_be(&data[start + format.raw_len()..start + format.raw_len() + 4]);
2341 let parent_two = u32_be(&data[start + format.raw_len() + 4..start + format.raw_len() + 8]);
2342 let generation_and_time_high =
2343 u32_be(&data[start + format.raw_len() + 8..start + format.raw_len() + 12]);
2344 let time_low = u32_be(&data[start + format.raw_len() + 12..start + entry_len]);
2345 let generation = generation_and_time_high >> 2;
2346 let commit_time = (u64::from(generation_and_time_high & 0x3) << 32) | u64::from(time_low);
2347 let parents = commit_graph_parents(parent_one, parent_two, extra_edges, parent_limit)?;
2348 entries.push(CommitGraphEntry {
2349 oid,
2350 tree,
2351 parents,
2352 generation,
2353 commit_time,
2354 corrected_commit_date_offset: None,
2355 });
2356 }
2357 Ok(entries)
2358}
2359
2360fn apply_commit_graph_generation_data(
2361 bytes: &[u8],
2362 chunks: &[CommitGraphChunk],
2363 commits: &mut [CommitGraphEntry],
2364) -> Result<()> {
2365 let data = commit_graph_chunk_data(bytes, chunks, *b"GDA2", false)?;
2366 let overflow = commit_graph_chunk_data(bytes, chunks, *b"GDO2", false)?;
2367 let Some(data) = data else {
2368 if overflow.is_some() {
2369 return Err(GitError::InvalidFormat(
2370 "commit-graph GDO2 chunk exists without GDA2 chunk".into(),
2371 ));
2372 }
2373 return Ok(());
2374 };
2375 let expected_len = commits
2376 .len()
2377 .checked_mul(4)
2378 .ok_or_else(|| GitError::InvalidFormat("commit-graph GDA2 chunk overflow".into()))?;
2379 if data.len() != expected_len {
2380 return Err(GitError::InvalidFormat(
2381 "commit-graph GDA2 chunk has invalid length".into(),
2382 ));
2383 }
2384 if let Some(overflow) = overflow
2385 && overflow.len() % 8 != 0
2386 {
2387 return Err(GitError::InvalidFormat(
2388 "commit-graph GDO2 chunk has invalid length".into(),
2389 ));
2390 }
2391
2392 let mut used_overflow = false;
2393 for (idx, commit) in commits.iter_mut().enumerate() {
2394 let start = idx * 4;
2395 let raw = u32_be(&data[start..start + 4]);
2396 let offset = if raw & 0x8000_0000 == 0 {
2397 u64::from(raw)
2398 } else {
2399 used_overflow = true;
2400 let Some(overflow) = overflow else {
2401 return Err(GitError::InvalidFormat(
2402 "commit-graph GDA2 overflow entry missing GDO2 chunk".into(),
2403 ));
2404 };
2405 let overflow_idx = (raw & 0x7fff_ffff) as usize;
2406 let overflow_start = overflow_idx.checked_mul(8).ok_or_else(|| {
2407 GitError::InvalidFormat("commit-graph GDO2 index overflow".into())
2408 })?;
2409 let overflow_end = overflow_start.checked_add(8).ok_or_else(|| {
2410 GitError::InvalidFormat("commit-graph GDO2 index overflow".into())
2411 })?;
2412 if overflow_end > overflow.len() {
2413 return Err(GitError::InvalidFormat(
2414 "commit-graph GDA2 overflow points past GDO2 chunk".into(),
2415 ));
2416 }
2417 u64_be(&overflow[overflow_start..overflow_end])
2418 };
2419 commit.corrected_commit_date_offset = Some(offset);
2420 }
2421 if overflow.is_some() && !used_overflow {
2422 return Err(GitError::InvalidFormat(
2423 "commit-graph GDO2 chunk is unused by GDA2".into(),
2424 ));
2425 }
2426 Ok(())
2427}
2428
2429fn parse_commit_graph_bloom_filters(
2430 bytes: &[u8],
2431 chunks: &[CommitGraphChunk],
2432 commit_count: usize,
2433) -> Result<Option<CommitGraphBloomFilters>> {
2434 let index = commit_graph_chunk_data(bytes, chunks, *b"BIDX", false)?;
2435 let data = commit_graph_chunk_data(bytes, chunks, *b"BDAT", false)?;
2436 let Some(data) = data else {
2437 return Ok(None);
2438 };
2439 let Some(index) = index else {
2440 return Err(GitError::InvalidFormat(
2441 "commit-graph BDAT chunk exists without BIDX chunk".into(),
2442 ));
2443 };
2444 let expected_index_len = commit_count
2445 .checked_mul(4)
2446 .ok_or_else(|| GitError::InvalidFormat("commit-graph BIDX chunk overflow".into()))?;
2447 if index.len() != expected_index_len {
2448 return Err(GitError::InvalidFormat(
2449 "commit-graph BIDX chunk has invalid length".into(),
2450 ));
2451 }
2452 if data.len() < 12 {
2453 return Err(GitError::InvalidFormat(
2454 "commit-graph BDAT chunk has invalid length".into(),
2455 ));
2456 }
2457 let hash_version = u32_be(&data[0..4]);
2458 let hash_count = u32_be(&data[4..8]);
2459 let bits_per_entry = u32_be(&data[8..12]);
2460 let payload = &data[12..];
2461
2462 let mut filters = Vec::with_capacity(commit_count);
2463 let mut previous = 0usize;
2464 for idx in 0..commit_count {
2465 let start = idx * 4;
2466 let cumulative = u32_be(&index[start..start + 4]) as usize;
2467 if cumulative < previous {
2468 return Err(GitError::InvalidFormat(
2469 "commit-graph BIDX offsets are not monotonic".into(),
2470 ));
2471 }
2472 if cumulative > payload.len() {
2473 return Err(GitError::InvalidFormat(
2474 "commit-graph BIDX offset points past BDAT payload".into(),
2475 ));
2476 }
2477 filters.push(payload[previous..cumulative].to_vec());
2478 previous = cumulative;
2479 }
2480 if previous != payload.len() {
2481 return Err(GitError::InvalidFormat(
2482 "commit-graph BDAT payload has trailing bytes".into(),
2483 ));
2484 }
2485
2486 Ok(Some(CommitGraphBloomFilters {
2487 hash_version,
2488 hash_count,
2489 bits_per_entry,
2490 filters,
2491 }))
2492}
2493
2494fn commit_graph_parents(
2495 parent_one: u32,
2496 parent_two: u32,
2497 extra_edges: Option<&[u8]>,
2498 parent_limit: usize,
2499) -> Result<Vec<u32>> {
2500 let mut parents = Vec::new();
2501 if parent_one != COMMIT_GRAPH_PARENT_NONE {
2502 validate_commit_graph_parent_position(parent_one, parent_limit)?;
2503 parents.push(parent_one);
2504 }
2505 if parent_two == COMMIT_GRAPH_PARENT_NONE {
2506 return Ok(parents);
2507 }
2508 if parent_two & COMMIT_GRAPH_EXTRA_EDGE == 0 {
2509 validate_commit_graph_parent_position(parent_two, parent_limit)?;
2510 parents.push(parent_two);
2511 return Ok(parents);
2512 }
2513
2514 let Some(extra_edges) = extra_edges else {
2515 return Err(GitError::InvalidFormat(
2516 "commit-graph octopus edge missing EDGE chunk".into(),
2517 ));
2518 };
2519 let mut edge_idx = (parent_two & COMMIT_GRAPH_EXTRA_EDGE_MASK) as usize;
2520 loop {
2521 let start = edge_idx
2522 .checked_mul(4)
2523 .ok_or_else(|| GitError::InvalidFormat("commit-graph EDGE index overflow".into()))?;
2524 let end = start
2525 .checked_add(4)
2526 .ok_or_else(|| GitError::InvalidFormat("commit-graph EDGE index overflow".into()))?;
2527 if end > extra_edges.len() {
2528 return Err(GitError::InvalidFormat(
2529 "commit-graph EDGE entry points past chunk".into(),
2530 ));
2531 }
2532 let edge = u32_be(&extra_edges[start..end]);
2533 let parent = edge & COMMIT_GRAPH_EXTRA_EDGE_MASK;
2534 validate_commit_graph_parent_position(parent, parent_limit)?;
2535 parents.push(parent);
2536 if edge & COMMIT_GRAPH_EXTRA_EDGE != 0 {
2537 return Ok(parents);
2538 }
2539 edge_idx = edge_idx
2540 .checked_add(1)
2541 .ok_or_else(|| GitError::InvalidFormat("commit-graph EDGE index overflow".into()))?;
2542 }
2543}
2544
2545fn validate_commit_graph_parent_position(parent: u32, commit_count: usize) -> Result<()> {
2546 if parent as usize >= commit_count {
2547 return Err(GitError::InvalidFormat(
2548 "commit-graph parent points past commit table".into(),
2549 ));
2550 }
2551 Ok(())
2552}
2553
2554fn parse_commit_graph_base_graphs(
2555 bytes: &[u8],
2556 chunks: &[CommitGraphChunk],
2557 format: ObjectFormat,
2558 base_graph_count: usize,
2559) -> Result<Vec<ObjectId>> {
2560 let data = commit_graph_chunk_data(bytes, chunks, *b"BASE", base_graph_count != 0)?;
2561 let Some(data) = data else {
2562 return Ok(Vec::new());
2563 };
2564 let expected_len = base_graph_count
2565 .checked_mul(format.raw_len())
2566 .ok_or_else(|| GitError::InvalidFormat("commit-graph BASE chunk overflow".into()))?;
2567 if data.len() != expected_len {
2568 return Err(GitError::InvalidFormat(
2569 "commit-graph BASE chunk has invalid length".into(),
2570 ));
2571 }
2572 let mut base_graphs = Vec::with_capacity(base_graph_count);
2573 for idx in 0..base_graph_count {
2574 let start = idx * format.raw_len();
2575 base_graphs.push(ObjectId::from_raw(
2576 format,
2577 &data[start..start + format.raw_len()],
2578 )?);
2579 }
2580 Ok(base_graphs)
2581}
2582
2583fn commit_graph_chunk_data<'a>(
2584 bytes: &'a [u8],
2585 chunks: &[CommitGraphChunk],
2586 id: [u8; 4],
2587 required: bool,
2588) -> Result<Option<&'a [u8]>> {
2589 let Some(chunk) = chunks.iter().find(|chunk| chunk.id == id) else {
2590 if required {
2591 return Err(GitError::InvalidFormat(format!(
2592 "commit-graph missing {} chunk",
2593 std::str::from_utf8(&id).unwrap_or("required")
2594 )));
2595 }
2596 return Ok(None);
2597 };
2598 let start = usize::try_from(chunk.offset)
2599 .map_err(|_| GitError::InvalidFormat("commit-graph chunk offset overflow".into()))?;
2600 let len = usize::try_from(chunk.len)
2601 .map_err(|_| GitError::InvalidFormat("commit-graph chunk length overflow".into()))?;
2602 let end = start
2603 .checked_add(len)
2604 .ok_or_else(|| GitError::InvalidFormat("commit-graph chunk range overflow".into()))?;
2605 let Some(data) = bytes.get(start..end) else {
2606 return Err(GitError::InvalidFormat(
2607 "commit-graph chunk extends past file".into(),
2608 ));
2609 };
2610 Ok(Some(data))
2611}
2612
2613#[derive(Debug, Clone, PartialEq, Eq)]
2614pub struct Bundle {
2615 pub version: u8,
2616 pub format: ObjectFormat,
2617 pub capabilities: Vec<BundleCapability>,
2618 pub prerequisites: Vec<BundlePrerequisite>,
2619 pub references: Vec<BundleReference>,
2620 pub pack: Vec<u8>,
2621}
2622
2623#[derive(Debug, Clone, PartialEq, Eq)]
2624pub struct BundleCapability {
2625 pub key: String,
2626 pub value: Option<Vec<u8>>,
2627}
2628
2629#[derive(Debug, Clone, PartialEq, Eq)]
2630pub struct BundlePrerequisite {
2631 pub oid: ObjectId,
2632 pub comment: Vec<u8>,
2633}
2634
2635#[derive(Debug, Clone, PartialEq, Eq)]
2636pub struct BundleReference {
2637 pub oid: ObjectId,
2638 pub name: String,
2639}
2640
2641impl Bundle {
2642 pub fn parse_standalone(bytes: &[u8]) -> Result<Self> {
2643 Self::parse(bytes, ObjectFormat::Sha1)
2644 }
2645
2646 pub fn parse(bytes: &[u8], default_format: ObjectFormat) -> Result<Self> {
2647 let (signature, mut offset) = next_lf_line(bytes, 0)
2648 .ok_or_else(|| GitError::InvalidFormat("bundle missing signature".into()))?;
2649 let version = match signature {
2650 b"# v2 git bundle" => 2,
2651 b"# v3 git bundle" => 3,
2652 _ => {
2653 return Err(GitError::InvalidFormat("missing bundle signature".into()));
2654 }
2655 };
2656
2657 let mut format = default_format;
2658 let mut capabilities = Vec::new();
2659 let mut prerequisites = Vec::new();
2660 let mut references = Vec::new();
2661 let mut seen_non_capability = false;
2662 loop {
2663 let Some((line, next_offset)) = next_lf_line(bytes, offset) else {
2664 return Err(GitError::InvalidFormat(
2665 "bundle header missing pack separator".into(),
2666 ));
2667 };
2668 offset = next_offset;
2669 if line.is_empty() {
2670 break;
2671 }
2672 match line[0] {
2673 b'@' => {
2674 if version != 3 {
2675 return Err(GitError::InvalidFormat(
2676 "bundle v2 cannot contain capabilities".into(),
2677 ));
2678 }
2679 if seen_non_capability {
2680 return Err(GitError::InvalidFormat(
2681 "bundle capability appears after prerequisites or references".into(),
2682 ));
2683 }
2684 let capability = parse_bundle_capability(&line[1..])?;
2685 if capability.key == "object-format" {
2686 let Some(value) = &capability.value else {
2687 return Err(GitError::InvalidFormat(
2688 "bundle object-format capability is missing a value".into(),
2689 ));
2690 };
2691 let text = std::str::from_utf8(value)
2692 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2693 format = text.parse()?;
2694 }
2695 capabilities.push(capability);
2696 }
2697 b'-' => {
2698 seen_non_capability = true;
2699 prerequisites.push(parse_bundle_prerequisite(&line[1..], format)?);
2700 }
2701 _ => {
2702 seen_non_capability = true;
2703 references.push(parse_bundle_reference(line, format)?);
2704 }
2705 }
2706 }
2707
2708 Ok(Self {
2709 version,
2710 format,
2711 capabilities,
2712 prerequisites,
2713 references,
2714 pack: bytes[offset..].to_vec(),
2715 })
2716 }
2717
2718 pub fn write(&self) -> Result<Vec<u8>> {
2719 if self.version != 2 && self.version != 3 {
2720 return Err(GitError::Unsupported(format!(
2721 "bundle version {}",
2722 self.version
2723 )));
2724 }
2725 if self.version == 2 && !self.capabilities.is_empty() {
2726 return Err(GitError::InvalidFormat(
2727 "bundle v2 cannot contain capabilities".into(),
2728 ));
2729 }
2730 let mut out = Vec::new();
2731 match self.version {
2732 2 => out.extend_from_slice(b"# v2 git bundle\n"),
2733 3 => out.extend_from_slice(b"# v3 git bundle\n"),
2734 _ => unreachable!(),
2735 }
2736 if self.version == 3 {
2737 let mut wrote_object_format = false;
2738 for capability in &self.capabilities {
2739 write_bundle_capability(&mut out, capability)?;
2740 if capability.key == "object-format" {
2741 let Some(value) = &capability.value else {
2742 return Err(GitError::InvalidFormat(
2743 "bundle object-format capability is missing a value".into(),
2744 ));
2745 };
2746 let text = std::str::from_utf8(value)
2747 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2748 let format: ObjectFormat = text.parse()?;
2749 if format != self.format {
2750 return Err(GitError::InvalidFormat(format!(
2751 "bundle object-format capability is {}, bundle uses {}",
2752 format.name(),
2753 self.format.name()
2754 )));
2755 }
2756 wrote_object_format = true;
2757 }
2758 }
2759 if self.format != ObjectFormat::Sha1 && !wrote_object_format {
2760 out.extend_from_slice(b"@object-format=");
2761 out.extend_from_slice(self.format.name().as_bytes());
2762 out.push(b'\n');
2763 }
2764 }
2765 for prerequisite in &self.prerequisites {
2766 ensure_bundle_oid_format(&prerequisite.oid, self.format, "prerequisite")?;
2767 out.push(b'-');
2768 out.extend_from_slice(prerequisite.oid.to_hex().as_bytes());
2769 out.push(b' ');
2770 out.extend_from_slice(&prerequisite.comment);
2771 out.push(b'\n');
2772 }
2773 for reference in &self.references {
2774 ensure_bundle_oid_format(&reference.oid, self.format, "reference")?;
2775 if reference.name.is_empty() || reference.name.as_bytes().contains(&b'\n') {
2776 return Err(GitError::InvalidFormat(
2777 "bundle reference has invalid name".into(),
2778 ));
2779 }
2780 out.extend_from_slice(reference.oid.to_hex().as_bytes());
2781 out.push(b' ');
2782 out.extend_from_slice(reference.name.as_bytes());
2783 out.push(b'\n');
2784 }
2785 out.push(b'\n');
2786 out.extend_from_slice(&self.pack);
2787 Ok(out)
2788 }
2789}
2790
2791fn next_lf_line(bytes: &[u8], offset: usize) -> Option<(&[u8], usize)> {
2792 let relative = bytes
2793 .get(offset..)?
2794 .iter()
2795 .position(|byte| *byte == b'\n')?;
2796 let end = offset + relative;
2797 Some((&bytes[offset..end], end + 1))
2798}
2799
2800fn parse_bundle_capability(line: &[u8]) -> Result<BundleCapability> {
2801 let (key, value) = match line.iter().position(|byte| *byte == b'=') {
2802 Some(idx) => (&line[..idx], Some(line[idx + 1..].to_vec())),
2803 None => (line, None),
2804 };
2805 if key.is_empty()
2806 || !key
2807 .iter()
2808 .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
2809 {
2810 return Err(GitError::InvalidFormat(
2811 "bundle capability has invalid key".into(),
2812 ));
2813 }
2814 let key = std::str::from_utf8(key)
2815 .map_err(|err| GitError::InvalidFormat(err.to_string()))?
2816 .to_string();
2817 Ok(BundleCapability { key, value })
2818}
2819
2820fn write_bundle_capability(out: &mut Vec<u8>, capability: &BundleCapability) -> Result<()> {
2821 if capability.key.is_empty()
2822 || !capability
2823 .key
2824 .bytes()
2825 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
2826 {
2827 return Err(GitError::InvalidFormat(
2828 "bundle capability has invalid key".into(),
2829 ));
2830 }
2831 if capability.key.as_bytes().contains(&b'\n') {
2832 return Err(GitError::InvalidFormat(
2833 "bundle capability has invalid key".into(),
2834 ));
2835 }
2836 out.push(b'@');
2837 out.extend_from_slice(capability.key.as_bytes());
2838 if let Some(value) = &capability.value {
2839 if value.contains(&b'\n') {
2840 return Err(GitError::InvalidFormat(
2841 "bundle capability has invalid value".into(),
2842 ));
2843 }
2844 out.push(b'=');
2845 out.extend_from_slice(value);
2846 }
2847 out.push(b'\n');
2848 Ok(())
2849}
2850
2851fn ensure_bundle_oid_format(oid: &ObjectId, format: ObjectFormat, kind: &str) -> Result<()> {
2852 if oid.format() != format {
2853 return Err(GitError::InvalidObjectId(format!(
2854 "bundle {kind} {oid} uses {}, bundle uses {}",
2855 oid.format().name(),
2856 format.name()
2857 )));
2858 }
2859 Ok(())
2860}
2861
2862fn parse_bundle_prerequisite(line: &[u8], format: ObjectFormat) -> Result<BundlePrerequisite> {
2863 let hex_len = format.hex_len();
2864 if line.len() < hex_len + 1 || line.get(hex_len).copied() != Some(b' ') {
2865 return Err(GitError::InvalidFormat(
2866 "bundle prerequisite line is malformed".into(),
2867 ));
2868 }
2869 let hex = std::str::from_utf8(&line[..hex_len])
2870 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2871 Ok(BundlePrerequisite {
2872 oid: ObjectId::from_hex(format, hex)?,
2873 comment: line[hex_len + 1..].to_vec(),
2874 })
2875}
2876
2877fn parse_bundle_reference(line: &[u8], format: ObjectFormat) -> Result<BundleReference> {
2878 let hex_len = format.hex_len();
2879 if line.len() <= hex_len + 1 || line.get(hex_len).copied() != Some(b' ') {
2880 return Err(GitError::InvalidFormat(
2881 "bundle reference line is malformed".into(),
2882 ));
2883 }
2884 let hex = std::str::from_utf8(&line[..hex_len])
2885 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2886 let name = std::str::from_utf8(&line[hex_len + 1..])
2887 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2888 if name.is_empty() {
2889 return Err(GitError::InvalidFormat(
2890 "bundle reference has empty name".into(),
2891 ));
2892 }
2893 Ok(BundleReference {
2894 oid: ObjectId::from_hex(format, hex)?,
2895 name: name.to_string(),
2896 })
2897}
2898
2899#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2901pub enum RefStorageFormat {
2902 Files,
2903 Reftable,
2904}
2905
2906impl RefStorageFormat {
2907 pub fn parse(value: &str) -> Result<Self> {
2908 match value {
2909 "" | "files" => Ok(Self::Files),
2910 "reftable" => Ok(Self::Reftable),
2911 other => Err(GitError::Command(format!(
2912 "unknown ref storage format '{other}'"
2913 ))),
2914 }
2915 }
2916
2917 pub const fn name(self) -> &'static str {
2918 match self {
2919 Self::Files => "files",
2920 Self::Reftable => "reftable",
2921 }
2922 }
2923}
2924
2925#[derive(Debug, Clone, PartialEq, Eq)]
2927pub struct InitOptions {
2928 pub worktree: PathBuf,
2929 pub git_dir_override: Option<PathBuf>,
2934 pub core_worktree: Option<String>,
2937 pub object_dir: Option<PathBuf>,
2942 pub object_format: ObjectFormat,
2943 pub object_format_explicit: bool,
2947 pub bare: bool,
2948 pub initial_branch: String,
2949 pub template_dir: Option<PathBuf>,
2950 pub copy_template_config: bool,
2951 pub separate_git_dir: Option<PathBuf>,
2952 pub shared_repository: Option<String>,
2953 pub ref_storage: RefStorageFormat,
2954 pub ref_storage_explicit: bool,
2958}
2959
2960#[derive(Debug, Clone, PartialEq, Eq)]
2961pub struct RepositoryLayout {
2962 pub git_dir: PathBuf,
2963 pub worktree: PathBuf,
2964 pub object_format: ObjectFormat,
2965 pub bare: bool,
2966 pub ref_storage: RefStorageFormat,
2967 pub reinitialized: bool,
2968}
2969
2970pub struct RepositoryBootstrap;
2971
2972struct ReferenceBackendEnv {
2973 raw: String,
2974 format: RefStorageFormat,
2975 path: PathBuf,
2976}
2977
2978fn reference_backend_env() -> Result<Option<ReferenceBackendEnv>> {
2979 let Ok(raw) = env::var("GIT_REFERENCE_BACKEND") else {
2980 return Ok(None);
2981 };
2982 let Some((backend, path)) = raw.split_once("://") else {
2983 return Err(GitError::Command(format!(
2984 "invalid value for 'extensions.refstorage': '{raw}'"
2985 )));
2986 };
2987 if path.is_empty() {
2988 return Err(GitError::Command(format!(
2989 "invalid value for 'extensions.refstorage': '{raw}'"
2990 )));
2991 }
2992 let format = match backend {
2993 "files" => RefStorageFormat::Files,
2994 "reftable" => RefStorageFormat::Reftable,
2995 _ => {
2996 return Err(GitError::Command(format!(
2997 "invalid value for 'extensions.refstorage': '{raw}'"
2998 )));
2999 }
3000 };
3001 let path = PathBuf::from(path);
3002 Ok(Some(ReferenceBackendEnv { raw, format, path }))
3003}
3004
3005impl RepositoryBootstrap {
3006 pub fn init(options: InitOptions) -> Result<RepositoryLayout> {
3007 if options.bare && options.separate_git_dir.is_some() {
3008 return Err(GitError::Command(
3009 "options '--bare' and '--separate-git-dir' cannot be used together".into(),
3010 ));
3011 }
3012
3013 let worktree = options.worktree;
3014 if !options.bare {
3015 fs::create_dir_all(&worktree)?;
3016 }
3017
3018 let dot_git = worktree.join(".git");
3019 let dot_git_is_symlink = fs::symlink_metadata(&dot_git)
3025 .map(|meta| meta.file_type().is_symlink())
3026 .unwrap_or(false);
3027 let git_link = if dot_git_is_symlink {
3028 fs::canonicalize(&dot_git).unwrap_or_else(|_| dot_git.clone())
3029 } else {
3030 dot_git.clone()
3031 };
3032 let mut write_git_link = false;
3033 let git_dir = if let Some(git_dir_override) = options.git_dir_override.clone() {
3034 git_dir_override
3035 } else if options.bare {
3036 worktree.clone()
3037 } else if git_link.is_file() {
3038 let existing = read_gitdir_file(&git_link)?.ok_or_else(|| {
3039 GitError::InvalidFormat(format!("invalid gitfile {}", git_link.display()))
3040 })?;
3041 if let Some(new_separate) = options.separate_git_dir.clone() {
3042 if existing != new_separate {
3043 if existing.exists() && !new_separate.exists() {
3044 if let Some(parent) = new_separate.parent() {
3045 create_shared_dir(parent, options.shared_repository.as_deref())?;
3046 }
3047 fs::rename(&existing, &new_separate)?;
3048 repair_worktrees_after_gitdir_move(&existing, &new_separate)?;
3052 }
3053 write_git_link = true;
3054 new_separate
3055 } else {
3056 existing
3057 }
3058 } else {
3059 existing
3060 }
3061 } else if let Some(separate_git_dir) = options.separate_git_dir.clone() {
3062 write_git_link = true;
3063 if let Some(parent) = separate_git_dir.parent() {
3064 create_shared_dir(parent, options.shared_repository.as_deref())?;
3065 }
3066 if git_link.is_dir() && !separate_git_dir.exists() {
3070 fs::rename(&git_link, &separate_git_dir)?;
3071 repair_worktrees_after_gitdir_move(&git_link, &separate_git_dir)?;
3075 }
3076 separate_git_dir
3077 } else {
3078 worktree.join(".git")
3079 };
3080
3081 let reinitialized = git_dir.join("HEAD").exists();
3082
3083 let alt_ref_storage = if reinitialized {
3090 None
3091 } else {
3092 reference_backend_env()?
3093 };
3094 let (object_format, ref_storage) = if reinitialized {
3095 let existing = read_existing_repository_formats(&git_dir)?;
3096 if options.object_format_explicit && options.object_format != existing.0 {
3097 return Err(GitError::Command(
3098 "attempt to reinitialize repository with different hash".into(),
3099 ));
3100 }
3101 if options.ref_storage_explicit && options.ref_storage != existing.1 {
3102 return Err(GitError::Command(
3103 "attempt to reinitialize repository with different reference storage format"
3104 .into(),
3105 ));
3106 }
3107 existing
3108 } else {
3109 (
3110 options.object_format,
3111 alt_ref_storage
3112 .as_ref()
3113 .map(|backend| backend.format)
3114 .unwrap_or(options.ref_storage),
3115 )
3116 };
3117 let ref_storage_dir = alt_ref_storage
3118 .as_ref()
3119 .map(|backend| backend.path.clone())
3120 .unwrap_or_else(|| git_dir.clone());
3121
3122 create_shared_dir(&git_dir, options.shared_repository.as_deref())?;
3123 let object_dir = options
3124 .object_dir
3125 .as_deref()
3126 .map(Path::to_path_buf)
3127 .unwrap_or_else(|| git_dir.join("objects"));
3128 create_shared_dir(
3129 object_dir.join("info"),
3130 options.shared_repository.as_deref(),
3131 )?;
3132 create_shared_dir(
3133 object_dir.join("pack"),
3134 options.shared_repository.as_deref(),
3135 )?;
3136
3137 if ref_storage == RefStorageFormat::Files {
3138 create_shared_dir(
3139 ref_storage_dir.join("refs/heads"),
3140 options.shared_repository.as_deref(),
3141 )?;
3142 create_shared_dir(
3143 ref_storage_dir.join("refs/tags"),
3144 options.shared_repository.as_deref(),
3145 )?;
3146 } else {
3147 create_shared_dir(
3148 ref_storage_dir.join("reftable"),
3149 options.shared_repository.as_deref(),
3150 )?;
3151 }
3152
3153 let head_path = git_dir.join("HEAD");
3154 if let Some(backend) = alt_ref_storage.as_ref() {
3155 fs::create_dir_all(git_dir.join("refs"))?;
3156 fs::write(
3157 git_dir.join("refs").join("heads"),
3158 b"repository uses alternate refs storage\n",
3159 )?;
3160 if !head_path.exists() {
3161 fs::write(&head_path, b"ref: refs/heads/.invalid\n")?;
3162 }
3163 if ref_storage == RefStorageFormat::Files {
3164 fs::write(
3165 ref_storage_dir.join("HEAD"),
3166 format!("ref: refs/heads/{}\n", options.initial_branch),
3167 )?;
3168 } else {
3169 write_initial_reftable(
3170 &ref_storage_dir,
3171 object_format,
3172 &options.initial_branch,
3173 options.shared_repository.as_deref(),
3174 )?;
3175 }
3176 let _ = backend;
3177 } else if ref_storage == RefStorageFormat::Files {
3178 if !head_path.exists() {
3179 fs::write(
3180 &head_path,
3181 format!("ref: refs/heads/{}\n", options.initial_branch),
3182 )?;
3183 }
3184 } else if !head_path.exists() {
3185 fs::write(&head_path, b"ref: refs/heads/.invalid\n")?;
3186 write_initial_reftable(
3187 &ref_storage_dir,
3188 object_format,
3189 &options.initial_branch,
3190 options.shared_repository.as_deref(),
3191 )?;
3192 }
3193
3194 let config_path = git_dir.join("config");
3195 if !config_path.exists() {
3196 let config_context = sley_config::ConfigIncludeContext::new(
3197 Some(sley_config::git_dir_for_include_context(&git_dir)),
3198 None,
3199 );
3200 let log_all_ref_updates_is_configured =
3201 sley_config::load_pre_dispatch_config(None, &config_context)
3202 .ok()
3203 .and_then(|config| {
3204 config
3205 .get("core", None, "logAllRefUpdates")
3206 .map(str::to_owned)
3207 })
3208 .is_some();
3209 fs::write(
3210 &config_path,
3211 build_init_config(
3212 object_format,
3213 options.bare,
3214 &options.shared_repository,
3215 ref_storage,
3216 alt_ref_storage.as_ref().map(|backend| backend.raw.as_str()),
3217 options.core_worktree.as_deref(),
3218 log_all_ref_updates_is_configured,
3219 )
3220 .to_canonical_bytes(),
3221 )?;
3222 }
3223
3224 if let Some(template_dir) = options.template_dir.as_deref() {
3225 apply_init_template(
3226 template_dir,
3227 &git_dir,
3228 options.copy_template_config,
3229 options.shared_repository.as_deref(),
3230 )?;
3231 }
3232
3233 if let Some(shared) = options.shared_repository.as_deref() {
3236 upsert_shared_repository_config(&config_path, shared)?;
3237 }
3238 let effective_shared = GitConfig::read(&config_path).ok().and_then(|config| {
3239 config
3240 .get("core", None, "sharedRepository")
3241 .map(str::to_owned)
3242 });
3243 apply_shared_file_mode(&head_path, effective_shared.as_deref())?;
3244 apply_shared_file_mode(&config_path, effective_shared.as_deref())?;
3245
3246 if write_git_link {
3247 let link_target = gitdir_link_path(&worktree, &git_dir)?;
3248 fs::write(&git_link, format!("gitdir: {link_target}\n"))?;
3252 apply_shared_file_mode(&git_link, options.shared_repository.as_deref())?;
3253 }
3254
3255 Ok(RepositoryLayout {
3256 git_dir,
3257 worktree,
3258 object_format,
3259 bare: options.bare,
3260 ref_storage,
3261 reinitialized,
3262 })
3263 }
3264}
3265
3266fn upsert_shared_repository_config(path: &Path, value: &str) -> Result<()> {
3267 let mut config = GitConfig::read(path)?;
3268 let canonical = shared_repository_config_value(value)
3269 .ok_or_else(|| GitError::Command(format!("unknown sharing mode '{value}'")))?;
3270 let section =
3271 config.sections.iter_mut().rev().find(|section| {
3272 section.name.eq_ignore_ascii_case("core") && section.subsection.is_none()
3273 });
3274 if let Some(section) = section {
3275 if let Some(entry) = section
3276 .entries
3277 .iter_mut()
3278 .rev()
3279 .find(|entry| entry.key.eq_ignore_ascii_case("sharedRepository"))
3280 {
3281 entry.value = Some(canonical);
3282 } else {
3283 section
3284 .entries
3285 .push(ConfigEntry::new("sharedRepository", Some(canonical)));
3286 }
3287 } else {
3288 config.sections.push(ConfigSection::new(
3289 "core",
3290 None,
3291 vec![ConfigEntry::new("sharedRepository", Some(canonical))],
3292 ));
3293 }
3294 fs::write(path, config.to_canonical_bytes())?;
3295 Ok(())
3296}
3297
3298fn read_existing_repository_formats(git_dir: &Path) -> Result<(ObjectFormat, RefStorageFormat)> {
3304 let Ok(config) = GitConfig::read(git_dir.join("config")) else {
3305 return Ok((ObjectFormat::Sha1, RefStorageFormat::Files));
3306 };
3307 let object_format = config.repository_object_format()?;
3308 let ref_storage = match config.get("extensions", None, "refStorage") {
3309 Some(value) if value.eq_ignore_ascii_case("reftable") => RefStorageFormat::Reftable,
3310 _ => RefStorageFormat::Files,
3311 };
3312 Ok((object_format, ref_storage))
3313}
3314
3315impl RepositoryLayout {
3316 pub fn init_at(
3317 path: impl AsRef<Path>,
3318 object_format: ObjectFormat,
3319 bare: bool,
3320 ) -> Result<Self> {
3321 Self::init_at_with_initial_branch(path, object_format, bare, "main")
3322 }
3323
3324 pub fn init_at_with_initial_branch(
3325 path: impl AsRef<Path>,
3326 object_format: ObjectFormat,
3327 bare: bool,
3328 initial_branch: &str,
3329 ) -> Result<Self> {
3330 RepositoryBootstrap::init(InitOptions {
3331 git_dir_override: None,
3332 core_worktree: None,
3333 object_dir: None,
3334 worktree: path.as_ref().to_path_buf(),
3335 object_format,
3336 object_format_explicit: false,
3337 bare,
3338 initial_branch: initial_branch.into(),
3339 template_dir: None,
3340 copy_template_config: false,
3341 separate_git_dir: None,
3342 shared_repository: None,
3343 ref_storage: RefStorageFormat::Files,
3344 ref_storage_explicit: false,
3345 })
3346 }
3347}
3348
3349fn build_init_config(
3350 object_format: ObjectFormat,
3351 bare: bool,
3352 shared_repository: &Option<String>,
3353 ref_storage: RefStorageFormat,
3354 ref_storage_value: Option<&str>,
3355 core_worktree: Option<&str>,
3356 log_all_ref_updates_is_configured: bool,
3357) -> GitConfig {
3358 let uses_extensions = object_format == ObjectFormat::Sha256
3359 || ref_storage == RefStorageFormat::Reftable
3360 || ref_storage_value.is_some();
3361 let mut core_entries = vec![
3362 ConfigEntry::new(
3363 "repositoryformatversion",
3364 Some(if uses_extensions { "1" } else { "0" }.into()),
3365 ),
3366 ConfigEntry::new("filemode", Some("true".into())),
3367 ConfigEntry::new("bare", Some(if bare { "true" } else { "false" }.into())),
3368 ];
3369 if !bare {
3370 if !log_all_ref_updates_is_configured {
3375 core_entries.push(ConfigEntry::new("logallrefupdates", Some("true".into())));
3376 }
3377 if let Some(worktree) = core_worktree {
3378 core_entries.push(ConfigEntry::new("worktree", Some(worktree.into())));
3379 }
3380 }
3381 if let Some(shared) = shared_repository
3382 && let Some(value) = shared_repository_config_value(shared)
3383 {
3384 core_entries.push(ConfigEntry::new("sharedRepository", Some(value)));
3385 }
3386 let mut sections = vec![ConfigSection::new("core", None, core_entries)];
3387 let mut extension_entries = Vec::new();
3388 if object_format == ObjectFormat::Sha256 {
3389 extension_entries.push(ConfigEntry::new("objectformat", Some("sha256".into())));
3390 }
3391 if let Some(value) = ref_storage_value {
3392 extension_entries.push(ConfigEntry::new("refStorage", Some(value.into())));
3393 } else if ref_storage == RefStorageFormat::Reftable {
3394 extension_entries.push(ConfigEntry::new("refStorage", Some("reftable".into())));
3395 }
3396 if !extension_entries.is_empty() {
3397 sections.push(ConfigSection::new("extensions", None, extension_entries));
3398 }
3399 GitConfig {
3400 preamble: Vec::new(),
3401 suffix: Vec::new(),
3402 sections,
3403 }
3404}
3405
3406fn write_initial_reftable(
3407 git_dir: &Path,
3408 object_format: ObjectFormat,
3409 initial_branch: &str,
3410 shared_repository: Option<&str>,
3411) -> Result<()> {
3412 let update_index = 1;
3413 let table_name = format!("0x{update_index:012x}-0x{update_index:012x}-00000000.ref");
3414 let refs = vec![ReftableRefRecord {
3415 name: "HEAD".into(),
3416 update_index,
3417 value: ReftableRefValue::Symbolic(format!("refs/heads/{initial_branch}")),
3418 }];
3419 let bytes = Reftable::write_ref_only(object_format, update_index, update_index, &refs)?;
3420 let reftable_dir = git_dir.join("reftable");
3421 let table_path = reftable_dir.join(&table_name);
3422 fs::write(&table_path, bytes)?;
3423 apply_shared_file_mode(&table_path, shared_repository)?;
3424 let list_path = reftable_dir.join("tables.list");
3425 fs::write(&list_path, format!("{table_name}\n"))?;
3426 apply_shared_file_mode(&list_path, shared_repository)?;
3427 let refs_dir = git_dir.join("refs");
3428 fs::create_dir_all(&refs_dir)?;
3429 let refs_heads = refs_dir.join("heads");
3430 fs::write(
3431 &refs_heads,
3432 b"this repository uses the reftable format\n" as &[u8],
3433 )?;
3434 apply_shared_file_mode(&refs_heads, shared_repository)?;
3435 Ok(())
3436}
3437
3438fn apply_init_template(
3439 template_dir: &Path,
3440 git_dir: &Path,
3441 copy_config: bool,
3442 shared_repository: Option<&str>,
3443) -> Result<()> {
3444 if !template_dir.is_dir() {
3445 return Ok(());
3446 }
3447 copy_init_template_entries(template_dir, git_dir, shared_repository)?;
3448 let template_config_path = template_dir.join("config");
3449 if copy_config && template_config_path.is_file() {
3450 let mut template_config = GitConfig::read(template_config_path)?;
3451 let current_config = GitConfig::read(git_dir.join("config"))?;
3452 template_config.sections.extend(current_config.sections);
3453 fs::write(git_dir.join("config"), template_config.to_canonical_bytes())?;
3454 }
3455 Ok(())
3456}
3457
3458fn copy_init_template_entries(
3459 source: &Path,
3460 destination: &Path,
3461 shared_repository: Option<&str>,
3462) -> Result<()> {
3463 for entry in fs::read_dir(source)? {
3464 let entry = entry?;
3465 let name = entry.file_name();
3466 let source_path = entry.path();
3467 let destination_path = destination.join(&name);
3468 if source_path.is_dir() {
3469 if !destination_path.exists() {
3470 create_shared_dir(&destination_path, shared_repository)?;
3471 }
3472 copy_init_template_entries(&source_path, &destination_path, shared_repository)?;
3473 } else if name != "config" && !destination_path.exists() {
3474 if let Some(parent) = destination_path.parent() {
3475 create_shared_dir(parent, shared_repository)?;
3476 }
3477 fs::copy(source_path, &destination_path)?;
3478 apply_shared_file_mode(&destination_path, shared_repository)?;
3479 }
3480 }
3481 Ok(())
3482}
3483
3484fn repair_worktrees_after_gitdir_move(old_git_dir: &Path, new_git_dir: &Path) -> Result<()> {
3490 let worktrees_dir = new_git_dir.join("worktrees");
3491 let entries = match fs::read_dir(&worktrees_dir) {
3492 Ok(entries) => entries,
3493 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
3495 Err(err) => return Err(err.into()),
3496 };
3497 for entry in entries {
3498 let entry = entry?;
3499 let admin_dir = entry.path();
3500 if !admin_dir.is_dir() {
3501 continue;
3502 }
3503 repair_worktree_after_gitdir_move(&admin_dir, old_git_dir)?;
3504 }
3505 Ok(())
3506}
3507
3508fn repair_worktree_after_gitdir_move(admin_dir: &Path, old_git_dir: &Path) -> Result<()> {
3513 let gitdir_file = admin_dir.join("gitdir");
3514 let Ok(raw) = fs::read_to_string(&gitdir_file) else {
3515 return Ok(());
3516 };
3517 let backlink = raw.trim();
3518 if backlink.is_empty() {
3519 return Ok(());
3520 }
3521 let backlink_path = PathBuf::from(backlink);
3522 let is_relative = backlink_path.is_relative();
3523 let id = admin_dir.file_name().unwrap_or_default();
3527 let dotgit_abs = if is_relative {
3528 let old_admin = old_git_dir.join("worktrees").join(id);
3529 normalize_lexical(&old_admin.join(&backlink_path))
3530 } else {
3531 normalize_lexical(&backlink_path)
3532 };
3533 if !dotgit_abs.exists() {
3534 return Ok(());
3535 }
3536 write_worktree_linking_files(&dotgit_abs, &gitdir_file, is_relative)?;
3537 Ok(())
3538}
3539
3540fn write_worktree_linking_files(
3547 dotgit: &Path,
3548 gitdir_file: &Path,
3549 use_relative: bool,
3550) -> Result<()> {
3551 let worktree_root = dotgit.parent().unwrap_or(Path::new("."));
3552 let worktree_root =
3553 fs::canonicalize(worktree_root).unwrap_or_else(|_| worktree_root.to_path_buf());
3554 let admin_dir = gitdir_file.parent().unwrap_or(Path::new("."));
3555 let admin_dir = fs::canonicalize(admin_dir).unwrap_or_else(|_| admin_dir.to_path_buf());
3556 let worktree_dotgit = worktree_root.join(".git");
3557 if use_relative {
3558 let backlink = relative_path_lexical(&worktree_dotgit, &admin_dir);
3560 fs::write(gitdir_file, format!("{backlink}\n"))?;
3561 let link = relative_path_lexical(&admin_dir, &worktree_root);
3563 fs::write(&worktree_dotgit, format!("gitdir: {link}\n"))?;
3564 } else {
3565 fs::write(gitdir_file, format!("{}\n", worktree_dotgit.display()))?;
3566 fs::write(
3567 &worktree_dotgit,
3568 format!("gitdir: {}\n", admin_dir.display()),
3569 )?;
3570 }
3571 Ok(())
3572}
3573
3574fn normalize_lexical(path: &Path) -> PathBuf {
3579 use std::path::Component;
3580 let mut out = PathBuf::new();
3581 for component in path.components() {
3582 match component {
3583 Component::ParentDir => {
3584 if !out.pop() {
3585 out.push("..");
3586 }
3587 }
3588 Component::CurDir => {}
3589 other => out.push(other.as_os_str()),
3590 }
3591 }
3592 out
3593}
3594
3595fn relative_path_lexical(target: &Path, base: &Path) -> String {
3599 let target = normalize_lexical(target);
3600 let base = normalize_lexical(base);
3601 let target_components: Vec<_> = target.components().collect();
3602 let base_components: Vec<_> = base.components().collect();
3603 let common = target_components
3604 .iter()
3605 .zip(base_components.iter())
3606 .take_while(|(a, b)| a == b)
3607 .count();
3608 let mut result = PathBuf::new();
3609 for _ in common..base_components.len() {
3610 result.push("..");
3611 }
3612 for component in &target_components[common..] {
3613 result.push(component.as_os_str());
3614 }
3615 if result.as_os_str().is_empty() {
3616 ".".to_string()
3617 } else {
3618 result.display().to_string()
3619 }
3620}
3621
3622fn read_gitdir_file(path: &Path) -> Result<Option<PathBuf>> {
3623 let contents = fs::read_to_string(path)?;
3624 let Some(target) = contents.trim().strip_prefix("gitdir:") else {
3625 return Ok(None);
3626 };
3627 let target = PathBuf::from(target.trim());
3628 if target.is_absolute() {
3629 Ok(Some(target))
3630 } else {
3631 Ok(Some(
3632 path.parent().unwrap_or_else(|| Path::new("")).join(target),
3633 ))
3634 }
3635}
3636
3637fn gitdir_link_path(_worktree: &Path, git_dir: &Path) -> Result<String> {
3644 let resolved = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
3645 Ok(resolved.display().to_string())
3646}
3647
3648fn create_shared_dir(path: impl AsRef<Path>, shared_repository: Option<&str>) -> Result<()> {
3649 let path = path.as_ref();
3650 let mut created = Vec::new();
3651 let mut ancestor = path;
3652 while !ancestor.exists() {
3653 created.push(ancestor.to_path_buf());
3654 let Some(parent) = ancestor.parent() else {
3655 break;
3656 };
3657 ancestor = parent;
3658 }
3659 fs::create_dir_all(path)?;
3660 if created.is_empty() {
3661 apply_shared_dir_mode(path, shared_repository)?;
3662 } else {
3663 for directory in created.iter().rev() {
3664 apply_shared_dir_mode(directory, shared_repository)?;
3665 }
3666 }
3667 Ok(())
3668}
3669
3670fn apply_shared_dir_mode(path: &Path, shared_repository: Option<&str>) -> Result<()> {
3671 #[cfg(unix)]
3672 {
3673 use std::os::unix::fs::PermissionsExt;
3674 let Some(perm) = shared_repository.and_then(parse_shared_repository_perm) else {
3675 return Ok(());
3676 };
3677 let metadata = fs::metadata(path)?;
3678 if metadata.is_dir() {
3679 let old_mode = metadata.permissions().mode();
3680 let new_mode = calc_shared_perm(perm, old_mode, true);
3681 if new_mode & 0o7777 != old_mode & 0o7777 {
3682 let mut permissions = metadata.permissions();
3683 permissions.set_mode(new_mode & 0o7777);
3684 fs::set_permissions(path, permissions)?;
3685 }
3686 }
3687 }
3688 #[cfg(not(unix))]
3689 {
3690 let _ = (path, shared_repository);
3691 }
3692 Ok(())
3693}
3694
3695fn apply_shared_file_mode(path: &Path, shared_repository: Option<&str>) -> Result<()> {
3696 #[cfg(unix)]
3697 {
3698 use std::os::unix::fs::PermissionsExt;
3699 let Some(perm) = shared_repository.and_then(parse_shared_repository_perm) else {
3700 return Ok(());
3701 };
3702 let metadata = fs::metadata(path)?;
3703 if metadata.is_file() {
3704 let old_mode = metadata.permissions().mode();
3705 let new_mode = calc_shared_perm(perm, old_mode, false);
3706 if new_mode & 0o7777 != old_mode & 0o7777 {
3707 let mut permissions = metadata.permissions();
3708 permissions.set_mode(new_mode & 0o7777);
3709 fs::set_permissions(path, permissions)?;
3710 }
3711 }
3712 }
3713 #[cfg(not(unix))]
3714 {
3715 let _ = (path, shared_repository);
3716 }
3717 Ok(())
3718}
3719
3720pub fn adjust_shared_repository_file(path: &Path, shared_repository: Option<&str>) -> Result<()> {
3723 apply_shared_file_mode(path, shared_repository)
3724}
3725
3726#[derive(Debug, Clone, Default)]
3732pub struct SharedRepositoryPermissions {
3733 git_dir: Option<PathBuf>,
3734 value: std::sync::Arc<std::sync::OnceLock<Option<String>>>,
3735}
3736
3737impl SharedRepositoryPermissions {
3738 #[must_use]
3741 pub fn from_git_dir(git_dir: &Path) -> Self {
3742 Self {
3743 git_dir: Some(git_dir.to_path_buf()),
3744 value: std::sync::Arc::default(),
3745 }
3746 }
3747
3748 pub fn adjust_file(&self, path: &Path) -> Result<()> {
3750 apply_shared_file_mode(path, self.value())
3751 }
3752
3753 pub fn create_dir_all(&self, path: &Path) -> Result<()> {
3756 create_shared_dir(path, self.value())
3757 }
3758
3759 pub fn adjust_dir(&self, path: &Path) -> Result<()> {
3761 apply_shared_dir_mode(path, self.value())
3762 }
3763
3764 fn value(&self) -> Option<&str> {
3765 self.value
3766 .get_or_init(|| {
3767 self.git_dir.as_ref().and_then(|git_dir| {
3768 GitConfig::read(git_dir.join("config"))
3769 .ok()
3770 .and_then(|config| {
3771 config
3772 .get("core", None, "sharedRepository")
3773 .map(str::to_string)
3774 })
3775 })
3776 })
3777 .as_deref()
3778 }
3779}
3780
3781pub fn canonical_shared_repository_value(value: &str) -> Result<Option<String>> {
3784 if matches!(value, "false" | "no" | "off" | "0" | "umask") {
3785 return Ok(None);
3786 }
3787 if let Some(value) = shared_repository_config_value(value) {
3788 if value.starts_with('0') {
3789 let mode = u32::from_str_radix(&value, 8)
3790 .map_err(|_| GitError::Command(format!("unknown sharing mode '{value}'")))?;
3791 if mode & 0o600 != 0o600 {
3792 return Err(GitError::Command(
3793 "permission denied while setting up shared repository".into(),
3794 ));
3795 }
3796 }
3797 return Ok(Some(value));
3798 }
3799 Err(GitError::Command(format!("unknown sharing mode '{value}'")))
3800}
3801
3802fn shared_repository_config_value(shared_repository: &str) -> Option<String> {
3806 match shared_repository {
3807 "false" | "0" | "umask" => None,
3808 "group" | "1" | "true" => Some("1".into()),
3809 "all" | "world" | "everybody" | "2" | "3" => Some("2".into()),
3810 value if value.starts_with('0') && value.len() > 1 => Some(value.to_string()),
3811 _ => None,
3812 }
3813}
3814
3815#[cfg(unix)]
3820#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3821enum SharedPerm {
3822 Group,
3824 Everybody,
3826 Exact(u32),
3828}
3829
3830#[cfg(unix)]
3831fn parse_shared_repository_perm(value: &str) -> Option<SharedPerm> {
3832 match value {
3833 "umask" | "false" | "no" | "off" => None,
3834 "group" | "true" | "yes" | "on" => Some(SharedPerm::Group),
3835 "all" | "world" | "everybody" => Some(SharedPerm::Everybody),
3836 _ => {
3837 let parsed = u32::from_str_radix(value, 8).ok()?;
3838 match parsed {
3839 0 => None,
3840 1 => Some(SharedPerm::Group),
3841 2 => Some(SharedPerm::Everybody),
3842 mode if (mode & 0o600) == 0o600 => Some(SharedPerm::Exact(mode & 0o666)),
3845 _ => None,
3846 }
3847 }
3848 }
3849}
3850
3851#[cfg(unix)]
3856fn calc_shared_perm(perm: SharedPerm, mode: u32, is_dir: bool) -> u32 {
3857 let (base, exact) = match perm {
3858 SharedPerm::Group => (0o660, false),
3859 SharedPerm::Everybody => (0o664, false),
3860 SharedPerm::Exact(bits) => (bits, true),
3861 };
3862 let mut tweak = base;
3863 if mode & 0o200 == 0 {
3864 tweak &= !0o222;
3865 }
3866 if mode & 0o100 != 0 {
3867 tweak |= (tweak & 0o444) >> 2;
3868 }
3869 let mut new_mode = if exact {
3870 (mode & !0o777) | tweak
3871 } else {
3872 mode | tweak
3873 };
3874 if is_dir {
3875 new_mode |= (new_mode & 0o444) >> 2;
3876 if new_mode & 0o060 != 0 {
3877 new_mode |= 0o2000;
3878 }
3879 }
3880 new_mode
3881}
3882
3883fn u32_be(bytes: &[u8]) -> u32 {
3884 u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
3885}
3886
3887fn u64_be(bytes: &[u8]) -> u64 {
3888 u64::from_be_bytes([
3889 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
3890 ])
3891}
3892
3893fn hash_function_id(format: ObjectFormat) -> u32 {
3894 match format {
3895 ObjectFormat::Sha1 => 1,
3896 ObjectFormat::Sha256 => 2,
3897 }
3898}
3899
3900#[cfg(test)]
3901mod tests {
3902 use super::*;
3903 use std::io::Write;
3904 use std::process::Command;
3905 use std::time::{SystemTime, UNIX_EPOCH};
3906
3907 #[test]
3908 fn reftable_empty_table_round_trips() {
3909 let bytes = Reftable::write_ref_only(ObjectFormat::Sha1, 1, 1, &[])
3910 .expect("test operation should succeed");
3911 let table = Reftable::parse(&bytes).expect("test operation should succeed");
3912
3913 assert_eq!(table.header.version, ReftableVersion::V1);
3914 assert_eq!(table.header.object_format, ObjectFormat::Sha1);
3915 assert_eq!(table.refs, Vec::new());
3916 }
3917
3918 #[test]
3919 fn reftable_ref_only_table_round_trips_refs() {
3920 let head = oid("1111111111111111111111111111111111111111");
3921 let tag = oid("2222222222222222222222222222222222222222");
3922 let peeled = oid("3333333333333333333333333333333333333333");
3923 let refs = vec![
3924 ReftableRefRecord {
3925 name: "refs/tags/v1".into(),
3926 update_index: 7,
3927 value: ReftableRefValue::Peeled {
3928 target: tag,
3929 peeled: peeled.clone(),
3930 },
3931 },
3932 ReftableRefRecord {
3933 name: "HEAD".into(),
3934 update_index: 7,
3935 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
3936 },
3937 ReftableRefRecord {
3938 name: "refs/heads/main".into(),
3939 update_index: 7,
3940 value: ReftableRefValue::Direct(head.clone()),
3941 },
3942 ];
3943
3944 let bytes = Reftable::write_ref_only(ObjectFormat::Sha1, 7, 7, &refs)
3945 .expect("test operation should succeed");
3946 let table = Reftable::parse(&bytes).expect("test operation should succeed");
3947
3948 assert_eq!(table.header.min_update_index, 7);
3949 assert_eq!(table.header.max_update_index, 7);
3950 assert_eq!(
3951 table.refs,
3952 vec![
3953 ReftableRefRecord {
3954 name: "HEAD".into(),
3955 update_index: 7,
3956 value: ReftableRefValue::Symbolic("refs/heads/main".into()),
3957 },
3958 ReftableRefRecord {
3959 name: "refs/heads/main".into(),
3960 update_index: 7,
3961 value: ReftableRefValue::Direct(head),
3962 },
3963 ReftableRefRecord {
3964 name: "refs/tags/v1".into(),
3965 update_index: 7,
3966 value: ReftableRefValue::Peeled {
3967 target: tag,
3968 peeled,
3969 },
3970 },
3971 ]
3972 );
3973 }
3974
3975 #[test]
3976 fn reftable_small_blocks_write_ref_and_object_indexes() {
3977 let commit = oid("24b24cf8a829f5b8c30dfc018b0a459a2ccaf380");
3978 let mut refs = vec![
3979 ReftableRefRecord {
3980 name: "HEAD".into(),
3981 update_index: 8,
3982 value: ReftableRefValue::Symbolic("refs/heads/master".into()),
3983 },
3984 ReftableRefRecord {
3985 name: "refs/heads/master".into(),
3986 update_index: 8,
3987 value: ReftableRefValue::Direct(commit),
3988 },
3989 ReftableRefRecord {
3990 name: "refs/tags/initial".into(),
3991 update_index: 8,
3992 value: ReftableRefValue::Direct(commit),
3993 },
3994 ];
3995 refs.extend((1..=5).map(|index| ReftableRefRecord {
3996 name: format!("refs/heads/branch-{index}"),
3997 update_index: 8,
3998 value: ReftableRefValue::Direct(commit),
3999 }));
4000 let options = ReftableWriteOptions {
4001 block_size: 100,
4002 ..ReftableWriteOptions::default()
4003 };
4004 let bytes = Reftable::write_with_options(ObjectFormat::Sha1, 1, 8, &refs, &[], options)
4005 .expect("small-block table should serialize");
4006 let footer = parse_reftable_footer(&bytes, ReftableVersion::V1)
4007 .expect("small-block footer should parse");
4008 let table = Reftable::parse(&bytes).expect("small-block table should round-trip");
4009
4010 assert_eq!(table.refs.len(), refs.len());
4011 assert_eq!(footer.ref_index_position, 400);
4012 assert_eq!(footer.obj_position, 500);
4013 assert_eq!(footer.obj_id_len, 2);
4014 assert_eq!(bytes[24], b'r');
4015 assert_eq!(bytes[100], b'r');
4016 assert_eq!(bytes[200], b'r');
4017 assert_eq!(bytes[300], b'r');
4018 assert_eq!(bytes[400], b'i');
4019 assert_eq!(bytes[500], b'o');
4020
4021 let without_objects = Reftable::write_with_options(
4022 ObjectFormat::Sha1,
4023 1,
4024 8,
4025 &refs,
4026 &[],
4027 ReftableWriteOptions {
4028 index_objects: false,
4029 ..options
4030 },
4031 )
4032 .expect("object index can be disabled");
4033 let footer = parse_reftable_footer(&without_objects, ReftableVersion::V1)
4034 .expect("footer without object index should parse");
4035 assert_eq!(footer.ref_index_position, 400);
4036 assert_eq!(footer.obj_position, 0);
4037 assert_eq!(footer.obj_id_len, 0);
4038 }
4039
4040 #[test]
4041 fn reftable_sha256_uses_version_2_hash_id() {
4042 let oid = ObjectId::from_hex(
4043 ObjectFormat::Sha256,
4044 "1111111111111111111111111111111111111111111111111111111111111111",
4045 )
4046 .expect("test operation should succeed");
4047 let refs = vec![ReftableRefRecord {
4048 name: "refs/heads/main".into(),
4049 update_index: 3,
4050 value: ReftableRefValue::Direct(oid),
4051 }];
4052
4053 let bytes = Reftable::write_ref_only(ObjectFormat::Sha256, 3, 3, &refs)
4054 .expect("test operation should succeed");
4055 let table = Reftable::parse(&bytes).expect("test operation should succeed");
4056
4057 assert_eq!(table.header.version, ReftableVersion::V2);
4058 assert_eq!(table.header.object_format, ObjectFormat::Sha256);
4059 assert_eq!(table.refs[0].value, ReftableRefValue::Direct(oid));
4060 }
4061
4062 #[test]
4063 fn upstream_git_reads_rust_written_minimal_reftable() {
4064 let root = unique_temp_dir("reftable-upstream");
4065 fs::create_dir_all(&root).expect("create temp repo");
4066 {
4067 run_success("git", &root, &["init", "-q"]);
4068 let oid = run_success_with_stdin(
4069 "git",
4070 &root,
4071 &["hash-object", "-w", "--stdin"],
4072 b"payload\n",
4073 );
4074 let oid = String::from_utf8(oid).expect("oid is utf8");
4075 let oid = ObjectId::from_hex(ObjectFormat::Sha1, oid.trim())
4076 .expect("test operation should succeed");
4077 let git_dir = root.join(".git");
4078 fs::write(
4079 git_dir.join("config"),
4080 b"[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = reftable\n",
4081 )
4082 .expect("write config");
4083 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n").expect("write HEAD");
4084 let reftable_dir = git_dir.join("reftable");
4085 fs::create_dir_all(&reftable_dir).expect("create reftable dir");
4086 let table_name = "0x000000000001-0x000000000001-00000000.ref";
4087 let table = Reftable::write_ref_only(
4088 ObjectFormat::Sha1,
4089 1,
4090 1,
4091 &[ReftableRefRecord {
4092 name: "refs/heads/main".into(),
4093 update_index: 1,
4094 value: ReftableRefValue::Direct(oid),
4095 }],
4096 )
4097 .expect("test operation should succeed");
4098 fs::write(reftable_dir.join(table_name), table).expect("write reftable");
4099 fs::write(reftable_dir.join("tables.list"), format!("{table_name}\n"))
4100 .expect("write tables.list");
4101
4102 let output = run_success("git", &root, &["show-ref"]);
4103 assert_eq!(
4104 String::from_utf8(output).expect("show-ref output is utf8"),
4105 format!("{oid} refs/heads/main\n")
4106 );
4107 };
4108 let _ = fs::remove_dir_all(&root);
4109 }
4110
4111 #[test]
4112 fn reftable_log_records_round_trip() {
4113 let old = oid("0000000000000000000000000000000000000000");
4114 let new1 = oid("1111111111111111111111111111111111111111");
4115 let new2 = oid("2222222222222222222222222222222222222222");
4116 let refs = vec![ReftableRefRecord {
4117 name: "refs/heads/main".into(),
4118 update_index: 2,
4119 value: ReftableRefValue::Direct(new2.clone()),
4120 }];
4121 let logs = vec![
4122 ReftableLogRecord {
4123 refname: "refs/heads/main".into(),
4124 update_index: 1,
4125 value: ReftableLogValue::Update(ReftableLogUpdate {
4126 old_oid: old.clone(),
4127 new_oid: new1.clone(),
4128 name: "Committer One".into(),
4129 email: "one@example.com".into(),
4130 time: 1_700_000_000,
4131 tz_offset: 120,
4132 message: "commit (initial): first".into(),
4133 }),
4134 },
4135 ReftableLogRecord {
4136 refname: "refs/heads/main".into(),
4137 update_index: 2,
4138 value: ReftableLogValue::Update(ReftableLogUpdate {
4139 old_oid: new1.clone(),
4140 new_oid: new2.clone(),
4141 name: "Committer Two".into(),
4142 email: "two@example.com".into(),
4143 time: 1_700_000_100,
4144 tz_offset: -300,
4145 message: "commit: second".into(),
4146 }),
4147 },
4148 ];
4149
4150 let bytes = Reftable::write(ObjectFormat::Sha1, 1, 2, &refs, &logs)
4151 .expect("test operation should succeed");
4152 let table = Reftable::parse(&bytes).expect("test operation should succeed");
4153
4154 assert_eq!(table.refs.len(), 1);
4155 assert_eq!(table.logs.len(), 2);
4157 assert_eq!(table.logs[0].update_index, 2);
4158 assert_eq!(table.logs[1].update_index, 1);
4159 let ReftableLogValue::Update(update0) = &table.logs[0].value else {
4160 panic!("expected update");
4161 };
4162 assert_eq!(update0.tz_offset, -300);
4163 assert_eq!(update0.old_oid, new1);
4164 assert_eq!(update0.new_oid, new2);
4165 assert_eq!(update0.message, "commit: second");
4166 let ReftableLogValue::Update(update1) = &table.logs[1].value else {
4167 panic!("expected update");
4168 };
4169 assert_eq!(update1.tz_offset, 120);
4170 assert_eq!(update1.old_oid, old);
4171 assert_eq!(update1.message, "commit (initial): first");
4172 }
4173
4174 #[test]
4177 fn upstream_git_reads_rust_written_reftable_log() {
4178 let root = unique_temp_dir("reftable-log-upstream");
4179 fs::create_dir_all(&root).expect("create temp repo");
4180 {
4181 run_success("git", &root, &["init", "-q"]);
4182 let empty_tree = run_success_with_stdin("git", &root, &["mktree"], b"");
4185 let empty_tree = String::from_utf8(empty_tree).expect("tree oid is utf8");
4186 let commit_oid = run_success(
4187 "git",
4188 &root,
4189 &[
4190 "-c",
4191 "user.name=Reftable Writer",
4192 "-c",
4193 "user.email=writer@example.com",
4194 "commit-tree",
4195 empty_tree.trim(),
4196 "-m",
4197 "seed",
4198 ],
4199 );
4200 let commit_str = String::from_utf8(commit_oid).expect("commit oid is utf8");
4201 let new_oid = ObjectId::from_hex(ObjectFormat::Sha1, commit_str.trim())
4202 .expect("test operation should succeed");
4203 let zero = oid("0000000000000000000000000000000000000000");
4204 let git_dir = root.join(".git");
4205 fs::write(
4206 git_dir.join("config"),
4207 b"[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = reftable\n",
4208 )
4209 .expect("write config");
4210 fs::write(git_dir.join("HEAD"), b"ref: refs/heads/.invalid\n").expect("write HEAD");
4211 let reftable_dir = git_dir.join("reftable");
4212 fs::create_dir_all(&reftable_dir).expect("create reftable dir");
4213 let table_name = "0x000000000001-0x000000000001-00000001.ref";
4216 let table = Reftable::write(
4217 ObjectFormat::Sha1,
4218 1,
4219 1,
4220 &[ReftableRefRecord {
4221 name: "refs/heads/main".into(),
4222 update_index: 1,
4223 value: ReftableRefValue::Direct(new_oid.clone()),
4224 }],
4225 &[ReftableLogRecord {
4226 refname: "refs/heads/main".into(),
4227 update_index: 1,
4228 value: ReftableLogValue::Update(ReftableLogUpdate {
4229 old_oid: zero,
4230 new_oid: new_oid.clone(),
4231 name: "Reftable Writer".into(),
4232 email: "writer@example.com".into(),
4233 time: 1_700_000_000,
4234 tz_offset: 0,
4235 message: "commit (initial): seed\n".into(),
4237 }),
4238 }],
4239 )
4240 .expect("test operation should succeed");
4241 fs::write(reftable_dir.join(table_name), table).expect("write reftable");
4242 fs::write(reftable_dir.join("tables.list"), format!("{table_name}\n"))
4243 .expect("write tables.list");
4244
4245 let output = run_success(
4247 "git",
4248 &root,
4249 &["reflog", "show", "--format=%H %gs", "refs/heads/main"],
4250 );
4251 let text = String::from_utf8(output).expect("reflog output is utf8");
4252 assert!(
4253 text.contains("commit (initial): seed"),
4254 "git reflog did not surface our log message: {text:?}"
4255 );
4256 assert!(
4257 text.contains(&new_oid.to_string()),
4258 "git reflog did not surface our new oid: {text:?}"
4259 );
4260 };
4261 if std::env::var_os("SLEY_KEEP").is_none() {
4262 let _ = fs::remove_dir_all(&root);
4263 }
4264 }
4265
4266 #[test]
4267 fn parses_commit_graph_core_chunks() {
4268 let tree = oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
4269 let commits = vec![
4270 (
4271 oid("1111111111111111111111111111111111111111"),
4272 tree,
4273 Vec::new(),
4274 1,
4275 1,
4276 ),
4277 (
4278 oid("2222222222222222222222222222222222222222"),
4279 tree,
4280 vec![0],
4281 2,
4282 2,
4283 ),
4284 (
4285 oid("3333333333333333333333333333333333333333"),
4286 tree,
4287 vec![1],
4288 3,
4289 3,
4290 ),
4291 (
4292 oid("4444444444444444444444444444444444444444"),
4293 tree,
4294 vec![0, 1, 2],
4295 4,
4296 0x1_0000_0001,
4297 ),
4298 ];
4299 let bytes = commit_graph(ObjectFormat::Sha1, 0, &commit_graph_chunks(&commits));
4300
4301 let parsed =
4302 CommitGraph::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed");
4303 assert_eq!(parsed.version, 1);
4304 assert_eq!(parsed.format, ObjectFormat::Sha1);
4305 assert_eq!(parsed.base_graph_count, 0);
4306 assert_eq!(parsed.commits.len(), 4);
4307 let merge = parsed
4308 .find(&commits[3].0)
4309 .expect("test operation should succeed");
4310 assert_eq!(merge.parents, vec![0, 1, 2]);
4311 assert_eq!(merge.generation, 4);
4312 assert_eq!(merge.commit_time, 0x1_0000_0001);
4313 assert_eq!(merge.corrected_commit_date_offset, None);
4314 assert!(parsed.base_graphs.is_empty());
4315 assert_eq!(parsed.bloom_filters, None);
4316 assert_eq!(parsed.chunks[0].id, *b"OIDF");
4317 }
4318
4319 #[test]
4320 fn writes_commit_graph_core_chunks_that_round_trip() {
4321 let base = oid("1111111111111111111111111111111111111111");
4322 let main = oid("2222222222222222222222222222222222222222");
4323 let side = oid("3333333333333333333333333333333333333333");
4324 let merge = oid("4444444444444444444444444444444444444444");
4325 let tree = oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
4326 let bytes = CommitGraph::write(
4327 ObjectFormat::Sha1,
4328 &[
4329 CommitGraphWriteEntry {
4330 oid: merge.clone(),
4331 tree,
4332 parents: vec![main.clone(), side.clone()],
4333 generation: 3,
4334 commit_time: 30,
4335 bloom_filter: None,
4336 },
4337 CommitGraphWriteEntry {
4338 oid: base,
4339 tree,
4340 parents: Vec::new(),
4341 generation: 1,
4342 commit_time: 10,
4343 bloom_filter: None,
4344 },
4345 CommitGraphWriteEntry {
4346 oid: main.clone(),
4347 tree,
4348 parents: vec![base],
4349 generation: 2,
4350 commit_time: 20,
4351 bloom_filter: None,
4352 },
4353 CommitGraphWriteEntry {
4354 oid: side.clone(),
4355 tree,
4356 parents: vec![base],
4357 generation: 2,
4358 commit_time: 21,
4359 bloom_filter: None,
4360 },
4361 ],
4362 )
4363 .expect("test operation should succeed");
4364
4365 let parsed =
4366 CommitGraph::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed");
4367 assert_eq!(parsed.commits.len(), 4);
4368 assert_eq!(
4369 parsed
4370 .find(&base)
4371 .expect("test operation should succeed")
4372 .parents,
4373 Vec::<u32>::new()
4374 );
4375 assert_eq!(
4376 parsed
4377 .find(&main)
4378 .expect("test operation should succeed")
4379 .parents,
4380 vec![0]
4381 );
4382 assert_eq!(
4383 parsed
4384 .find(&side)
4385 .expect("test operation should succeed")
4386 .parents,
4387 vec![0]
4388 );
4389 assert_eq!(
4390 parsed
4391 .find(&merge)
4392 .expect("test operation should succeed")
4393 .parents,
4394 vec![1, 2]
4395 );
4396 assert_eq!(
4397 parsed
4398 .find(&merge)
4399 .expect("test operation should succeed")
4400 .generation,
4401 3
4402 );
4403 assert_eq!(
4404 parsed
4405 .find(&merge)
4406 .expect("test operation should succeed")
4407 .commit_time,
4408 30
4409 );
4410 }
4411
4412 #[test]
4413 fn parses_commit_graph_bloom_filters() {
4414 let tree = oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
4415 let commits = vec![
4416 (
4417 oid("1111111111111111111111111111111111111111"),
4418 tree,
4419 Vec::new(),
4420 1,
4421 1,
4422 ),
4423 (
4424 oid("2222222222222222222222222222222222222222"),
4425 tree,
4426 vec![0],
4427 2,
4428 2,
4429 ),
4430 ];
4431 let mut chunks = commit_graph_chunks(&commits);
4432 chunks.push((*b"BIDX", commit_graph_bidx(&[2, 3])));
4433 chunks.push((*b"BDAT", commit_graph_bdat(2, 7, 10, &[0xaa, 0xbb, 0xcc])));
4434 let bytes = commit_graph(ObjectFormat::Sha1, 0, &chunks);
4435
4436 let parsed =
4437 CommitGraph::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed");
4438 assert_eq!(
4439 parsed.bloom_filters,
4440 Some(CommitGraphBloomFilters {
4441 hash_version: 2,
4442 hash_count: 7,
4443 bits_per_entry: 10,
4444 filters: vec![vec![0xaa, 0xbb], vec![0xcc]],
4445 })
4446 );
4447 }
4448
4449 #[test]
4450 fn writes_commit_graph_bloom_filters_with_requested_hash_version() {
4451 let tree = oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
4452 let mut settings = DEFAULT_COMMIT_GRAPH_BLOOM_SETTINGS;
4453 settings.hash_version = 2;
4454 let bytes = CommitGraph::write_with_bloom_settings(
4455 ObjectFormat::Sha1,
4456 &[CommitGraphWriteEntry {
4457 oid: oid("1111111111111111111111111111111111111111"),
4458 tree,
4459 parents: Vec::new(),
4460 generation: 1,
4461 commit_time: 1,
4462 bloom_filter: Some(vec![0xaa, 0xbb]),
4463 }],
4464 settings,
4465 )
4466 .expect("test operation should succeed");
4467
4468 let parsed =
4469 CommitGraph::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed");
4470 let bloom = parsed
4471 .bloom_filters
4472 .expect("test operation should parse bloom filters");
4473 assert_eq!(bloom.hash_version, 2);
4474 assert_eq!(bloom.hash_count, settings.hash_count);
4475 assert_eq!(bloom.bits_per_entry, settings.bits_per_entry);
4476 assert_eq!(bloom.filters, vec![vec![0xaa, 0xbb]]);
4477 }
4478
4479 #[test]
4480 fn rejects_unsupported_commit_graph_bloom_hash_version_on_write() {
4481 let tree = oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
4482 let mut settings = DEFAULT_COMMIT_GRAPH_BLOOM_SETTINGS;
4483 settings.hash_version = 3;
4484
4485 assert!(
4486 CommitGraph::write_with_bloom_settings(
4487 ObjectFormat::Sha1,
4488 &[CommitGraphWriteEntry {
4489 oid: oid("1111111111111111111111111111111111111111"),
4490 tree,
4491 parents: Vec::new(),
4492 generation: 1,
4493 commit_time: 1,
4494 bloom_filter: Some(vec![0xaa]),
4495 }],
4496 settings,
4497 )
4498 .is_err()
4499 );
4500 }
4501
4502 #[test]
4503 fn parses_commit_graph_generation_data() {
4504 let tree = oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
4505 let commits = vec![
4506 (
4507 oid("1111111111111111111111111111111111111111"),
4508 tree,
4509 Vec::new(),
4510 1,
4511 1,
4512 ),
4513 (
4514 oid("2222222222222222222222222222222222222222"),
4515 tree,
4516 vec![0],
4517 2,
4518 2,
4519 ),
4520 ];
4521 let mut chunks = commit_graph_chunks(&commits);
4522 chunks.push((*b"GDA2", commit_graph_gda2(&[7, 0x8000_0000])));
4523 chunks.push((*b"GDO2", commit_graph_gdo2(&[0x1_0000_0007])));
4524 let bytes = commit_graph(ObjectFormat::Sha1, 0, &chunks);
4525
4526 let parsed =
4527 CommitGraph::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed");
4528 assert_eq!(parsed.commits[0].corrected_commit_date_offset, Some(7));
4529 assert_eq!(
4530 parsed.commits[1].corrected_commit_date_offset,
4531 Some(0x1_0000_0007)
4532 );
4533 }
4534
4535 #[test]
4536 fn parses_commit_graph_base_graph_hashes() {
4537 let commit = (
4538 oid("1111111111111111111111111111111111111111"),
4539 oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
4540 Vec::new(),
4541 1,
4542 1,
4543 );
4544 let base_graph = oid("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
4545 let mut chunks = commit_graph_chunks(&[commit]);
4546 chunks.push((*b"BASE", base_graph.as_bytes().to_vec()));
4547 let bytes = commit_graph(ObjectFormat::Sha1, 1, &chunks);
4548
4549 let parsed =
4550 CommitGraph::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed");
4551 assert_eq!(parsed.base_graph_count, 1);
4552 assert_eq!(parsed.base_graphs, vec![base_graph]);
4553 }
4554
4555 #[test]
4556 fn rejects_bad_commit_graph_shape() {
4557 let commit = (
4558 oid("1111111111111111111111111111111111111111"),
4559 oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
4560 Vec::new(),
4561 1,
4562 1,
4563 );
4564 let chunks = commit_graph_chunks(std::slice::from_ref(&commit));
4565 let mut bad_checksum = commit_graph(ObjectFormat::Sha1, 0, &chunks);
4566 let last = bad_checksum.len() - 1;
4567 bad_checksum[last] ^= 1;
4568 assert!(CommitGraph::parse(&bad_checksum, ObjectFormat::Sha1).is_err());
4569
4570 let missing_cdat = commit_graph(
4571 ObjectFormat::Sha1,
4572 0,
4573 &chunks
4574 .iter()
4575 .filter(|(id, _data)| id != b"CDAT")
4576 .cloned()
4577 .collect::<Vec<_>>(),
4578 );
4579 assert!(CommitGraph::parse(&missing_cdat, ObjectFormat::Sha1).is_err());
4580
4581 let bad_fanout = commit_graph(
4582 ObjectFormat::Sha1,
4583 0,
4584 &[
4585 (*b"OIDF", vec![0; 256 * 4]),
4586 (*b"OIDL", commit.0.as_bytes().to_vec()),
4587 (*b"CDAT", commit_graph_cdat(std::slice::from_ref(&commit)).0),
4588 ],
4589 );
4590 assert!(CommitGraph::parse(&bad_fanout, ObjectFormat::Sha1).is_err());
4591
4592 let octopus = (
4593 oid("2222222222222222222222222222222222222222"),
4594 oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
4595 vec![0, 0, 0],
4596 2,
4597 2,
4598 );
4599 let missing_edge = commit_graph(
4600 ObjectFormat::Sha1,
4601 0,
4602 &commit_graph_chunks(&[commit.clone(), octopus])
4603 .into_iter()
4604 .filter(|(id, _data)| id != b"EDGE")
4605 .collect::<Vec<_>>(),
4606 );
4607 assert!(CommitGraph::parse(&missing_edge, ObjectFormat::Sha1).is_err());
4608
4609 let mut bad_base = commit_graph_chunks(&[commit]);
4610 bad_base.push((*b"BASE", vec![0]));
4611 let bad_base = commit_graph(ObjectFormat::Sha1, 1, &bad_base);
4612 assert!(CommitGraph::parse(&bad_base, ObjectFormat::Sha1).is_err());
4613 }
4614
4615 #[test]
4616 fn rejects_bad_commit_graph_generation_data() {
4617 let commit = (
4618 oid("1111111111111111111111111111111111111111"),
4619 oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
4620 Vec::new(),
4621 1,
4622 1,
4623 );
4624
4625 let mut short_gda2 = commit_graph_chunks(std::slice::from_ref(&commit));
4626 short_gda2.push((*b"GDA2", vec![0]));
4627 let short_gda2 = commit_graph(ObjectFormat::Sha1, 0, &short_gda2);
4628 assert!(CommitGraph::parse(&short_gda2, ObjectFormat::Sha1).is_err());
4629
4630 let mut missing_gdo2 = commit_graph_chunks(std::slice::from_ref(&commit));
4631 missing_gdo2.push((*b"GDA2", commit_graph_gda2(&[0x8000_0000])));
4632 let missing_gdo2 = commit_graph(ObjectFormat::Sha1, 0, &missing_gdo2);
4633 assert!(CommitGraph::parse(&missing_gdo2, ObjectFormat::Sha1).is_err());
4634
4635 let mut bad_gdo2 = commit_graph_chunks(std::slice::from_ref(&commit));
4636 bad_gdo2.push((*b"GDA2", commit_graph_gda2(&[0x8000_0000])));
4637 bad_gdo2.push((*b"GDO2", vec![0]));
4638 let bad_gdo2 = commit_graph(ObjectFormat::Sha1, 0, &bad_gdo2);
4639 assert!(CommitGraph::parse(&bad_gdo2, ObjectFormat::Sha1).is_err());
4640
4641 let mut unused_gdo2 = commit_graph_chunks(std::slice::from_ref(&commit));
4642 unused_gdo2.push((*b"GDA2", commit_graph_gda2(&[1])));
4643 unused_gdo2.push((*b"GDO2", commit_graph_gdo2(&[2])));
4644 let unused_gdo2 = commit_graph(ObjectFormat::Sha1, 0, &unused_gdo2);
4645 assert!(CommitGraph::parse(&unused_gdo2, ObjectFormat::Sha1).is_err());
4646
4647 let mut orphan_gdo2 = commit_graph_chunks(&[commit]);
4648 orphan_gdo2.push((*b"GDO2", commit_graph_gdo2(&[2])));
4649 let orphan_gdo2 = commit_graph(ObjectFormat::Sha1, 0, &orphan_gdo2);
4650 assert!(CommitGraph::parse(&orphan_gdo2, ObjectFormat::Sha1).is_err());
4651 }
4652
4653 #[test]
4654 fn rejects_bad_commit_graph_bloom_filters() {
4655 let commit = (
4656 oid("1111111111111111111111111111111111111111"),
4657 oid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
4658 Vec::new(),
4659 1,
4660 1,
4661 );
4662
4663 let mut bidx_without_bdat = commit_graph_chunks(std::slice::from_ref(&commit));
4664 bidx_without_bdat.push((*b"BIDX", commit_graph_bidx(&[1])));
4665 let bidx_without_bdat = commit_graph(ObjectFormat::Sha1, 0, &bidx_without_bdat);
4666 assert_eq!(
4667 CommitGraph::parse(&bidx_without_bdat, ObjectFormat::Sha1)
4668 .expect("test operation should succeed")
4669 .bloom_filters,
4670 None
4671 );
4672
4673 let mut bdat_without_bidx = commit_graph_chunks(std::slice::from_ref(&commit));
4674 bdat_without_bidx.push((*b"BDAT", commit_graph_bdat(2, 7, 10, &[0xaa])));
4675 let bdat_without_bidx = commit_graph(ObjectFormat::Sha1, 0, &bdat_without_bidx);
4676 assert!(CommitGraph::parse(&bdat_without_bidx, ObjectFormat::Sha1).is_err());
4677
4678 let mut short_bidx = commit_graph_chunks(std::slice::from_ref(&commit));
4679 short_bidx.push((*b"BIDX", vec![0]));
4680 short_bidx.push((*b"BDAT", commit_graph_bdat(2, 7, 10, &[0xaa])));
4681 let short_bidx = commit_graph(ObjectFormat::Sha1, 0, &short_bidx);
4682 assert!(CommitGraph::parse(&short_bidx, ObjectFormat::Sha1).is_err());
4683
4684 let mut short_bdat = commit_graph_chunks(std::slice::from_ref(&commit));
4685 short_bdat.push((*b"BIDX", commit_graph_bidx(&[0])));
4686 short_bdat.push((*b"BDAT", vec![0]));
4687 let short_bdat = commit_graph(ObjectFormat::Sha1, 0, &short_bdat);
4688 assert!(CommitGraph::parse(&short_bdat, ObjectFormat::Sha1).is_err());
4689
4690 let mut bidx_past_payload = commit_graph_chunks(std::slice::from_ref(&commit));
4691 bidx_past_payload.push((*b"BIDX", commit_graph_bidx(&[2])));
4692 bidx_past_payload.push((*b"BDAT", commit_graph_bdat(2, 7, 10, &[0xaa])));
4693 let bidx_past_payload = commit_graph(ObjectFormat::Sha1, 0, &bidx_past_payload);
4694 assert!(CommitGraph::parse(&bidx_past_payload, ObjectFormat::Sha1).is_err());
4695
4696 let mut trailing_payload = commit_graph_chunks(&[commit]);
4697 trailing_payload.push((*b"BIDX", commit_graph_bidx(&[1])));
4698 trailing_payload.push((*b"BDAT", commit_graph_bdat(2, 7, 10, &[0xaa, 0xbb])));
4699 let trailing_payload = commit_graph(ObjectFormat::Sha1, 0, &trailing_payload);
4700 assert!(CommitGraph::parse(&trailing_payload, ObjectFormat::Sha1).is_err());
4701 }
4702
4703 #[test]
4704 fn parses_bundle_v2_header_and_pack() {
4705 let prerequisite = oid("1111111111111111111111111111111111111111");
4706 let reference = oid("2222222222222222222222222222222222222222");
4707 let bytes = format!(
4708 "# v2 git bundle\n-{prerequisite} prerequisite comment\n{reference} refs/heads/main\n\n"
4709 )
4710 .into_bytes()
4711 .into_iter()
4712 .chain(b"PACKdata".iter().copied())
4713 .collect::<Vec<_>>();
4714
4715 let parsed =
4716 Bundle::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed");
4717 assert_eq!(parsed.version, 2);
4718 assert_eq!(parsed.format, ObjectFormat::Sha1);
4719 assert!(parsed.capabilities.is_empty());
4720 assert_eq!(
4721 parsed.prerequisites,
4722 vec![BundlePrerequisite {
4723 oid: prerequisite,
4724 comment: b"prerequisite comment".to_vec(),
4725 }]
4726 );
4727 assert_eq!(
4728 parsed.references,
4729 vec![BundleReference {
4730 oid: reference,
4731 name: "refs/heads/main".into(),
4732 }]
4733 );
4734 assert_eq!(parsed.pack, b"PACKdata");
4735 }
4736
4737 #[test]
4738 fn parses_bundle_v3_capabilities_and_sha256_ids() {
4739 let prerequisite = ObjectId::from_hex(
4740 ObjectFormat::Sha256,
4741 "1111111111111111111111111111111111111111111111111111111111111111",
4742 )
4743 .expect("test operation should succeed");
4744 let reference = ObjectId::from_hex(
4745 ObjectFormat::Sha256,
4746 "2222222222222222222222222222222222222222222222222222222222222222",
4747 )
4748 .expect("test operation should succeed");
4749 let bytes = format!(
4750 "# v3 git bundle\n@object-format=sha256\n@filter=blob:none\n-{prerequisite} base\n{reference} refs/heads/main\n\n"
4751 )
4752 .into_bytes();
4753
4754 let parsed =
4755 Bundle::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed");
4756 assert_eq!(parsed.version, 3);
4757 assert_eq!(parsed.format, ObjectFormat::Sha256);
4758 assert_eq!(
4759 parsed.capabilities,
4760 vec![
4761 BundleCapability {
4762 key: "object-format".into(),
4763 value: Some(b"sha256".to_vec()),
4764 },
4765 BundleCapability {
4766 key: "filter".into(),
4767 value: Some(b"blob:none".to_vec()),
4768 },
4769 ]
4770 );
4771 assert_eq!(parsed.prerequisites[0].oid, prerequisite);
4772 assert_eq!(parsed.references[0].oid, reference);
4773 }
4774
4775 #[test]
4776 fn standalone_bundle_parse_uses_sha1_default_and_header_object_format_override() {
4777 let sha1 = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"tip\n")
4778 .expect("test operation should succeed");
4779 let sha256 = sley_core::object_id_for_bytes(ObjectFormat::Sha256, "blob", b"tip\n")
4780 .expect("test operation should succeed");
4781
4782 let sha1_bytes = format!("# v2 git bundle\n{sha1} refs/heads/main\n\nPACK").into_bytes();
4783 let sha1_bundle =
4784 Bundle::parse_standalone(&sha1_bytes).expect("test operation should succeed");
4785 assert_eq!(sha1_bundle.format, ObjectFormat::Sha1);
4786 assert_eq!(sha1_bundle.references[0].oid, sha1);
4787
4788 let sha256_bytes =
4789 format!("# v3 git bundle\n@object-format=sha256\n{sha256} refs/heads/main\n\nPACK")
4790 .into_bytes();
4791 let sha256_bundle =
4792 Bundle::parse_standalone(&sha256_bytes).expect("test operation should succeed");
4793 assert_eq!(sha256_bundle.format, ObjectFormat::Sha256);
4794 assert_eq!(sha256_bundle.references[0].oid, sha256);
4795 }
4796
4797 #[test]
4798 fn writes_bundle_v2_header_and_pack() {
4799 let prerequisite = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"base\n")
4800 .expect("test operation should succeed");
4801 let reference = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"tip\n")
4802 .expect("test operation should succeed");
4803 let bundle = Bundle {
4804 version: 2,
4805 format: ObjectFormat::Sha1,
4806 capabilities: Vec::new(),
4807 prerequisites: vec![BundlePrerequisite {
4808 oid: prerequisite.clone(),
4809 comment: b"base comment".to_vec(),
4810 }],
4811 references: vec![BundleReference {
4812 oid: reference.clone(),
4813 name: "refs/heads/main".into(),
4814 }],
4815 pack: b"PACKv2".to_vec(),
4816 };
4817
4818 let bytes = bundle.write().expect("test operation should succeed");
4819 let expected = format!(
4820 "# v2 git bundle\n-{prerequisite} base comment\n{reference} refs/heads/main\n\n"
4821 )
4822 .into_bytes()
4823 .into_iter()
4824 .chain(b"PACKv2".iter().copied())
4825 .collect::<Vec<_>>();
4826 assert_eq!(bytes, expected);
4827 assert_eq!(
4828 Bundle::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed"),
4829 bundle
4830 );
4831 }
4832
4833 #[test]
4834 fn writes_bundle_v3_sha256_object_format_capability() {
4835 let oid = sley_core::object_id_for_bytes(ObjectFormat::Sha256, "blob", b"tip\n")
4836 .expect("test operation should succeed");
4837 let bundle = Bundle {
4838 version: 3,
4839 format: ObjectFormat::Sha256,
4840 capabilities: vec![BundleCapability {
4841 key: "filter".into(),
4842 value: Some(b"blob:none".to_vec()),
4843 }],
4844 prerequisites: Vec::new(),
4845 references: vec![BundleReference {
4846 oid,
4847 name: "refs/heads/main".into(),
4848 }],
4849 pack: b"PACKv3".to_vec(),
4850 };
4851
4852 let bytes = bundle.write().expect("test operation should succeed");
4853 let text = String::from_utf8(bytes.clone()).expect("test operation should succeed");
4854 assert!(text.starts_with("# v3 git bundle\n@filter=blob:none\n@object-format=sha256\n"));
4855 let parsed =
4856 Bundle::parse(&bytes, ObjectFormat::Sha1).expect("test operation should succeed");
4857 assert_eq!(parsed.format, ObjectFormat::Sha256);
4858 assert_eq!(parsed.references[0].oid, oid);
4859 assert_eq!(parsed.pack, b"PACKv3");
4860 }
4861
4862 #[test]
4863 fn rejects_bad_bundle_write_inputs() {
4864 let sha1 = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"tip\n")
4865 .expect("test operation should succeed");
4866 let sha256 = sley_core::object_id_for_bytes(ObjectFormat::Sha256, "blob", b"tip\n")
4867 .expect("test operation should succeed");
4868 let mut bundle = Bundle {
4869 version: 2,
4870 format: ObjectFormat::Sha1,
4871 capabilities: vec![BundleCapability {
4872 key: "filter".into(),
4873 value: Some(b"blob:none".to_vec()),
4874 }],
4875 prerequisites: Vec::new(),
4876 references: Vec::new(),
4877 pack: Vec::new(),
4878 };
4879 assert!(bundle.write().is_err());
4880
4881 bundle.version = 3;
4882 bundle.capabilities = vec![BundleCapability {
4883 key: "bad_key".into(),
4884 value: None,
4885 }];
4886 assert!(bundle.write().is_err());
4887
4888 bundle.capabilities = Vec::new();
4889 bundle.references = vec![BundleReference {
4890 oid: sha256,
4891 name: "refs/heads/main".into(),
4892 }];
4893 assert!(bundle.write().is_err());
4894
4895 bundle.references = vec![BundleReference {
4896 oid: sha1,
4897 name: "bad\nname".into(),
4898 }];
4899 assert!(bundle.write().is_err());
4900 }
4901
4902 #[test]
4903 fn rejects_bad_bundle_headers() {
4904 assert!(Bundle::parse(b"# v4 git bundle\n\n", ObjectFormat::Sha1).is_err());
4905 assert!(
4906 Bundle::parse(
4907 b"# v2 git bundle\n@filter=blob:none\n\n",
4908 ObjectFormat::Sha1
4909 )
4910 .is_err()
4911 );
4912 assert!(Bundle::parse(b"# v3 git bundle\n@bad_key=value\n\n", ObjectFormat::Sha1).is_err());
4913 assert!(
4914 Bundle::parse(
4915 b"# v3 git bundle\n1111111111111111111111111111111111111111 refs/heads/main\n@filter=blob:none\n\n",
4916 ObjectFormat::Sha1,
4917 )
4918 .is_err()
4919 );
4920 assert!(
4921 Bundle::parse(
4922 b"# v3 git bundle\n@object-format=unknown\n\n",
4923 ObjectFormat::Sha1,
4924 )
4925 .is_err()
4926 );
4927 assert!(
4928 Bundle::parse(
4929 b"# v2 git bundle\n1111111111111111111111111111111111111111 refs/heads/main",
4930 ObjectFormat::Sha1,
4931 )
4932 .is_err()
4933 );
4934 assert!(
4935 Bundle::parse(
4936 b"# v2 git bundle\n1111111111111111111111111111111111111111 \n\n",
4937 ObjectFormat::Sha1,
4938 )
4939 .is_err()
4940 );
4941 }
4942
4943 #[test]
4944 fn repository_bootstrap_copies_template_files() {
4945 let root = unique_temp_dir("init-template");
4946 let template = root.join("template");
4947 let repo = root.join("repo");
4948 fs::create_dir_all(template.join("hooks")).expect("create template hooks");
4949 fs::write(template.join("description"), b"template repo\n").expect("write description");
4950 fs::write(template.join("hooks/pre-commit"), b"#!/bin/sh\n").expect("write hook");
4951 fs::write(template.join("config"), b"[user]\n\tname = Template User\n")
4952 .expect("write template config");
4953
4954 let layout = RepositoryBootstrap::init(InitOptions {
4955 git_dir_override: None,
4956 core_worktree: None,
4957 object_dir: None,
4958 worktree: repo.clone(),
4959 object_format: ObjectFormat::Sha1,
4960 object_format_explicit: false,
4961 bare: false,
4962 initial_branch: "main".into(),
4963 template_dir: Some(template),
4964 copy_template_config: true,
4965 separate_git_dir: None,
4966 shared_repository: None,
4967 ref_storage: RefStorageFormat::Files,
4968 ref_storage_explicit: false,
4969 })
4970 .expect("init should succeed");
4971
4972 assert_eq!(
4973 fs::read_to_string(layout.git_dir.join("description")).expect("read description"),
4974 "template repo\n"
4975 );
4976 assert!(layout.git_dir.join("hooks/pre-commit").is_file());
4977 let config = GitConfig::read(layout.git_dir.join("config")).expect("read config");
4978 assert_eq!(config.get("user", None, "name"), Some("Template User"));
4979 let _ = fs::remove_dir_all(&root);
4980 }
4981
4982 #[test]
4983 fn repository_bootstrap_separate_git_dir_writes_gitfile() {
4984 let root = unique_temp_dir("init-separate-gitdir");
4985 let worktree = root.join("worktree");
4986 let gitdir = root.join("external.git");
4987 let layout = RepositoryBootstrap::init(InitOptions {
4988 git_dir_override: None,
4989 core_worktree: None,
4990 object_dir: None,
4991 worktree: worktree.clone(),
4992 object_format: ObjectFormat::Sha1,
4993 object_format_explicit: false,
4994 bare: false,
4995 initial_branch: "main".into(),
4996 template_dir: None,
4997 copy_template_config: false,
4998 separate_git_dir: Some(gitdir.clone()),
4999 shared_repository: None,
5000 ref_storage: RefStorageFormat::Files,
5001 ref_storage_explicit: false,
5002 })
5003 .expect("init should succeed");
5004
5005 assert_eq!(layout.git_dir, gitdir);
5006 assert!(gitdir.join("HEAD").is_file());
5007 let gitfile = fs::read_to_string(worktree.join(".git")).expect("read gitfile");
5008 assert!(gitfile.starts_with("gitdir: "));
5009 let _ = fs::remove_dir_all(&root);
5010 }
5011
5012 #[test]
5013 fn repository_bootstrap_initializes_an_external_object_directory() {
5014 let root = unique_temp_dir("init-external-object-dir");
5015 let repo = root.join("repo");
5016 let object_dir = root.join("objects");
5017 let layout = RepositoryBootstrap::init(InitOptions {
5018 git_dir_override: None,
5019 core_worktree: None,
5020 object_dir: Some(object_dir.clone()),
5021 worktree: repo,
5022 object_format: ObjectFormat::Sha1,
5023 object_format_explicit: false,
5024 bare: false,
5025 initial_branch: "main".into(),
5026 template_dir: None,
5027 copy_template_config: false,
5028 separate_git_dir: None,
5029 shared_repository: None,
5030 ref_storage: RefStorageFormat::Files,
5031 ref_storage_explicit: false,
5032 })
5033 .expect("init should succeed");
5034
5035 assert!(object_dir.join("info").is_dir());
5036 assert!(object_dir.join("pack").is_dir());
5037 assert!(!layout.git_dir.join("objects/pack").exists());
5038 let _ = fs::remove_dir_all(&root);
5039 }
5040
5041 #[test]
5042 fn repository_bootstrap_reftable_writes_extension_and_table() {
5043 let root = unique_temp_dir("init-reftable");
5044 let repo = root.join("repo");
5045 let layout = RepositoryBootstrap::init(InitOptions {
5046 git_dir_override: None,
5047 core_worktree: None,
5048 object_dir: None,
5049 worktree: repo,
5050 object_format: ObjectFormat::Sha1,
5051 object_format_explicit: false,
5052 bare: false,
5053 initial_branch: "main".into(),
5054 template_dir: None,
5055 copy_template_config: false,
5056 separate_git_dir: None,
5057 shared_repository: None,
5058 ref_storage: RefStorageFormat::Reftable,
5059 ref_storage_explicit: false,
5060 })
5061 .expect("init should succeed");
5062
5063 let config = GitConfig::read(layout.git_dir.join("config")).expect("read config");
5064 assert_eq!(
5065 config.get("extensions", None, "refStorage"),
5066 Some("reftable")
5067 );
5068 assert!(layout.git_dir.join("reftable/tables.list").is_file());
5069 assert_eq!(
5070 fs::read(layout.git_dir.join("refs/heads")).expect("read reftable sentinel"),
5071 b"this repository uses the reftable format\n"
5072 );
5073 let _ = fs::remove_dir_all(&root);
5074 }
5075
5076 #[test]
5077 fn init_config_preserves_an_existing_log_all_ref_updates_choice() {
5078 let inherited = build_init_config(
5079 ObjectFormat::Sha1,
5080 false,
5081 &None,
5082 RefStorageFormat::Reftable,
5083 None,
5084 None,
5085 true,
5086 );
5087 assert_eq!(inherited.get("core", None, "logAllRefUpdates"), None);
5088
5089 let defaulted = build_init_config(
5090 ObjectFormat::Sha1,
5091 false,
5092 &None,
5093 RefStorageFormat::Reftable,
5094 None,
5095 None,
5096 false,
5097 );
5098 assert_eq!(
5099 defaulted.get("core", None, "logAllRefUpdates"),
5100 Some("true")
5101 );
5102 }
5103
5104 #[test]
5105 fn repository_bootstrap_honors_shared_repository_config() {
5106 let root = unique_temp_dir("init-shared");
5107 let repo = root.join("repo");
5108 let layout = RepositoryBootstrap::init(InitOptions {
5109 git_dir_override: None,
5110 core_worktree: None,
5111 object_dir: None,
5112 worktree: repo,
5113 object_format: ObjectFormat::Sha1,
5114 object_format_explicit: false,
5115 bare: false,
5116 initial_branch: "main".into(),
5117 template_dir: None,
5118 copy_template_config: false,
5119 separate_git_dir: None,
5120 shared_repository: Some("group".into()),
5121 ref_storage: RefStorageFormat::Files,
5122 ref_storage_explicit: false,
5123 })
5124 .expect("init should succeed");
5125
5126 let config = GitConfig::read(layout.git_dir.join("config")).expect("read config");
5127 assert_eq!(config.get("core", None, "sharedRepository"), Some("1"));
5128 let _ = fs::remove_dir_all(&root);
5129 }
5130
5131 #[cfg(unix)]
5132 #[test]
5133 fn exact_shared_repository_adjusts_intermediate_layout_directories() {
5134 use std::os::unix::fs::PermissionsExt;
5135
5136 let root = unique_temp_dir("init-shared-exact-dirs");
5137 let repo = root.join("repo");
5138 let layout = RepositoryBootstrap::init(InitOptions {
5139 git_dir_override: None,
5140 core_worktree: None,
5141 object_dir: None,
5142 worktree: repo,
5143 object_format: ObjectFormat::Sha1,
5144 object_format_explicit: false,
5145 bare: false,
5146 initial_branch: "main".into(),
5147 template_dir: None,
5148 copy_template_config: false,
5149 separate_git_dir: None,
5150 shared_repository: Some("0660".into()),
5151 ref_storage: RefStorageFormat::Files,
5152 ref_storage_explicit: false,
5153 })
5154 .expect("init shared repository");
5155
5156 for relative in [
5157 "",
5158 "objects",
5159 "objects/info",
5160 "objects/pack",
5161 "refs",
5162 "refs/heads",
5163 ] {
5164 let path = layout.git_dir.join(relative);
5165 let mode = fs::metadata(&path)
5166 .expect("shared directory")
5167 .permissions()
5168 .mode();
5169 assert_eq!(
5170 mode & 0o7777,
5171 0o2770,
5172 "unexpected mode for {}",
5173 path.display()
5174 );
5175 }
5176 let _ = fs::remove_dir_all(&root);
5177 }
5178
5179 #[test]
5180 fn shared_repository_values_are_canonical_and_owner_safe() {
5181 assert_eq!(
5182 canonical_shared_repository_value("group").expect("group is valid"),
5183 Some("1".into())
5184 );
5185 assert_eq!(
5186 canonical_shared_repository_value("all").expect("all is valid"),
5187 Some("2".into())
5188 );
5189 assert_eq!(
5190 canonical_shared_repository_value("0660").expect("owner rw mode is valid"),
5191 Some("0660".into())
5192 );
5193 assert_eq!(
5194 canonical_shared_repository_value("umask").expect("umask is valid"),
5195 None
5196 );
5197 assert!(canonical_shared_repository_value("0400").is_err());
5198 assert!(canonical_shared_repository_value("mystery").is_err());
5199 }
5200
5201 #[cfg(unix)]
5202 #[test]
5203 fn shared_repository_file_adjustment_preserves_read_only_intent() {
5204 use std::os::unix::fs::PermissionsExt;
5205
5206 let root = unique_temp_dir("shared-file-mode");
5207 fs::create_dir_all(&root).expect("create temp root");
5208 let path = root.join("refs");
5209 fs::write(&path, b"refs\n").expect("write refs");
5210
5211 fs::set_permissions(&path, fs::Permissions::from_mode(0o400)).expect("set read-only mode");
5212 adjust_shared_repository_file(&path, Some("0660")).expect("adjust read-only mode");
5213 assert_eq!(
5214 fs::metadata(&path).expect("metadata").permissions().mode() & 0o777,
5215 0o440
5216 );
5217
5218 fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).expect("set writable mode");
5219 adjust_shared_repository_file(&path, Some("0660")).expect("adjust writable mode");
5220 assert_eq!(
5221 fs::metadata(&path).expect("metadata").permissions().mode() & 0o777,
5222 0o660
5223 );
5224 let _ = fs::remove_dir_all(root);
5225 }
5226
5227 fn oid(hex: &str) -> ObjectId {
5228 ObjectId::from_hex(ObjectFormat::Sha1, hex).expect("test operation should succeed")
5229 }
5230
5231 fn unique_temp_dir(name: &str) -> PathBuf {
5232 let nanos = SystemTime::now()
5233 .duration_since(UNIX_EPOCH)
5234 .expect("system time before unix epoch")
5235 .as_nanos();
5236 std::env::temp_dir().join(format!("sley-{name}-{}-{nanos}", std::process::id()))
5237 }
5238
5239 fn run_success(program: &str, cwd: &Path, args: &[&str]) -> Vec<u8> {
5240 let output = Command::new(program)
5241 .current_dir(cwd)
5242 .args(args)
5243 .output()
5244 .unwrap_or_else(|err| panic!("failed to run {program} {args:?}: {err}"));
5245 assert!(
5246 output.status.success(),
5247 "{program} {args:?} failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
5248 output.status.code(),
5249 String::from_utf8_lossy(&output.stdout),
5250 String::from_utf8_lossy(&output.stderr)
5251 );
5252 output.stdout
5253 }
5254
5255 fn run_success_with_stdin(program: &str, cwd: &Path, args: &[&str], stdin: &[u8]) -> Vec<u8> {
5256 let mut child = Command::new(program)
5257 .current_dir(cwd)
5258 .args(args)
5259 .stdin(std::process::Stdio::piped())
5260 .stdout(std::process::Stdio::piped())
5261 .stderr(std::process::Stdio::piped())
5262 .spawn()
5263 .unwrap_or_else(|err| panic!("failed to spawn {program} {args:?}: {err}"));
5264 child
5265 .stdin
5266 .as_mut()
5267 .expect("stdin is piped")
5268 .write_all(stdin)
5269 .expect("write stdin");
5270 let output = child
5271 .wait_with_output()
5272 .unwrap_or_else(|err| panic!("failed to wait for {program} {args:?}: {err}"));
5273 assert!(
5274 output.status.success(),
5275 "{program} {args:?} failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
5276 output.status.code(),
5277 String::from_utf8_lossy(&output.stdout),
5278 String::from_utf8_lossy(&output.stderr)
5279 );
5280 output.stdout
5281 }
5282
5283 fn commit_graph(
5284 format: ObjectFormat,
5285 base_graph_count: u8,
5286 chunks: &[([u8; 4], Vec<u8>)],
5287 ) -> Vec<u8> {
5288 let lookup_len = (chunks.len() + 1) * 12;
5289 let mut out = Vec::new();
5290 out.extend_from_slice(b"CGPH");
5291 out.push(1);
5292 out.push(hash_function_id(format) as u8);
5293 out.push(chunks.len() as u8);
5294 out.push(base_graph_count);
5295 let mut chunk_offset = (8 + lookup_len) as u64;
5296 for (id, data) in chunks {
5297 out.extend_from_slice(id);
5298 out.extend_from_slice(&chunk_offset.to_be_bytes());
5299 chunk_offset += data.len() as u64;
5300 }
5301 out.extend_from_slice(&[0, 0, 0, 0]);
5302 out.extend_from_slice(&chunk_offset.to_be_bytes());
5303 for (_id, data) in chunks {
5304 out.extend_from_slice(data);
5305 }
5306 let checksum =
5307 sley_core::digest_bytes(format, &out).expect("test operation should succeed");
5308 out.extend_from_slice(checksum.as_bytes());
5309 out
5310 }
5311
5312 fn commit_graph_chunks(
5313 entries: &[(ObjectId, ObjectId, Vec<u32>, u32, u64)],
5314 ) -> Vec<([u8; 4], Vec<u8>)> {
5315 let mut entries = entries.to_vec();
5316 entries.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes()));
5317 let object_ids: Vec<ObjectId> = entries.iter().map(|entry| entry.0).collect();
5318 let (cdat, edge) = commit_graph_cdat(&entries);
5319 let mut chunks = vec![
5320 (*b"OIDF", commit_graph_fanout(&object_ids)),
5321 (*b"OIDL", commit_graph_oid_lookup(&object_ids)),
5322 (*b"CDAT", cdat),
5323 ];
5324 if !edge.is_empty() {
5325 chunks.push((*b"EDGE", edge));
5326 }
5327 chunks
5328 }
5329
5330 fn commit_graph_fanout(object_ids: &[ObjectId]) -> Vec<u8> {
5331 let mut counts = [0u32; 256];
5332 for oid in object_ids {
5333 counts[oid.as_bytes()[0] as usize] += 1;
5334 }
5335 let mut running = 0u32;
5336 let mut out = Vec::new();
5337 for count in counts {
5338 running += count;
5339 out.extend_from_slice(&running.to_be_bytes());
5340 }
5341 out
5342 }
5343
5344 fn commit_graph_oid_lookup(object_ids: &[ObjectId]) -> Vec<u8> {
5345 let mut out = Vec::new();
5346 for oid in object_ids {
5347 out.extend_from_slice(oid.as_bytes());
5348 }
5349 out
5350 }
5351
5352 fn commit_graph_cdat(
5353 entries: &[(ObjectId, ObjectId, Vec<u32>, u32, u64)],
5354 ) -> (Vec<u8>, Vec<u8>) {
5355 let mut cdat = Vec::new();
5356 let mut edge = Vec::new();
5357 for (_oid, tree, parents, generation, commit_time) in entries {
5358 cdat.extend_from_slice(tree.as_bytes());
5359 let first_parent = parents.first().copied().unwrap_or(COMMIT_GRAPH_PARENT_NONE);
5360 let second_parent = match parents.len() {
5361 0 | 1 => COMMIT_GRAPH_PARENT_NONE,
5362 2 => parents[1],
5363 _ => {
5364 let edge_start = (edge.len() / 4) as u32;
5365 for (idx, parent) in parents[1..].iter().enumerate() {
5366 let mut value = *parent;
5367 if idx == parents.len() - 2 {
5368 value |= COMMIT_GRAPH_EXTRA_EDGE;
5369 }
5370 edge.extend_from_slice(&value.to_be_bytes());
5371 }
5372 COMMIT_GRAPH_EXTRA_EDGE | edge_start
5373 }
5374 };
5375 cdat.extend_from_slice(&first_parent.to_be_bytes());
5376 cdat.extend_from_slice(&second_parent.to_be_bytes());
5377 let generation_and_time_high =
5378 (*generation << 2) | (((*commit_time >> 32) as u32) & 0x3);
5379 cdat.extend_from_slice(&generation_and_time_high.to_be_bytes());
5380 cdat.extend_from_slice(&(*commit_time as u32).to_be_bytes());
5381 }
5382 (cdat, edge)
5383 }
5384
5385 fn commit_graph_gda2(values: &[u32]) -> Vec<u8> {
5386 let mut out = Vec::new();
5387 for value in values {
5388 out.extend_from_slice(&value.to_be_bytes());
5389 }
5390 out
5391 }
5392
5393 fn commit_graph_gdo2(values: &[u64]) -> Vec<u8> {
5394 let mut out = Vec::new();
5395 for value in values {
5396 out.extend_from_slice(&value.to_be_bytes());
5397 }
5398 out
5399 }
5400
5401 fn commit_graph_bidx(values: &[u32]) -> Vec<u8> {
5402 let mut out = Vec::new();
5403 for value in values {
5404 out.extend_from_slice(&value.to_be_bytes());
5405 }
5406 out
5407 }
5408
5409 fn commit_graph_bdat(
5410 hash_version: u32,
5411 hash_count: u32,
5412 bits_per_entry: u32,
5413 payload: &[u8],
5414 ) -> Vec<u8> {
5415 let mut out = Vec::new();
5416 out.extend_from_slice(&hash_version.to_be_bytes());
5417 out.extend_from_slice(&hash_count.to_be_bytes());
5418 out.extend_from_slice(&bits_per_entry.to_be_bytes());
5419 out.extend_from_slice(payload);
5420 out
5421 }
5422}