Skip to main content

git_internal/internal/
index.rs

1//! Minimal Git index (.git/index) reader/writer that maps working tree metadata to `IndexEntry`
2//! records, including POSIX timestamp handling and hash serialization helpers.
3
4#[cfg(unix)]
5use std::os::unix::fs::MetadataExt;
6use std::{
7    collections::BTreeMap,
8    fmt::{Display, Formatter},
9    fs::{self, File},
10    io,
11    io::{BufReader, Read, Write},
12    path::{Path, PathBuf},
13    time::{SystemTime, UNIX_EPOCH},
14};
15
16use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
17
18use crate::{
19    errors::GitError,
20    hash::{ObjectHash, get_hash_kind},
21    internal::pack::wrapper::Wrapper,
22    utils::{self, HashAlgorithm},
23};
24
25/// POSIX time with seconds and nanoseconds
26#[derive(PartialEq, Eq, Debug, Clone)]
27pub struct Time {
28    seconds: u32,
29    nanos: u32,
30}
31impl Time {
32    /// Read Time from stream
33    pub fn from_stream(stream: &mut impl Read) -> Result<Self, GitError> {
34        let seconds = stream.read_u32::<BigEndian>()?;
35        let nanos = stream.read_u32::<BigEndian>()?;
36        Ok(Time { seconds, nanos })
37    }
38
39    /// Convert to SystemTime
40    #[allow(dead_code)]
41    fn to_system_time(&self) -> SystemTime {
42        UNIX_EPOCH + std::time::Duration::new(self.seconds.into(), self.nanos)
43    }
44
45    /// Create Time from SystemTime
46    pub fn from_system_time(system_time: SystemTime) -> Self {
47        match system_time.duration_since(UNIX_EPOCH) {
48            Ok(duration) => {
49                let seconds = duration
50                    .as_secs()
51                    .try_into()
52                    .expect("Time is too far in the future");
53                let nanos = duration.subsec_nanos();
54                Time { seconds, nanos }
55            }
56            Err(_) => panic!("Time is before the UNIX epoch"),
57        }
58    }
59}
60impl Display for Time {
61    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
62        write!(f, "{}:{}", self.seconds, self.nanos)
63    }
64}
65
66/// 16 bits
67#[derive(Debug)]
68pub struct Flags {
69    pub assume_valid: bool,
70    pub extended: bool,   // must be 0 in v2
71    pub stage: u8,        // 2-bit during merge
72    pub name_length: u16, // 12-bit
73}
74
75impl From<u16> for Flags {
76    fn from(flags: u16) -> Self {
77        Flags {
78            assume_valid: flags & 0x8000 != 0,
79            extended: flags & 0x4000 != 0,
80            stage: ((flags & 0x3000) >> 12) as u8,
81            name_length: flags & 0xFFF,
82        }
83    }
84}
85
86impl TryInto<u16> for &Flags {
87    type Error = &'static str;
88    fn try_into(self) -> Result<u16, Self::Error> {
89        let mut flags = 0u16;
90        if self.assume_valid {
91            flags |= 0x8000; // 16
92        }
93        if self.extended {
94            flags |= 0x4000; // 15
95        }
96        flags |= (self.stage as u16) << 12; // 13-14
97        if self.name_length > 0xFFF {
98            return Err("Name length is too long");
99        }
100        flags |= self.name_length; // 0-11
101        Ok(flags)
102    }
103}
104
105impl Flags {
106    pub fn new(name_len: u16) -> Self {
107        Flags {
108            assume_valid: true,
109            extended: false,
110            stage: 0,
111            name_length: name_len,
112        }
113    }
114}
115
116/// An entry in the Git index file.
117pub struct IndexEntry {
118    pub ctime: Time,
119    pub mtime: Time,
120    pub dev: u32,  // 0 for windows
121    pub ino: u32,  // 0 for windows
122    pub mode: u32, // 0o100644 // 4-bit object type + 3-bit unused + 9-bit unix permission
123    pub uid: u32,  // 0 for windows
124    pub gid: u32,  // 0 for windows
125    pub size: u32,
126    pub hash: ObjectHash,
127    pub flags: Flags,
128    pub name: String,
129}
130impl Display for IndexEntry {
131    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
132        write!(
133            f,
134            "IndexEntry {{ ctime: {}, mtime: {}, dev: {}, ino: {}, mode: {:o}, uid: {}, gid: {}, size: {}, hash: {}, flags: {:?}, name: {} }}",
135            self.ctime,
136            self.mtime,
137            self.dev,
138            self.ino,
139            self.mode,
140            self.uid,
141            self.gid,
142            self.size,
143            self.hash,
144            self.flags,
145            self.name
146        )
147    }
148}
149
150impl IndexEntry {
151    /** Metadata must be got by [fs::symlink_metadata] to avoid following symlink */
152    pub fn new(meta: &fs::Metadata, hash: ObjectHash, name: String) -> Self {
153        let mut entry = IndexEntry {
154            ctime: Time::from_system_time(meta.created().unwrap()),
155            mtime: Time::from_system_time(meta.modified().unwrap()),
156            dev: 0,
157            ino: 0,
158            uid: 0,
159            gid: 0,
160            size: meta.len() as u32,
161            hash,
162            flags: Flags::new(name.len() as u16),
163            name,
164            mode: 0o100644,
165        };
166        #[cfg(unix)]
167        {
168            entry.dev = meta.dev() as u32;
169            entry.ino = meta.ino() as u32;
170            entry.uid = meta.uid();
171            entry.gid = meta.gid();
172
173            entry.mode = match meta.mode() & 0o170000/* file mode */ {
174                0o100000 => {
175                    match meta.mode() & 0o111 {
176                        0 => 0o100644, // no execute permission
177                        _ => 0o100755, // with execute permission
178                    }
179                }
180                0o120000 => 0o120000, // symlink
181                _ =>  entry.mode, // keep the original mode
182            }
183        }
184        #[cfg(windows)]
185        {
186            if meta.is_symlink() {
187                entry.mode = 0o120000;
188            }
189        }
190        entry
191    }
192
193    /// - `file`: **to workdir path**
194    /// - `workdir`: absolute or relative path
195    pub fn new_from_file(file: &Path, hash: ObjectHash, workdir: &Path) -> io::Result<Self> {
196        let name = file.to_str().unwrap().to_string();
197        let file_abs = workdir.join(file);
198        let meta = fs::symlink_metadata(file_abs)?; // without following symlink
199        let index = IndexEntry::new(&meta, hash, name);
200        Ok(index)
201    }
202
203    /// Create IndexEntry from blob object
204    pub fn new_from_blob(name: String, hash: ObjectHash, size: u32) -> Self {
205        IndexEntry {
206            ctime: Time {
207                seconds: 0,
208                nanos: 0,
209            },
210            mtime: Time {
211                seconds: 0,
212                nanos: 0,
213            },
214            dev: 0,
215            ino: 0,
216            mode: 0o100644,
217            uid: 0,
218            gid: 0,
219            size,
220            hash,
221            flags: Flags::new(name.len() as u16),
222            name,
223        }
224    }
225}
226
227/// see [index-format](https://git-scm.com/docs/index-format)
228/// <br> to Working Dir relative path
229pub struct Index {
230    entries: BTreeMap<(String, u8), IndexEntry>,
231}
232
233impl Index {
234    pub fn new() -> Self {
235        Index {
236            entries: BTreeMap::new(),
237        }
238    }
239
240    fn check_header(file: &mut impl Read) -> Result<u32, GitError> {
241        let mut magic = [0; 4];
242        file.read_exact(&mut magic)?;
243        if magic != *b"DIRC" {
244            return Err(GitError::InvalidIndexHeader(
245                String::from_utf8_lossy(&magic).to_string(),
246            ));
247        }
248
249        let version = file.read_u32::<BigEndian>()?;
250        // only support v2 now
251        if version != 2 {
252            return Err(GitError::InvalidIndexHeader(version.to_string()));
253        }
254
255        let entries = file.read_u32::<BigEndian>()?;
256        Ok(entries)
257    }
258
259    pub fn size(&self) -> usize {
260        self.entries.len()
261    }
262
263    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, GitError> {
264        let file = File::open(path.as_ref())?; // read-only
265        let total_size = file.metadata()?.len();
266        let file = &mut Wrapper::new(BufReader::new(file)); // TODO move Wrapper & utils to a common module
267
268        let num = Index::check_header(file)?;
269        let mut index = Index::new();
270
271        for _ in 0..num {
272            let mut entry = IndexEntry {
273                ctime: Time::from_stream(file)?,
274                mtime: Time::from_stream(file)?,
275                dev: file.read_u32::<BigEndian>()?, //utils::read_u32_be(file)?,
276                ino: file.read_u32::<BigEndian>()?,
277                mode: file.read_u32::<BigEndian>()?,
278                uid: file.read_u32::<BigEndian>()?,
279                gid: file.read_u32::<BigEndian>()?,
280                size: file.read_u32::<BigEndian>()?,
281                hash: utils::read_sha(file)?,
282                flags: Flags::from(file.read_u16::<BigEndian>()?),
283                name: String::new(),
284            };
285            let name_len = entry.flags.name_length as usize;
286            let mut name = vec![0; name_len];
287            file.read_exact(&mut name)?;
288            // The exact encoding is undefined, but the '.' and '/' characters are encoded in 7-bit ASCII
289            entry.name =
290                String::from_utf8(name).map_err(|e| GitError::ConversionError(e.to_string()))?; // TODO check the encoding
291            index
292                .entries
293                .insert((entry.name.clone(), entry.flags.stage), entry);
294
295            // 1-8 nul bytes as necessary to pad the entry to a multiple of eight bytes
296            // while keeping the name NUL-terminated.
297            let hash_len = get_hash_kind().size();
298            let entry_len = hash_len + 2 + name_len;
299            let padding = 1 + ((8 - ((entry_len + 1) % 8)) % 8); // at least 1 byte nul
300            utils::read_bytes(file, padding)?;
301        }
302
303        // Extensions
304        while file.bytes_read() + get_hash_kind().size() < total_size as usize {
305            // The remaining bytes must be the pack checksum (size = get_hash_kind().size())
306            let sign = utils::read_bytes(file, 4)?;
307            println!(
308                "{:?}",
309                String::from_utf8(sign.clone())
310                    .map_err(|e| GitError::ConversionError(e.to_string()))?
311            );
312            // If the first byte is 'A'...'Z' the extension is optional and can be ignored.
313            if sign[0] >= b'A' && sign[0] <= b'Z' {
314                // Optional extension
315                let size = file.read_u32::<BigEndian>()?;
316                utils::read_bytes(file, size as usize)?; // Ignore the extension
317            } else {
318                // 'link' or 'sdir' extension
319                return Err(GitError::InvalidIndexFile(
320                    "Unsupported extension".to_string(),
321                ));
322            }
323        }
324
325        // check sum
326        let file_hash = file.final_hash();
327        let check_sum = utils::read_sha(file)?;
328        if file_hash != check_sum {
329            return Err(GitError::InvalidIndexFile("Check sum failed".to_string()));
330        }
331        assert_eq!(index.size(), num as usize);
332        Ok(index)
333    }
334
335    pub fn to_file(&self, path: impl AsRef<Path>) -> Result<(), GitError> {
336        let mut file = File::create(path)?;
337        let mut hash = HashAlgorithm::new();
338
339        let mut header = Vec::new();
340        header.write_all(b"DIRC")?;
341        header.write_u32::<BigEndian>(2u32)?; // version 2
342        header.write_u32::<BigEndian>(self.entries.len() as u32)?;
343        file.write_all(&header)?;
344        hash.update(&header);
345
346        for (_, entry) in self.entries.iter() {
347            let mut entry_bytes = Vec::new();
348            entry_bytes.write_u32::<BigEndian>(entry.ctime.seconds)?;
349            entry_bytes.write_u32::<BigEndian>(entry.ctime.nanos)?;
350            entry_bytes.write_u32::<BigEndian>(entry.mtime.seconds)?;
351            entry_bytes.write_u32::<BigEndian>(entry.mtime.nanos)?;
352            entry_bytes.write_u32::<BigEndian>(entry.dev)?;
353            entry_bytes.write_u32::<BigEndian>(entry.ino)?;
354            entry_bytes.write_u32::<BigEndian>(entry.mode)?;
355            entry_bytes.write_u32::<BigEndian>(entry.uid)?;
356            entry_bytes.write_u32::<BigEndian>(entry.gid)?;
357            entry_bytes.write_u32::<BigEndian>(entry.size)?;
358            entry_bytes.write_all(entry.hash.as_ref())?;
359            entry_bytes.write_u16::<BigEndian>((&entry.flags).try_into().unwrap())?;
360            entry_bytes.write_all(entry.name.as_bytes())?;
361            let hash_len = get_hash_kind().size();
362            let entry_len = hash_len + 2 + entry.name.len();
363            let padding = 1 + ((8 - ((entry_len + 1) % 8)) % 8); // at least 1 byte nul
364            entry_bytes.write_all(&vec![0; padding])?;
365            file.write_all(&entry_bytes)?;
366            hash.update(&entry_bytes);
367        }
368
369        // Extensions
370
371        // check sum
372        let file_hash =
373            ObjectHash::from_bytes(&hash.finalize()).map_err(GitError::InvalidIndexFile)?;
374        file.write_all(file_hash.as_ref())?;
375        Ok(())
376    }
377
378    pub fn refresh(&mut self, file: impl AsRef<Path>, workdir: &Path) -> Result<bool, GitError> {
379        let path = file.as_ref();
380        let name = path
381            .to_str()
382            .ok_or(GitError::InvalidPathError(format!("{path:?}")))?;
383
384        if let Some(entry) = self.entries.get_mut(&(name.to_string(), 0)) {
385            let abs_path = workdir.join(path);
386            let meta = fs::symlink_metadata(&abs_path)?;
387            // Try creation time; on error, warn and use modification time (or now)
388            let new_ctime = Time::from_system_time(Self::time_or_now(
389                "creation time",
390                &abs_path,
391                meta.created(),
392            ));
393            let new_mtime = Time::from_system_time(Self::time_or_now(
394                "modification time",
395                &abs_path,
396                meta.modified(),
397            ));
398            let new_size = meta.len() as u32;
399
400            // re-calculate SHA1/SHA256
401            let mut file = File::open(&abs_path)?;
402            let mut hasher = HashAlgorithm::new();
403            io::copy(&mut file, &mut hasher)?;
404            let new_hash = ObjectHash::from_bytes(&hasher.finalize()).unwrap();
405
406            // refresh index
407            if entry.ctime != new_ctime
408                || entry.mtime != new_mtime
409                || entry.size != new_size
410                || entry.hash != new_hash
411            {
412                entry.ctime = new_ctime;
413                entry.mtime = new_mtime;
414                entry.size = new_size;
415                entry.hash = new_hash;
416                return Ok(true);
417            }
418        }
419        Ok(false)
420    }
421
422    /// Try to get a timestamp, logging on error, and finally falling back to now.
423    fn time_or_now(what: &str, path: &Path, res: io::Result<SystemTime>) -> SystemTime {
424        match res {
425            Ok(ts) => ts,
426            Err(e) => {
427                eprintln!(
428                    "warning: failed to get {what} for {path:?}: {e}; using SystemTime::now()",
429                    what = what,
430                    path = path.display()
431                );
432                SystemTime::now()
433            }
434        }
435    }
436}
437
438impl Default for Index {
439    fn default() -> Self {
440        Self::new()
441    }
442}
443
444impl Index {
445    /// Load index. If it does not exist, return an empty index.
446    pub fn load(index_file: impl AsRef<Path>) -> Result<Self, GitError> {
447        let path = index_file.as_ref();
448        if !path.exists() {
449            return Ok(Index::new());
450        }
451        Index::from_file(path)
452    }
453
454    pub fn update(&mut self, entry: IndexEntry) {
455        self.add(entry)
456    }
457
458    pub fn add(&mut self, entry: IndexEntry) {
459        self.entries
460            .insert((entry.name.clone(), entry.flags.stage), entry);
461    }
462
463    pub fn remove(&mut self, name: &str, stage: u8) -> Option<IndexEntry> {
464        self.entries.remove(&(name.to_string(), stage))
465    }
466
467    pub fn get(&self, name: &str, stage: u8) -> Option<&IndexEntry> {
468        self.entries.get(&(name.to_string(), stage))
469    }
470
471    pub fn tracked(&self, name: &str, stage: u8) -> bool {
472        self.entries.contains_key(&(name.to_string(), stage))
473    }
474
475    pub fn get_hash(&self, file: &str, stage: u8) -> Option<ObjectHash> {
476        self.get(file, stage).map(|entry| entry.hash)
477    }
478
479    pub fn verify_hash(&self, file: &str, stage: u8, hash: &ObjectHash) -> bool {
480        let inner_hash = self.get_hash(file, stage);
481        if let Some(inner_hash) = inner_hash {
482            &inner_hash == hash
483        } else {
484            false
485        }
486    }
487    /// is file modified after last `add` (need hash to confirm content change)
488    /// - `workdir` is used to rebuild absolute file path
489    pub fn is_modified(&self, file: &str, stage: u8, workdir: &Path) -> bool {
490        if let Some(entry) = self.get(file, stage) {
491            let path_abs = workdir.join(file);
492            let meta = path_abs.symlink_metadata().unwrap();
493            // TODO more fields
494            let same = entry.ctime
495                == Time::from_system_time(meta.created().unwrap_or(SystemTime::now()))
496                && entry.mtime
497                    == Time::from_system_time(meta.modified().unwrap_or(SystemTime::now()))
498                && entry.size == meta.len() as u32;
499
500            !same
501        } else {
502            panic!("File not found in index");
503        }
504    }
505
506    /// Get all entries with the same stage
507    pub fn tracked_entries(&self, stage: u8) -> Vec<&IndexEntry> {
508        // ? should use stage or not
509        self.entries
510            .iter()
511            .filter(|(_, entry)| entry.flags.stage == stage)
512            .map(|(_, entry)| entry)
513            .collect()
514    }
515
516    /// Get all tracked files(stage = 0)
517    pub fn tracked_files(&self) -> Vec<PathBuf> {
518        self.tracked_entries(0)
519            .iter()
520            .map(|entry| PathBuf::from(&entry.name))
521            .collect()
522    }
523
524    /// Judge if the file(s) of `dir` is in the index
525    /// - false if `dir` is a file
526    pub fn contains_dir_file(&self, dir: &str) -> bool {
527        let dir = Path::new(dir);
528        self.entries.iter().any(|((name, _), _)| {
529            let path = Path::new(name);
530            path.starts_with(dir) && path != dir // TODO change to is_sub_path!
531        })
532    }
533
534    /// remove all files in `dir` from index
535    /// - do nothing if `dir` is a file
536    pub fn remove_dir_files(&mut self, dir: &str) -> Vec<String> {
537        let dir = Path::new(dir);
538        let mut removed = Vec::new();
539        self.entries.retain(|(name, _), _| {
540            let path = Path::new(name);
541            if path.starts_with(dir) && path != dir {
542                removed.push(name.clone());
543                false
544            } else {
545                true
546            }
547        });
548        removed
549    }
550
551    /// saved to index file
552    pub fn save(&self, index_file: impl AsRef<Path>) -> Result<(), GitError> {
553        self.to_file(index_file)
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use std::io::Cursor;
560
561    use super::*;
562    use crate::hash::{HashKind, set_hash_kind_for_test};
563
564    /// Test Time conversion
565    #[test]
566    fn test_time() {
567        let time = Time {
568            seconds: 0,
569            nanos: 0,
570        };
571        let system_time = time.to_system_time();
572        let new_time = Time::from_system_time(system_time);
573        assert_eq!(time, new_time);
574    }
575
576    /// Test Flags conversion
577    #[test]
578    fn test_check_header() {
579        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
580        source.push("tests/data/index/index-2");
581
582        let file = File::open(source).unwrap();
583        let entries = Index::check_header(&mut BufReader::new(file)).unwrap();
584        assert_eq!(entries, 2);
585    }
586
587    /// Test IndexEntry creation
588    #[test]
589    fn test_index() {
590        let _guard = set_hash_kind_for_test(HashKind::Sha1);
591        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
592        source.push("tests/data/index/index-760");
593
594        let index = Index::from_file(source).unwrap();
595        assert_eq!(index.size(), 760);
596        for (_, entry) in index.entries.iter() {
597            println!("{entry}");
598        }
599    }
600
601    /// Test IndexEntry creation with SHA256
602    #[test]
603    fn test_index_sha256() {
604        let _guard = set_hash_kind_for_test(HashKind::Sha256);
605        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
606        source.push("tests/data/index/index-9-256");
607
608        let index = Index::from_file(source).unwrap();
609        assert_eq!(index.size(), 9);
610        for (_, entry) in index.entries.iter() {
611            println!("{entry}");
612        }
613    }
614
615    /// Flags bit packing/unpacking covers all fields and enforces name length limit.
616    #[test]
617    fn flags_round_trip_and_length_limit() {
618        let mut flags = Flags {
619            assume_valid: true,
620            extended: true,
621            stage: 2,
622            name_length: 0x0ABC,
623        };
624        let packed: u16 = (&flags).try_into().expect("should pack");
625        let unpacked = Flags::from(packed);
626        assert_eq!(unpacked.assume_valid, flags.assume_valid);
627        assert_eq!(unpacked.extended, flags.extended);
628        assert_eq!(unpacked.stage, flags.stage);
629        assert_eq!(unpacked.name_length, flags.name_length);
630
631        flags.name_length = 0x1FFF;
632        let overflow: Result<u16, _> = (&flags).try_into();
633        assert!(overflow.is_err(), "length overflow should err");
634    }
635
636    /// IndexEntry::new_from_blob populates fields and sets flags length.
637    #[test]
638    fn index_entry_new_from_blob_populates_fields() {
639        let hash = ObjectHash::from_bytes(&[0u8; 20]).unwrap();
640        let entry = IndexEntry::new_from_blob("file.txt".to_string(), hash, 42);
641        assert_eq!(entry.name, "file.txt");
642        assert_eq!(entry.size, 42);
643        assert_eq!(entry.hash, hash);
644        assert_eq!(entry.flags.name_length, "file.txt".len() as u16);
645        assert_eq!(entry.mode, 0o100644);
646    }
647
648    /// Index container operations: add/get/tracked/dir helpers.
649    #[test]
650    fn index_add_and_query_helpers() {
651        let _guard = set_hash_kind_for_test(HashKind::Sha1);
652        let mut index = Index::new();
653        let hash = ObjectHash::from_bytes(&[1u8; 20]).unwrap();
654        let entry = IndexEntry::new_from_blob("a/b.txt".to_string(), hash, 10);
655        index.add(entry);
656
657        // get finds stage-0 by name
658        let got = index.get("a/b.txt", 0).expect("entry exists");
659        assert_eq!(got.hash, hash);
660
661        // tracked_entries/files return stage-0 paths
662        let tracked = index.tracked_entries(0);
663        assert_eq!(tracked.len(), 1);
664        let files = index.tracked_files();
665        assert_eq!(files, vec![PathBuf::from("a/b.txt")]);
666
667        // contains_dir_file true for subpath, false for exact file
668        assert!(index.contains_dir_file("a"));
669        assert!(!index.contains_dir_file("a/b.txt"));
670
671        // remove_dir_files removes under dir and returns removed names
672        let removed = index.remove_dir_files("a");
673        assert_eq!(removed, vec!["a/b.txt".to_string()]);
674        assert!(index.get("a/b.txt", 0).is_none());
675    }
676
677    /// check_header should reject bad magic/versions and accept valid header.
678    #[test]
679    fn check_header_validation() {
680        // valid header: "DIRC" + version 2 + 0 entries
681        let mut valid = Cursor::new(b"DIRC\0\0\0\x02\0\0\0\0".to_vec());
682        let entries = Index::check_header(&mut valid).expect("valid header");
683        assert_eq!(entries, 0);
684
685        // bad magic
686        let mut bad_magic = Cursor::new(b"XXXX\0\0\0\x02\0\0\0\0".to_vec());
687        assert!(Index::check_header(&mut bad_magic).is_err());
688
689        // bad version
690        let mut bad_version = Cursor::new(b"DIRC\0\0\0\x01\0\0\0\0".to_vec());
691        assert!(Index::check_header(&mut bad_version).is_err());
692    }
693
694    /// Test saving Index to file
695    #[test]
696    fn test_index_to_file() {
697        let temp_dir = tempfile::tempdir().unwrap();
698        let temp_path = temp_dir.path().join("index-760");
699
700        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
701        source.push("tests/data/index/index-760");
702
703        let index = Index::from_file(source).unwrap();
704        index.to_file(&temp_path).unwrap();
705        let new_index = Index::from_file(temp_path).unwrap();
706        assert_eq!(index.size(), new_index.size());
707    }
708
709    /// Test IndexEntry creation from file
710    #[test]
711    fn test_index_entry_create() {
712        let _guard = set_hash_kind_for_test(HashKind::Sha1);
713        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
714        source.push("Cargo.toml");
715
716        let file = Path::new(source.as_path()); // use as a normal file
717        let hash = ObjectHash::from_bytes(&[0; 20]).unwrap();
718        let workdir = Path::new("../");
719        let entry = IndexEntry::new_from_file(file, hash, workdir).unwrap();
720        println!("{entry}");
721    }
722
723    /// Test IndexEntry creation from file with SHA256
724    #[test]
725    fn test_index_entry_create_sha256() {
726        let _guard = set_hash_kind_for_test(HashKind::Sha256);
727        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
728        source.push("Cargo.toml");
729
730        let file = Path::new(source.as_path());
731        let hash = ObjectHash::from_bytes(&[0u8; 32]).unwrap();
732        let workdir = Path::new("../");
733        let entry = IndexEntry::new_from_file(file, hash, workdir).unwrap();
734        println!("{entry}");
735    }
736}