1use std::fs;
19use std::io::{self, Write};
20use std::path::Path;
21
22use sha1::{Digest, Sha1};
23
24use crate::config::ConfigSet;
25use crate::error::{Error, Result};
26use crate::objects::ObjectId;
27
28pub const MODE_REGULAR: u32 = 0o100644;
30pub const MODE_EXECUTABLE: u32 = 0o100755;
32pub const MODE_SYMLINK: u32 = 0o120000;
34pub const MODE_GITLINK: u32 = 0o160000;
36pub const MODE_TREE: u32 = 0o040000;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct IndexEntry {
42 pub ctime_sec: u32,
44 pub ctime_nsec: u32,
46 pub mtime_sec: u32,
48 pub mtime_nsec: u32,
50 pub dev: u32,
52 pub ino: u32,
54 pub mode: u32,
56 pub uid: u32,
58 pub gid: u32,
60 pub size: u32,
62 pub oid: ObjectId,
64 pub flags: u16,
66 pub flags_extended: Option<u16>,
68 pub path: Vec<u8>,
70}
71
72impl IndexEntry {
73 #[must_use]
75 pub fn stage(&self) -> u8 {
76 ((self.flags >> 12) & 0x3) as u8
77 }
78
79 #[must_use]
81 pub fn assume_unchanged(&self) -> bool {
82 self.flags & 0x8000 != 0
83 }
84
85 #[must_use]
87 pub fn skip_worktree(&self) -> bool {
88 self.flags_extended
89 .map(|f| f & 0x4000 != 0)
90 .unwrap_or(false)
91 }
92
93 pub fn set_assume_unchanged(&mut self, value: bool) {
95 if value {
96 self.flags |= 0x8000;
97 } else {
98 self.flags &= !0x8000;
99 }
100 }
101
102 pub fn set_skip_worktree(&mut self, value: bool) {
104 let fe = self.flags_extended.get_or_insert(0);
105 if value {
106 *fe |= 0x4000;
107 } else {
108 *fe &= !0x4000;
109 if *fe == 0 {
110 self.flags_extended = None;
111 }
112 }
113 }
114
115 #[must_use]
117 pub fn intent_to_add(&self) -> bool {
118 self.flags_extended
119 .map(|f| f & 0x2000 != 0)
120 .unwrap_or(false)
121 }
122
123 pub fn set_intent_to_add(&mut self, value: bool) {
125 let fe = self.flags_extended.get_or_insert(0);
126 if value {
127 *fe |= 0x2000;
128 } else {
129 *fe &= !0x2000;
130 if *fe == 0 {
131 self.flags_extended = None;
132 }
133 }
134 }
135}
136
137#[derive(Debug, Clone, Default)]
139pub struct Index {
140 pub version: u32,
142 pub entries: Vec<IndexEntry>,
144}
145
146const INDEX_FORMAT_DEFAULT: u32 = 3;
148const INDEX_FORMAT_LB: u32 = 2;
150const INDEX_FORMAT_UB: u32 = 4;
153
154pub fn get_index_format_from_env() -> Option<u32> {
160 let val = std::env::var("GIT_INDEX_VERSION").ok()?;
161 if val.is_empty() {
162 return None;
163 }
164 match val.parse::<u32>() {
165 Ok(v) if (INDEX_FORMAT_LB..=INDEX_FORMAT_UB).contains(&v) => Some(v),
166 _ => {
167 eprintln!(
168 "warning: GIT_INDEX_VERSION set, but the value is invalid.\n\
169 Using version {INDEX_FORMAT_DEFAULT}"
170 );
171 Some(INDEX_FORMAT_DEFAULT)
172 }
173 }
174}
175
176impl Index {
177 #[must_use]
181 pub fn new() -> Self {
182 let version = get_index_format_from_env().unwrap_or(2);
183 Self {
184 version,
185 entries: Vec::new(),
186 }
187 }
188
189 pub fn new_with_config(
193 config_index_version: Option<&str>,
194 config_many_files: Option<&str>,
195 ) -> Self {
196 if let Some(v) = get_index_format_from_env() {
198 return Self {
199 version: v,
200 entries: Vec::new(),
201 };
202 }
203 if let Some(val) = config_index_version {
205 if let Ok(v) = val.parse::<u32>() {
206 if (INDEX_FORMAT_LB..=INDEX_FORMAT_UB).contains(&v) {
207 return Self {
208 version: v,
209 entries: Vec::new(),
210 };
211 }
212 }
213 eprintln!(
215 "warning: index.version set, but the value is invalid.\n\
216 Using version {INDEX_FORMAT_DEFAULT}"
217 );
218 return Self {
219 version: INDEX_FORMAT_DEFAULT,
220 entries: Vec::new(),
221 };
222 }
223 if let Some(val) = config_many_files {
225 let lowered = val.to_lowercase();
226 let enabled = matches!(lowered.as_str(), "true" | "yes" | "1" | "on");
227 if enabled {
228 return Self {
229 version: 4,
230 entries: Vec::new(),
231 };
232 }
233 }
234 Self {
235 version: 2,
236 entries: Vec::new(),
237 }
238 }
239
240 pub fn load(path: &Path) -> Result<Self> {
248 match fs::read(path) {
249 Ok(data) => Self::parse(&data),
250 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Self::new()),
251 Err(e) => Err(Error::Io(e)),
252 }
253 }
254
255 pub fn parse(data: &[u8]) -> Result<Self> {
261 if data.len() < 12 {
262 return Err(Error::IndexError("file too short".to_owned()));
263 }
264
265 let (body, checksum) = data.split_at(data.len() - 20);
267 let mut hasher = Sha1::new();
268 hasher.update(body);
269 let computed = hasher.finalize();
270 if computed.as_slice() != checksum {
271 return Err(Error::IndexError("SHA-1 checksum mismatch".to_owned()));
272 }
273
274 let magic = &body[..4];
276 if magic != b"DIRC" {
277 return Err(Error::IndexError("bad magic: expected DIRC".to_owned()));
278 }
279 let version = u32::from_be_bytes(
280 body[4..8]
281 .try_into()
282 .map_err(|_| Error::IndexError("cannot read version".to_owned()))?,
283 );
284 if version != 2 && version != 3 && version != 4 {
285 return Err(Error::IndexError(format!(
286 "unsupported index version {version}"
287 )));
288 }
289 let count = u32::from_be_bytes(
290 body[8..12]
291 .try_into()
292 .map_err(|_| Error::IndexError("cannot read entry count".to_owned()))?,
293 );
294
295 let mut pos = 12usize;
296 let mut entries = Vec::with_capacity(count as usize);
297
298 let mut prev_path: Vec<u8> = Vec::new();
299 for _ in 0..count {
300 let (entry, consumed) = parse_entry(&body[pos..], version, &prev_path)?;
301 prev_path = entry.path.clone();
302 entries.push(entry);
303 pos += consumed;
304 }
305
306 Ok(Self { version, entries })
307 }
308
309 pub fn write(&self, path: &Path) -> Result<()> {
315 let mut body = Vec::new();
316 self.serialize_into(&mut body)?;
317
318 let mut hasher = Sha1::new();
319 hasher.update(&body);
320 let checksum = hasher.finalize();
321
322 let tmp_path = path.with_extension("lock");
323 let pid_path = pid_path_for_lock(&tmp_path);
324 let lockfile_pid_enabled = lockfile_pid_enabled(path);
325
326 let mut lock_file = match fs::OpenOptions::new()
327 .write(true)
328 .create_new(true)
329 .open(&tmp_path)
330 {
331 Ok(file) => file,
332 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
333 let message = build_lock_exists_message(&tmp_path, &pid_path, &e);
334 return Err(Error::Io(io::Error::new(
335 io::ErrorKind::AlreadyExists,
336 message,
337 )));
338 }
339 Err(e) => return Err(Error::Io(e)),
340 };
341
342 let mut wrote_pid_file = false;
343 if lockfile_pid_enabled {
344 if let Err(e) = write_lock_pid_file(&pid_path) {
345 let _ = fs::remove_file(&tmp_path);
346 return Err(Error::Io(e));
347 }
348 wrote_pid_file = true;
349 }
350
351 if let Err(e) = (|| -> io::Result<()> {
352 lock_file.write_all(&body)?;
353 lock_file.write_all(&checksum)?;
354 Ok(())
355 })() {
356 let _ = fs::remove_file(&tmp_path);
357 if wrote_pid_file {
358 let _ = fs::remove_file(&pid_path);
359 }
360 return Err(Error::Io(e));
361 }
362 drop(lock_file);
363
364 if let Err(e) = fs::rename(&tmp_path, path) {
365 let _ = fs::remove_file(&tmp_path);
366 if wrote_pid_file {
367 let _ = fs::remove_file(&pid_path);
368 }
369 return Err(Error::Io(e));
370 }
371 {
372 if wrote_pid_file {
373 let _ = fs::remove_file(&pid_path);
374 }
375 }
376 Ok(())
377 }
378
379 fn serialize_into(&self, out: &mut Vec<u8>) -> Result<()> {
381 let has_extended_flags = self.entries.iter().any(|e| e.flags_extended.is_some());
385 let write_version = if has_extended_flags {
386 3
387 } else if self.version >= 3 {
388 2
389 } else {
390 self.version
391 };
392 out.extend_from_slice(b"DIRC");
394 out.extend_from_slice(&write_version.to_be_bytes());
395 out.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
396
397 for entry in &self.entries {
398 serialize_entry(entry, write_version, out);
399 }
400 Ok(())
401 }
402
403 pub fn add_or_replace(&mut self, entry: IndexEntry) {
405 let path = &entry.path;
406 let stage = entry.stage();
407 let result = self.entries.binary_search_by(|e| {
409 e.path
410 .as_slice()
411 .cmp(path.as_slice())
412 .then_with(|| e.stage().cmp(&stage))
413 });
414 match result {
415 Ok(pos) => {
416 self.entries[pos] = entry;
418 }
419 Err(pos) => {
420 self.entries.insert(pos, entry);
422 }
423 }
424 }
425
426 pub fn stage_file(&mut self, entry: IndexEntry) {
430 let path = entry.path.clone();
431 self.entries.retain(|e| e.path != path || e.stage() == 0);
433 self.add_or_replace(entry);
435 }
436
437 pub fn remove(&mut self, path: &[u8]) -> bool {
441 let before = self.entries.len();
442 self.entries.retain(|e| e.path != path);
443 self.entries.len() < before
444 }
445
446 pub fn sort(&mut self) {
448 self.entries
449 .sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
450 }
451
452 #[must_use]
454 pub fn get(&self, path: &[u8], stage: u8) -> Option<&IndexEntry> {
455 self.entries
456 .iter()
457 .find(|e| e.path == path && e.stage() == stage)
458 }
459
460 pub fn get_mut(&mut self, path: &[u8], stage: u8) -> Option<&mut IndexEntry> {
462 self.entries
463 .iter_mut()
464 .find(|e| e.path == path && e.stage() == stage)
465 }
466}
467
468fn lockfile_pid_enabled(index_path: &Path) -> bool {
469 let git_dir = match index_path.parent() {
470 Some(dir) => dir,
471 None => return false,
472 };
473
474 ConfigSet::load(Some(git_dir), true)
475 .ok()
476 .and_then(|cfg| cfg.get_bool("core.lockfilepid"))
477 .and_then(|res| res.ok())
478 .unwrap_or(false)
479}
480
481fn pid_path_for_lock(lock_path: &Path) -> std::path::PathBuf {
482 let file_name = lock_path
483 .file_name()
484 .map(|s| s.to_string_lossy().to_string())
485 .unwrap_or_else(|| "index.lock".to_owned());
486 let pid_name = if let Some(base) = file_name.strip_suffix(".lock") {
487 format!("{base}~pid.lock")
488 } else {
489 format!("{file_name}~pid.lock")
490 };
491 lock_path.with_file_name(pid_name)
492}
493
494fn write_lock_pid_file(pid_path: &Path) -> io::Result<()> {
495 use std::io::Write as _;
496 let mut file = fs::OpenOptions::new()
497 .write(true)
498 .create(true)
499 .truncate(true)
500 .open(pid_path)?;
501 writeln!(file, "pid {}", std::process::id())?;
502 Ok(())
503}
504
505fn build_lock_exists_message(lock_path: &Path, pid_path: &Path, err: &io::Error) -> String {
506 let mut msg = format!("Unable to create '{}': {}.\n\n", lock_path.display(), err);
507
508 if let Some(pid) = read_lock_pid(pid_path) {
509 if is_process_running(pid) {
510 msg.push_str(&format!(
511 "Lock is held by process {pid}; if no git process is running, the lock file may be stale (PIDs can be reused)"
512 ));
513 } else {
514 msg.push_str(&format!(
515 "Lock was held by process {pid}, which is no longer running; the lock file appears to be stale"
516 ));
517 }
518 } else {
519 msg.push_str(
520 "Another git process seems to be running in this repository, or the lock file may be stale",
521 );
522 }
523
524 msg
525}
526
527fn read_lock_pid(pid_path: &Path) -> Option<u64> {
528 let raw = fs::read_to_string(pid_path).ok()?;
529 let trimmed = raw.trim();
530 if let Some(v) = trimmed.strip_prefix("pid ") {
531 return v.trim().parse::<u64>().ok();
532 }
533 trimmed.parse::<u64>().ok()
534}
535
536fn is_process_running(pid: u64) -> bool {
537 #[cfg(target_os = "linux")]
538 {
539 let proc_path = std::path::PathBuf::from(format!("/proc/{pid}"));
540 proc_path.exists()
541 }
542
543 #[cfg(not(target_os = "linux"))]
544 {
545 let status = std::process::Command::new("kill")
546 .arg("-0")
547 .arg(pid.to_string())
548 .status();
549 status.map(|s| s.success()).unwrap_or(false)
550 }
551}
552
553fn parse_entry(data: &[u8], version: u32, prev_path: &[u8]) -> Result<(IndexEntry, usize)> {
555 if data.len() < 62 {
556 return Err(Error::IndexError("entry too short".to_owned()));
557 }
558
559 let mut pos = 0;
560
561 macro_rules! read_u32 {
562 () => {{
563 let v = u32::from_be_bytes(
564 data[pos..pos + 4]
565 .try_into()
566 .map_err(|_| Error::IndexError("truncated u32".to_owned()))?,
567 );
568 pos += 4;
569 v
570 }};
571 }
572
573 let ctime_sec = read_u32!();
574 let ctime_nsec = read_u32!();
575 let mtime_sec = read_u32!();
576 let mtime_nsec = read_u32!();
577 let dev = read_u32!();
578 let ino = read_u32!();
579 let mode = read_u32!();
580 let uid = read_u32!();
581 let gid = read_u32!();
582 let size = read_u32!();
583
584 let oid = ObjectId::from_bytes(&data[pos..pos + 20])?;
585 pos += 20;
586
587 let flags = u16::from_be_bytes(
588 data[pos..pos + 2]
589 .try_into()
590 .map_err(|_| Error::IndexError("truncated flags".to_owned()))?,
591 );
592 pos += 2;
593
594 let flags_extended = if version >= 3 && flags & 0x4000 != 0 {
595 let fe = u16::from_be_bytes(
596 data[pos..pos + 2]
597 .try_into()
598 .map_err(|_| Error::IndexError("truncated extended flags".to_owned()))?,
599 );
600 pos += 2;
601 Some(fe)
602 } else {
603 None
604 };
605
606 let path;
607 if version == 4 {
608 let (strip_len, varint_bytes) = read_varint(&data[pos..]);
610 pos += varint_bytes;
611 let nul = data[pos..]
612 .iter()
613 .position(|&b| b == 0)
614 .ok_or_else(|| Error::IndexError("v4 entry path missing NUL".to_owned()))?;
615 let suffix = &data[pos..pos + nul];
616 pos += nul + 1;
617 let keep = prev_path.len().saturating_sub(strip_len);
618 let mut full_path = prev_path[..keep].to_vec();
619 full_path.extend_from_slice(suffix);
620 path = full_path;
621 } else {
622 let nul = data[pos..]
624 .iter()
625 .position(|&b| b == 0)
626 .ok_or_else(|| Error::IndexError("entry path missing NUL terminator".to_owned()))?;
627 path = data[pos..pos + nul].to_vec();
628 pos += nul + 1;
629 let entry_start = 0usize;
630 let entry_len = pos - entry_start;
631 let padded = (entry_len + 7) & !7;
632 let padding = padded.saturating_sub(entry_len);
633 pos += padding;
634 }
635
636 Ok((
637 IndexEntry {
638 ctime_sec,
639 ctime_nsec,
640 mtime_sec,
641 mtime_nsec,
642 dev,
643 ino,
644 mode,
645 uid,
646 gid,
647 size,
648 oid,
649 flags,
650 flags_extended,
651 path,
652 },
653 pos,
654 ))
655}
656
657fn read_varint(data: &[u8]) -> (usize, usize) {
661 let mut value: usize = 0;
662 let mut shift = 0usize;
663 let mut pos = 0;
664 loop {
665 if pos >= data.len() {
666 break;
667 }
668 let byte = data[pos] as usize;
669 pos += 1;
670 value |= (byte & 0x7F) << shift;
671 if byte & 0x80 == 0 {
672 break;
673 }
674 shift += 7;
675 if shift > 28 {
677 break;
678 }
679 }
680 (value, pos)
681}
682
683fn serialize_entry(entry: &IndexEntry, version: u32, out: &mut Vec<u8>) {
684 let start = out.len();
685
686 let write_u32 = |out: &mut Vec<u8>, v: u32| out.extend_from_slice(&v.to_be_bytes());
687
688 write_u32(out, entry.ctime_sec);
689 write_u32(out, entry.ctime_nsec);
690 write_u32(out, entry.mtime_sec);
691 write_u32(out, entry.mtime_nsec);
692 write_u32(out, entry.dev);
693 write_u32(out, entry.ino);
694 write_u32(out, entry.mode);
695 write_u32(out, entry.uid);
696 write_u32(out, entry.gid);
697 write_u32(out, entry.size);
698 out.extend_from_slice(entry.oid.as_bytes());
699
700 let mut flags = entry.flags;
702 if version >= 3 && entry.flags_extended.is_some() {
703 flags |= 0x4000;
704 } else {
705 flags &= !0x4000;
706 }
707 let path_len = entry.path.len().min(0xFFF) as u16;
709 flags = (flags & 0xF000) | path_len;
710 out.extend_from_slice(&flags.to_be_bytes());
711
712 if version >= 3 {
713 if let Some(fe) = entry.flags_extended {
714 out.extend_from_slice(&fe.to_be_bytes());
715 }
716 }
717
718 out.extend_from_slice(&entry.path);
719 out.push(0);
720
721 let entry_len = out.len() - start;
723 let padded = (entry_len + 7) & !7;
724 let padding = padded - entry_len;
725 for _ in 0..padding {
726 out.push(0);
727 }
728}
729
730pub fn entry_from_stat(
743 path: &Path,
744 rel_path: &[u8],
745 oid: ObjectId,
746 mode: u32,
747) -> Result<IndexEntry> {
748 let meta = fs::symlink_metadata(path)?;
749 Ok(entry_from_metadata(&meta, rel_path, oid, mode))
750}
751
752#[must_use]
757pub fn entry_from_metadata(
758 meta: &fs::Metadata,
759 rel_path: &[u8],
760 oid: ObjectId,
761 mode: u32,
762) -> IndexEntry {
763 use std::os::unix::fs::MetadataExt;
764 IndexEntry {
765 ctime_sec: meta.ctime() as u32,
766 ctime_nsec: meta.ctime_nsec() as u32,
767 mtime_sec: meta.mtime() as u32,
768 mtime_nsec: meta.mtime_nsec() as u32,
769 dev: meta.dev() as u32,
770 ino: meta.ino() as u32,
771 mode,
772 uid: meta.uid(),
773 gid: meta.gid(),
774 size: meta.size() as u32,
775 oid,
776 flags: rel_path.len().min(0xFFF) as u16,
777 flags_extended: None,
778 path: rel_path.to_vec(),
779 }
780}
781
782#[must_use]
792pub fn normalize_mode(raw_mode: u32) -> u32 {
793 const S_IFMT: u32 = 0o170000;
794 const S_IFLNK: u32 = 0o120000;
795 const S_IFREG: u32 = 0o100000;
796
797 let fmt = raw_mode & S_IFMT;
798 if fmt == S_IFLNK {
799 return MODE_SYMLINK;
800 }
801 if fmt == S_IFREG {
802 if raw_mode & 0o111 != 0 {
804 return MODE_EXECUTABLE;
805 }
806 return MODE_REGULAR;
807 }
808 MODE_REGULAR
810}
811
812#[cfg(test)]
813mod tests {
814 #![allow(clippy::expect_used, clippy::unwrap_used)]
815
816 use super::*;
817 use tempfile::TempDir;
818
819 fn dummy_oid() -> ObjectId {
820 ObjectId::from_bytes(&[0u8; 20]).unwrap()
821 }
822
823 fn make_entry(path: &str) -> IndexEntry {
824 IndexEntry {
825 ctime_sec: 0,
826 ctime_nsec: 0,
827 mtime_sec: 0,
828 mtime_nsec: 0,
829 dev: 0,
830 ino: 0,
831 mode: MODE_REGULAR,
832 uid: 0,
833 gid: 0,
834 size: 0,
835 oid: dummy_oid(),
836 flags: path.len().min(0xFFF) as u16,
837 flags_extended: None,
838 path: path.as_bytes().to_vec(),
839 }
840 }
841
842 #[test]
843 fn round_trip_empty_index() {
844 let dir = TempDir::new().unwrap();
845 let path = dir.path().join("index");
846
847 let idx = Index::new();
848 idx.write(&path).unwrap();
849
850 let loaded = Index::load(&path).unwrap();
851 assert_eq!(loaded.entries.len(), 0);
852 }
853
854 #[test]
855 fn round_trip_with_entries() {
856 let dir = TempDir::new().unwrap();
857 let path = dir.path().join("index");
858
859 let mut idx = Index::new();
860 idx.add_or_replace(make_entry("foo.txt"));
861 idx.add_or_replace(make_entry("bar/baz.txt"));
862 idx.write(&path).unwrap();
863
864 let loaded = Index::load(&path).unwrap();
865 assert_eq!(loaded.entries.len(), 2);
866 assert_eq!(loaded.entries[0].path, b"bar/baz.txt");
867 assert_eq!(loaded.entries[1].path, b"foo.txt");
868 }
869
870 #[test]
871 fn requested_v4_writes_a_compatible_index_format() {
872 let dir = TempDir::new().unwrap();
873 let path = dir.path().join("index");
874
875 let mut idx = Index {
876 version: 4,
877 ..Index::default()
878 };
879 idx.add_or_replace(make_entry("one"));
880 idx.add_or_replace(make_entry("two/one"));
881 idx.write(&path).unwrap();
882
883 let data = fs::read(&path).unwrap();
884 assert_eq!(&data[4..8], &2u32.to_be_bytes());
885
886 let loaded = Index::load(&path).unwrap();
887 assert_eq!(loaded.entries[0].path, b"one");
888 assert_eq!(loaded.entries[1].path, b"two/one");
889 }
890}