1use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6
7use sha1::{Digest, Sha1};
8use sha2::Sha256;
9
10use crate::config::ConfigSet;
11use crate::error::{Error, Result};
12use crate::ewah_bitmap::EwahBitmap;
13use crate::git_date::approx::approxidate_careful;
14use crate::index::{Index, IndexEntry};
15use crate::objects::{HashAlgo, ObjectId};
16
17#[derive(Debug, Clone)]
19pub(crate) struct SplitIndexLink {
20 pub base_oid: ObjectId,
22 pub delete_bitmap: Option<EwahBitmap>,
23 pub replace_bitmap: Option<EwahBitmap>,
24}
25
26fn parse_shared_repository_perm(raw: Option<&str>) -> i32 {
27 const PERM_UMASK: i32 = 0;
28 const OLD_PERM_GROUP: i32 = 1;
29 const OLD_PERM_EVERYBODY: i32 = 2;
30 const PERM_GROUP: i32 = 0o660;
31 const PERM_EVERYBODY: i32 = 0o664;
32
33 let Some(value) = raw.map(str::trim).filter(|s| !s.is_empty()) else {
34 return PERM_UMASK;
35 };
36 if value.eq_ignore_ascii_case("umask") {
37 return PERM_UMASK;
38 }
39 if value.eq_ignore_ascii_case("group") {
40 return PERM_GROUP;
41 }
42 if value.eq_ignore_ascii_case("all")
43 || value.eq_ignore_ascii_case("world")
44 || value.eq_ignore_ascii_case("everybody")
45 {
46 return PERM_EVERYBODY;
47 }
48 if !value.is_empty() && value.chars().all(|c| ('0'..='7').contains(&c)) {
49 if let Ok(i) = i32::from_str_radix(value, 8) {
50 return match i {
51 PERM_UMASK => PERM_UMASK,
52 OLD_PERM_GROUP => PERM_GROUP,
53 OLD_PERM_EVERYBODY => PERM_EVERYBODY,
54 _ => {
55 if (i & 0o600) != 0o600 {
56 return PERM_UMASK;
57 }
58 -(i & 0o666)
59 }
60 };
61 }
62 }
63 if value.eq_ignore_ascii_case("true") {
64 PERM_GROUP
65 } else if value.eq_ignore_ascii_case("false") {
66 PERM_UMASK
67 } else {
68 PERM_UMASK
69 }
70}
71
72#[cfg(unix)]
73fn calc_shared_perm(shared_repo: i32, mode: u32) -> u32 {
74 let tweak = if shared_repo < 0 {
75 (-shared_repo) as u32
76 } else {
77 shared_repo as u32
78 };
79
80 let mut new_mode = if shared_repo < 0 {
81 (mode & !0o777) | tweak
82 } else {
83 mode | tweak
84 };
85
86 if mode & 0o200 == 0 {
87 new_mode &= !0o222;
88 }
89 if mode & 0o100 != 0 {
90 new_mode |= (new_mode & 0o444) >> 2;
91 }
92
93 new_mode
94}
95
96#[cfg(unix)]
97fn adjust_shared_perm_file(path: &Path, shared_repo: i32) -> io::Result<()> {
98 if shared_repo == 0 {
99 return Ok(());
100 }
101 use std::os::unix::fs::PermissionsExt;
102 let meta = fs::metadata(path)?;
103 let old = meta.permissions().mode();
104 let new_mode = calc_shared_perm(shared_repo, old);
105 if (old ^ new_mode) & 0o777 != 0 {
106 let mut p = meta.permissions();
107 p.set_mode(new_mode & 0o777);
108 fs::set_permissions(path, p)?;
109 }
110 Ok(())
111}
112
113#[cfg(not(unix))]
114fn adjust_shared_perm_file(_path: &Path, _shared_repo: i32) -> io::Result<()> {
115 Ok(())
116}
117
118pub(crate) fn entries_equal_for_split(a: &IndexEntry, b: &IndexEntry) -> bool {
120 let mask: u16 = 0xF000 | 0x8000;
121 let a_flags = a.flags & mask;
122 let b_flags = b.flags & mask;
123 let ext_mask: u16 = 0x7000;
124 let a_ext = a.flags_extended.unwrap_or(0) & ext_mask;
125 let b_ext = b.flags_extended.unwrap_or(0) & ext_mask;
126 a.ctime_sec == b.ctime_sec
127 && a.ctime_nsec == b.ctime_nsec
128 && a.mtime_sec == b.mtime_sec
129 && a.mtime_nsec == b.mtime_nsec
130 && a.dev == b.dev
131 && a.ino == b.ino
132 && a.mode == b.mode
133 && a.uid == b.uid
134 && a.gid == b.gid
135 && a.size == b.size
136 && a.oid == b.oid
137 && a_flags == b_flags
138 && a_ext == b_ext
139}
140
141fn replace_positions_in_order(link: &SplitIndexLink) -> Vec<usize> {
142 let Some(bm) = &link.replace_bitmap else {
143 return Vec::new();
144 };
145 if bm.bit_size == 0 {
146 return Vec::new();
147 }
148 let mut v = Vec::new();
149 bm.each_set_bit(|p| v.push(p));
150 v
151}
152
153fn bitmap_has_bit(bm: &EwahBitmap, i: usize) -> bool {
154 let mut found = false;
155 bm.each_set_bit(|pos| {
156 if pos == i {
157 found = true;
158 }
159 });
160 found
161}
162
163pub(crate) fn merge_split_into_index(
165 index: &mut Index,
166 link: SplitIndexLink,
167 base_entries: Vec<IndexEntry>,
168) -> Result<()> {
169 let saved = std::mem::take(&mut index.entries);
170 let replace_pos = replace_positions_in_order(&link);
171 let stubs: Vec<IndexEntry> = saved
172 .iter()
173 .filter(|e| e.path.is_empty())
174 .cloned()
175 .collect();
176 if stubs.len() != replace_pos.len() {
177 return Err(Error::IndexError(format!(
178 "split index: expected {} replacement stubs, found {}",
179 replace_pos.len(),
180 stubs.len()
181 )));
182 }
183 let mut stub_iter = stubs.into_iter();
184 let rest: Vec<IndexEntry> = saved.into_iter().filter(|e| !e.path.is_empty()).collect();
185
186 let delete = &link.delete_bitmap;
187 let replace = &link.replace_bitmap;
188
189 let mut merged: Vec<IndexEntry> = Vec::new();
190
191 for (i, mut base_e) in base_entries.into_iter().enumerate() {
192 if delete
193 .as_ref()
194 .is_some_and(|b| b.bit_size > 0 && bitmap_has_bit(b, i))
195 {
196 continue;
197 }
198 if replace
199 .as_ref()
200 .is_some_and(|b| b.bit_size > 0 && bitmap_has_bit(b, i))
201 {
202 let Some(rep) = stub_iter.next() else {
203 return Err(Error::IndexError(
204 "split index: missing replacement entry".to_owned(),
205 ));
206 };
207 let mut e = rep;
208 e.path = base_e.path.clone();
209 e.base_index_pos = (i + 1) as u32;
210 merged.push(e);
211 } else {
212 base_e.base_index_pos = (i + 1) as u32;
213 merged.push(base_e);
214 }
215 }
216
217 if stub_iter.next().is_some() {
218 return Err(Error::IndexError(
219 "split index: too many replacement stubs".to_owned(),
220 ));
221 }
222
223 for mut e in rest {
224 e.base_index_pos = 0;
225 merged.push(e);
226 }
227
228 merged.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
229 index.entries = merged;
230 Ok(())
231}
232
233pub(crate) fn parse_link_extension(data: &[u8], algo: HashAlgo) -> Result<SplitIndexLink> {
235 let hash_len = algo.len();
236 if data.len() < hash_len {
237 return Err(Error::IndexError(
238 "corrupt link extension (too short)".to_owned(),
239 ));
240 }
241 let base_oid = ObjectId::from_bytes(&data[..hash_len])?;
242 let mut rest = &data[hash_len..];
243 if rest.is_empty() {
244 return Ok(SplitIndexLink {
245 base_oid,
246 delete_bitmap: None,
247 replace_bitmap: None,
248 });
249 }
250 let Some((del, consumed)) = EwahBitmap::deserialize_prefix(rest) else {
251 return Err(Error::IndexError(
252 "corrupt delete bitmap in link extension".to_owned(),
253 ));
254 };
255 rest = &rest[consumed..];
256 let Some((rep, consumed2)) = EwahBitmap::deserialize_prefix(rest) else {
257 return Err(Error::IndexError(
258 "corrupt replace bitmap in link extension".to_owned(),
259 ));
260 };
261 rest = &rest[consumed2..];
262 if !rest.is_empty() {
263 return Err(Error::IndexError(
264 "garbage at the end of link extension".to_owned(),
265 ));
266 }
267 Ok(SplitIndexLink {
268 base_oid,
269 delete_bitmap: Some(del),
270 replace_bitmap: Some(rep),
271 })
272}
273
274pub(crate) fn serialize_link_extension_payload(
276 base_oid: &ObjectId,
277 delete: &EwahBitmap,
278 replace: &EwahBitmap,
279) -> Vec<u8> {
280 let mut out = base_oid.as_bytes().to_vec();
281 delete.serialize(&mut out);
282 replace.serialize(&mut out);
283 out
284}
285
286fn resolve_shared_index_file(git_dir: &Path, index_path: &Path, base_oid: &ObjectId) -> PathBuf {
289 let name = format!("sharedindex.{}", base_oid.to_hex());
290 let primary = git_dir.join(&name);
291
292 let try_path = |p: PathBuf| -> Option<PathBuf> {
293 if p.is_file() {
294 Some(p)
295 } else {
296 None
297 }
298 };
299
300 if let Some(p) = try_path(primary.clone()) {
301 return p;
302 }
303 if let Some(parent) = index_path.parent() {
304 if let Some(p) = try_path(parent.join(&name)) {
305 return p;
306 }
307 }
308 if let Ok(cwd) = std::env::current_dir() {
309 let mut dir = cwd.as_path();
310 loop {
311 if let Some(p) = try_path(dir.join(".git").join(&name)) {
312 return p;
313 }
314 let Some(p) = dir.parent() else {
315 break;
316 };
317 dir = p;
318 }
319 }
320 if let Some(d) = index_path.parent() {
321 if let Ok(read) = fs::read_dir(d) {
322 for ent in read.flatten() {
323 let Ok(ft) = ent.file_type() else {
324 continue;
325 };
326 if !ft.is_dir() {
327 continue;
328 }
329 if let Some(p) = try_path(ent.path().join(".git").join(&name)) {
330 return p;
331 }
332 }
333 }
334 }
335 primary
336}
337
338pub(crate) fn hash_index_body(body: &[u8], algo: HashAlgo) -> ObjectId {
339 let digest: Vec<u8> = match algo {
340 HashAlgo::Sha1 => {
341 let mut hasher = Sha1::new();
342 hasher.update(body);
343 hasher.finalize().to_vec()
344 }
345 HashAlgo::Sha256 => {
346 let mut hasher = Sha256::new();
347 hasher.update(body);
348 hasher.finalize().to_vec()
349 }
350 };
351 ObjectId::from_bytes(&digest).unwrap_or_else(|_| unreachable!("digest is a valid OID width"))
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355pub(crate) enum SplitIndexConfig {
356 Disabled,
357 Unset,
358 Enabled,
359}
360
361pub(crate) fn split_index_config(cfg: &ConfigSet) -> SplitIndexConfig {
362 match cfg.get("core.splitIndex") {
363 None => SplitIndexConfig::Unset,
364 Some(v) => {
365 let t = v.trim();
366 if t.eq_ignore_ascii_case("false") || t == "0" {
367 SplitIndexConfig::Disabled
368 } else if t.eq_ignore_ascii_case("true") || t == "1" {
369 SplitIndexConfig::Enabled
370 } else {
371 SplitIndexConfig::Unset
372 }
373 }
374 }
375}
376
377pub(crate) fn max_percent_split_change(cfg: &ConfigSet) -> i32 {
378 match cfg.get("splitIndex.maxPercentChange") {
379 None => -1,
380 Some(v) => v.trim().parse::<i32>().unwrap_or(-1),
381 }
382}
383
384fn default_max_percent() -> i32 {
385 20
386}
387
388pub(crate) fn should_rebuild_shared_index(index: &Index, cfg: &ConfigSet) -> bool {
389 let max_split = max_percent_split_change(cfg);
390 let max_split = match max_split {
391 -1 => default_max_percent(),
392 0 => return true,
393 100 => return false,
394 n => n,
395 };
396 let mut not_shared = 0u64;
397 for e in &index.entries {
398 if e.base_index_pos == 0 {
399 not_shared += 1;
400 }
401 }
402 let total = index.entries.len() as u64;
403 if total == 0 {
404 return false;
405 }
406 total * (max_split as u64) < not_shared * 100
407}
408
409pub(crate) fn git_test_split_index_env() -> bool {
410 std::env::var("GIT_TEST_SPLIT_INDEX")
411 .ok()
412 .map(|v| {
413 let t = v.trim();
414 t == "1" || t.eq_ignore_ascii_case("true") || t.eq_ignore_ascii_case("yes")
415 })
416 .unwrap_or(false)
417}
418
419pub(crate) fn git_test_check_cache_tree() -> bool {
429 match std::env::var("GIT_TEST_CHECK_CACHE_TREE") {
430 Ok(v) => {
431 let t = v.trim();
432 !(t.is_empty()
433 || t == "0"
434 || t.eq_ignore_ascii_case("false")
435 || t.eq_ignore_ascii_case("no"))
436 }
437 Err(_) => true,
438 }
439}
440
441pub(crate) fn git_test_split_index_force_reorder(base_oid: &ObjectId) -> bool {
442 git_test_split_index_env() && (base_oid.as_bytes()[0] & 15) < 6
443}
444
445pub(crate) fn shared_index_expire_threshold(cfg: &ConfigSet) -> u64 {
446 let raw = cfg
447 .get("splitIndex.sharedIndexExpire")
448 .map(|s| s.trim().to_owned());
449 let spec = raw
450 .as_deref()
451 .filter(|s| !s.is_empty())
452 .unwrap_or("2.weeks.ago");
453 if spec.eq_ignore_ascii_case("never") {
454 return 0;
455 }
456 let mut err = 0;
457 approxidate_careful(spec, Some(&mut err))
458}
459
460fn should_delete_shared_index(path: &Path, expiration: u64) -> bool {
461 if expiration == 0 {
462 return false;
463 }
464 let Ok(meta) = fs::metadata(path) else {
465 return false;
466 };
467 #[cfg(unix)]
468 {
469 use std::os::unix::fs::MetadataExt;
470 meta.mtime() as u64 <= expiration
471 }
472 #[cfg(not(unix))]
473 {
474 let _ = meta;
475 false
476 }
477}
478
479pub(crate) fn clean_stale_shared_index_files(git_dir: &Path, current_hex: &str, cfg: &ConfigSet) {
480 let expiration = shared_index_expire_threshold(cfg);
481 let Ok(read_dir) = fs::read_dir(git_dir) else {
482 return;
483 };
484 for ent in read_dir.flatten() {
485 let name = ent.file_name();
486 let Some(name) = name.to_str() else {
487 continue;
488 };
489 let Some(hex) = name.strip_prefix("sharedindex.") else {
490 continue;
491 };
492 if hex == current_hex {
493 continue;
494 }
495 let path = ent.path();
496 if should_delete_shared_index(&path, expiration) {
497 let _ = fs::remove_file(&path);
498 }
499 }
500}
501
502pub(crate) fn freshen_shared_index(path: &Path) {
503 let _ = filetime_set_to_now(path);
504}
505
506#[cfg(unix)]
507fn filetime_set_to_now(path: &Path) -> io::Result<()> {
508 use std::time::SystemTime;
509 let t = SystemTime::now();
510 let ft = filetime::FileTime::from_system_time(t);
511 filetime::set_file_mtime(path, ft)
512}
513
514#[cfg(not(unix))]
515fn filetime_set_to_now(_path: &Path) -> io::Result<()> {
516 Ok(())
517}
518
519#[derive(Debug, Clone, Copy, Default)]
521pub struct WriteSplitIndexRequest {
522 pub explicit: Option<bool>,
524}
525
526impl WriteSplitIndexRequest {
527 pub fn want_write_split(self, cfg: &ConfigSet, index: &Index) -> bool {
535 match self.explicit {
536 Some(false) => {
537 if matches!(split_index_config(cfg), SplitIndexConfig::Enabled) {
538 eprintln!(
539 "warning: core.splitIndex is set to true; remove or change it, if you really want to disable split index"
540 );
541 }
542 false
543 }
544 Some(true) => {
545 if matches!(split_index_config(cfg), SplitIndexConfig::Disabled) {
546 eprintln!(
547 "warning: core.splitIndex is set to false; remove or change it, if you really want to enable split index"
548 );
549 }
550 true
551 }
552 None => {
553 if matches!(split_index_config(cfg), SplitIndexConfig::Disabled) {
554 return false;
555 }
556 index.split_link.is_some()
557 || matches!(split_index_config(cfg), SplitIndexConfig::Enabled)
558 || git_test_split_index_env()
559 }
560 }
561 }
562}
563
564fn find_entry_pos_sorted(entries: &[IndexEntry], path: &[u8], stage: u8) -> Option<usize> {
565 entries
566 .binary_search_by(|e| {
567 e.path
568 .as_slice()
569 .cmp(path)
570 .then_with(|| e.stage().cmp(&stage))
571 })
572 .ok()
573}
574
575fn load_shared_entries(
576 git_dir: &Path,
577 index_path: &Path,
578 base_oid: &ObjectId,
579) -> Result<Vec<IndexEntry>> {
580 let p = resolve_shared_index_file(git_dir, index_path, base_oid);
581 let data = fs::read(&p).map_err(Error::Io)?;
582 let mut shared = Index::parse(&data)?;
583 for (i, e) in shared.entries.iter_mut().enumerate() {
584 e.base_index_pos = (i + 1) as u32;
585 }
586 Ok(shared.entries)
587}
588
589pub(crate) fn write_index_file_split(
591 path: &Path,
592 git_dir: &Path,
593 index: &mut Index,
594 cfg: &ConfigSet,
595 request: WriteSplitIndexRequest,
596 skip_hash: bool,
597) -> Result<()> {
598 if git_test_check_cache_tree() {
603 crate::write_tree::verify_cache_tree(index)?;
604 }
605
606 let want_split = request.want_write_split(cfg, index);
607
608 let shared_repo = parse_shared_repository_perm(cfg.get("core.sharedRepository").as_deref());
609
610 if !want_split {
611 index.split_link = None;
612 for e in &mut index.entries {
613 e.base_index_pos = 0;
614 }
615 index.write_to_path(path, skip_hash)?;
616 adjust_shared_perm_file(path, shared_repo).map_err(Error::Io)?;
617 return Ok(());
618 }
619
620 let default_index = git_dir.join("index");
623 let is_primary_index = path == default_index
624 || path
625 .canonicalize()
626 .ok()
627 .zip(default_index.canonicalize().ok())
628 .is_some_and(|(a, b)| a == b);
629 if !is_primary_index {
630 index.split_link = None;
631 for e in &mut index.entries {
632 e.base_index_pos = 0;
633 }
634 index.write_to_path(path, skip_hash)?;
635 adjust_shared_perm_file(path, shared_repo).map_err(Error::Io)?;
636 return Ok(());
637 }
638
639 if index.sparse_directories {
640 return Err(Error::IndexError(
641 "cannot write split index for a sparse index".to_owned(),
642 ));
643 }
644
645 let prev_base = index
646 .split_link
647 .as_ref()
648 .map(|l| l.base_oid)
649 .unwrap_or(ObjectId::zero());
650
651 let mut rebuild = index.split_link.is_none()
652 || should_rebuild_shared_index(index, cfg)
653 || git_test_split_index_force_reorder(&prev_base);
654
655 if git_test_split_index_env() && index.split_link.is_none() {
656 rebuild = true;
657 }
658
659 let base_snapshot: Vec<IndexEntry> = if rebuild {
660 let mut v: Vec<IndexEntry> = index.entries.to_vec();
661 v.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
662 for (i, e) in v.iter_mut().enumerate() {
663 e.base_index_pos = (i + 1) as u32;
664 }
665 v
666 } else {
667 let link = index.split_link.as_ref().ok_or_else(|| {
668 Error::IndexError("split index missing base link during reuse".to_owned())
669 })?;
670 load_shared_entries(git_dir, path, &link.base_oid)?
671 };
672
673 if rebuild {
678 for e in &mut index.entries {
679 if let Some(i) = base_snapshot
680 .iter()
681 .position(|b| b.path == e.path && b.stage() == e.stage())
682 {
683 e.base_index_pos = (i + 1) as u32;
684 } else {
685 e.base_index_pos = 0;
686 }
687 }
688 }
689
690 let base_oid = if rebuild {
691 let shared_index = Index {
692 version: index.version,
693 entries: base_snapshot.clone(),
694 sparse_directories: false,
695 untracked_cache: None,
696 fsmonitor_last_update: None,
697 resolve_undo: None,
698 split_link: None,
699 cache_tree_root: None,
700 cache_tree: None,
701 hash_algo: index.hash_algo,
702 };
703 let tmp = match tempfile::NamedTempFile::new_in(git_dir) {
704 Ok(t) => t,
705 Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
706 index.split_link = None;
708 for e in &mut index.entries {
709 e.base_index_pos = 0;
710 }
711 index.write_to_path(path, skip_hash)?;
712 adjust_shared_perm_file(path, shared_repo).map_err(Error::Io)?;
713 return Ok(());
714 }
715 Err(e) => return Err(Error::Io(e)),
716 };
717 let tmp_path = tmp.path().to_path_buf();
718 shared_index.write_to_path(&tmp_path, skip_hash)?;
719 adjust_shared_perm_file(&tmp_path, shared_repo).map_err(Error::Io)?;
720 let file_data = fs::read(&tmp_path).map_err(Error::Io)?;
721 let hash_len = index.hash_algo.len();
722 if file_data.len() < hash_len {
723 return Err(Error::IndexError("shared index temp too short".to_owned()));
724 }
725 let body = &file_data[..file_data.len() - hash_len];
726 let oid = hash_index_body(body, index.hash_algo);
727 let dest = git_dir.join(format!("sharedindex.{}", oid.to_hex()));
728 if let Err(e) = fs::rename(&tmp_path, &dest) {
729 if e.kind() == io::ErrorKind::PermissionDenied {
730 let _ = fs::remove_file(&tmp_path);
731 index.split_link = None;
732 for ent in &mut index.entries {
733 ent.base_index_pos = 0;
734 }
735 index.write_to_path(path, skip_hash)?;
736 adjust_shared_perm_file(path, shared_repo).map_err(Error::Io)?;
737 return Ok(());
738 }
739 return Err(Error::Io(e));
740 }
741 clean_stale_shared_index_files(git_dir, &oid.to_hex(), cfg);
742 oid
743 } else {
744 let oid = index
745 .split_link
746 .as_ref()
747 .ok_or_else(|| {
748 Error::IndexError("split index missing base link during reuse".to_owned())
749 })?
750 .base_oid;
751 freshen_shared_index(&resolve_shared_index_file(git_dir, path, &oid));
752 oid
753 };
754
755 let mut merged_by_pos: Vec<Option<usize>> = vec![None; base_snapshot.len()];
758 for (p, e) in index.entries.iter().enumerate() {
759 if e.base_index_pos == 0 {
760 continue;
761 }
762 let i = e.base_index_pos.saturating_sub(1) as usize;
763 if i < base_snapshot.len()
764 && base_snapshot[i].path == e.path
765 && base_snapshot[i].stage() == e.stage()
766 {
767 merged_by_pos[i] = Some(p);
768 }
769 }
770
771 let mut delete_bm = EwahBitmap::new();
772 let mut replace_bm = EwahBitmap::new();
773 let mut main_entries: Vec<IndexEntry> = Vec::new();
774
775 for i in 0..base_snapshot.len() {
776 let b = &base_snapshot[i];
777 if let Some(p) = merged_by_pos[i] {
778 let c = &index.entries[p];
779 if entries_equal_for_split(b, c) {
780 continue;
781 }
782 replace_bm.set_bit_extend(i);
783 let mut stub = c.clone();
784 stub.path.clear();
785 stub.base_index_pos = 0;
786 main_entries.push(stub);
787 } else {
788 delete_bm.set_bit_extend(i);
789 }
790 }
791
792 for e in &index.entries {
793 if e.base_index_pos == 0 {
794 let mut c = e.clone();
795 c.base_index_pos = 0;
796 main_entries.push(c);
797 continue;
798 }
799 let i = e.base_index_pos.saturating_sub(1) as usize;
800 if i >= base_snapshot.len()
801 || base_snapshot[i].path != e.path
802 || base_snapshot[i].stage() != e.stage()
803 {
804 let mut c = e.clone();
805 c.base_index_pos = 0;
806 main_entries.push(c);
807 continue;
808 }
809 if entries_equal_for_split(&base_snapshot[i], e) {
810 continue;
811 }
812 }
814
815 main_entries.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
816
817 let link = SplitIndexLink {
818 base_oid,
819 delete_bitmap: Some(delete_bm),
820 replace_bitmap: Some(replace_bm),
821 };
822
823 let out_index = Index {
824 version: index.version,
825 entries: main_entries,
826 sparse_directories: false,
827 untracked_cache: index.untracked_cache.clone(),
828 fsmonitor_last_update: index.fsmonitor_last_update.clone(),
829 resolve_undo: None,
830 split_link: Some(link),
831 cache_tree_root: index.cache_tree_root,
832 cache_tree: index.cache_tree.clone(),
833 hash_algo: index.hash_algo,
834 };
835
836 out_index.write_to_path(path, skip_hash)?;
837 adjust_shared_perm_file(path, shared_repo).map_err(Error::Io)?;
838
839 for e in &mut index.entries {
840 if let Some(pos) = find_entry_pos_sorted(&base_snapshot, &e.path, e.stage()) {
841 if entries_equal_for_split(&base_snapshot[pos], e) {
842 e.base_index_pos = (pos + 1) as u32;
843 continue;
844 }
845 }
846 e.base_index_pos = 0;
847 }
848
849 index.split_link = Some(SplitIndexLink {
850 base_oid,
851 delete_bitmap: None,
852 replace_bitmap: None,
853 });
854
855 Ok(())
856}
857
858pub fn resolve_split_index_if_needed(
861 index: &mut Index,
862 git_dir: &Path,
863 index_path: &Path,
864) -> Result<()> {
865 let Some(link) = index.split_link.clone() else {
866 return Ok(());
867 };
868 if link.base_oid.is_zero() {
869 return Ok(());
870 }
871 let base_oid = link.base_oid;
872 let shared_path = resolve_shared_index_file(git_dir, index_path, &base_oid);
873 let data = fs::read(&shared_path).map_err(|e| {
874 Error::IndexError(format!(
875 "split index: cannot read shared index {}: {e}",
876 shared_path.display()
877 ))
878 })?;
879 let hash_len = index.hash_algo.len();
880 if data.len() < hash_len {
881 return Err(Error::IndexError(
882 "split index: shared index too short".to_owned(),
883 ));
884 }
885 let body = &data[..data.len() - hash_len];
886 let got = hash_index_body(body, index.hash_algo);
887 if got != base_oid {
888 return Err(Error::IndexError(format!(
889 "broken index, expect {} in {}, got {}",
890 base_oid.to_hex(),
891 shared_path.display(),
892 got.to_hex()
893 )));
894 }
895 freshen_shared_index(&shared_path);
896 let base_entries = Index::parse(&data)?.entries;
897 merge_split_into_index(index, link, base_entries)?;
898 index.split_link = Some(SplitIndexLink {
899 base_oid,
900 delete_bitmap: None,
901 replace_bitmap: None,
902 });
903 Ok(())
904}
905
906pub fn format_dump_split_index_file(data: &[u8], index: &Index) -> Result<String> {
909 use std::fmt::Write;
910 let hash_len = index.hash_algo.len();
911 if data.len() < hash_len {
912 return Err(Error::IndexError("index too short".to_owned()));
913 }
914 let body = &data[..data.len() - hash_len];
915 let trail = &data[data.len() - hash_len..];
916 let own = if trail.iter().all(|&b| b == 0) {
917 hash_index_body(body, index.hash_algo)
918 } else {
919 ObjectId::from_bytes(trail)?
920 };
921
922 let mut s = String::new();
923 writeln!(s, "own {}", own.to_hex()).map_err(|e| Error::IndexError(e.to_string()))?;
924 let Some(link) = &index.split_link else {
925 writeln!(s, "not a split index").map_err(|e| Error::IndexError(e.to_string()))?;
926 return Ok(s);
927 };
928 writeln!(s, "base {}", link.base_oid.to_hex()).map_err(|e| Error::IndexError(e.to_string()))?;
929 for e in &index.entries {
930 let path_disp = String::from_utf8_lossy(&e.path);
932 writeln!(
933 s,
934 "{:06o} {} {}\t{}",
935 e.mode,
936 e.oid.to_hex(),
937 e.stage(),
938 path_disp
939 )
940 .map_err(|e| Error::IndexError(e.to_string()))?;
941 }
942 write!(s, "replacements:").map_err(|e| Error::IndexError(e.to_string()))?;
943 if let Some(bm) = &link.replace_bitmap {
944 bm.each_set_bit(|pos| {
945 write!(s, " {}", pos).ok();
946 });
947 }
948 writeln!(s).map_err(|e| Error::IndexError(e.to_string()))?;
949 write!(s, "deletions:").map_err(|e| Error::IndexError(e.to_string()))?;
950 if let Some(bm) = &link.delete_bitmap {
951 bm.each_set_bit(|pos| {
952 write!(s, " {}", pos).ok();
953 });
954 }
955 writeln!(s).map_err(|e| Error::IndexError(e.to_string()))?;
956 Ok(s)
957}