Skip to main content

git_sprout/
scratch_index.rs

1// ABOUTME: Writes the index that tells git which paths are already on disk and current.
2// ABOUTME: Git replaces it with the real index at the end, so it carries no extensions.
3
4use std::path::Path;
5
6use gix_hash::ObjectId;
7use gix_index::entry::{Flags, Mode, Stat};
8use gix_index::{write, File, State};
9
10/// One verified clone, described the way the index describes it.
11#[derive(Debug, Clone)]
12pub struct Record {
13    pub path: Vec<u8>,
14    /// The mode from the tree, which is what git would have written.
15    pub mode: u32,
16    pub oid: ObjectId,
17    /// The stat data of the clone itself, so git sees the path as up to date.
18    pub stat: Stat,
19}
20
21/// The error a scratch index write can fail with. Every one of them is survivable:
22/// without the index git simply checks the whole tree out.
23#[derive(Debug)]
24pub enum Error {
25    UnknownMode(u32),
26    Write(gix_index::file::write::Error),
27}
28
29/// The only index version this writes. Git keeps whatever version it reads, so a
30/// repository asking for any other version is left to check itself out.
31pub const SUPPORTED_VERSION: u32 = 2;
32
33/// The lowest and highest index versions git accepts.
34const VERSION_RANGE: std::ops::RangeInclusive<u32> = 2..=4;
35
36/// The version git would give a fresh index, following the same order git does.
37///
38/// This matters because git keeps the version of the index it reads: a scratch index in
39/// the wrong version would decide the version of the final index too, which is observable.
40/// `git worktree add --no-checkout` writes no index at all, so the answer has to come from
41/// the environment and the configuration rather than from a file.
42pub fn default_version(
43    environment: Option<&str>,
44    configured: Option<&str>,
45    many_files: bool,
46) -> u32 {
47    let requested = environment
48        .or(configured)
49        .and_then(|value| value.trim().parse::<u32>().ok())
50        .or_else(|| many_files.then_some(4));
51    match requested {
52        Some(version) if VERSION_RANGE.contains(&version) => version,
53        _ => SUPPORTED_VERSION,
54    }
55}
56
57/// Writes `records` as the index at `index_path`, replacing whatever is there.
58pub fn write(
59    index_path: &Path,
60    object_hash: gix_hash::Kind,
61    records: &[Record],
62) -> Result<(), Error> {
63    let mut state = State::new(object_hash);
64    for record in records {
65        let mode = Mode::from_bits(record.mode).ok_or(Error::UnknownMode(record.mode))?;
66        state.dangerously_push_entry(
67            record.stat,
68            record.oid,
69            Flags::empty(),
70            mode,
71            record.path.as_slice().into(),
72        );
73    }
74    state.sort_entries();
75    let mut file = File::from_state(state, index_path);
76    file.write(write::Options {
77        extensions: write::Extensions::None,
78        skip_hash: false,
79    })
80    .map_err(Error::Write)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn a_plain_repository_gets_version_two() {
89        assert_eq!(default_version(None, None, false), 2);
90    }
91
92    #[test]
93    fn configuration_and_environment_choose_the_version() {
94        assert_eq!(default_version(None, Some("4"), false), 4);
95        assert_eq!(default_version(Some("3"), Some("4"), false), 3);
96    }
97
98    #[test]
99    fn many_files_asks_for_version_four() {
100        assert_eq!(default_version(None, None, true), 4);
101        assert_eq!(default_version(None, Some("2"), true), 2);
102    }
103
104    #[test]
105    fn an_impossible_version_falls_back_to_the_default() {
106        assert_eq!(default_version(None, Some("9"), false), 2);
107        assert_eq!(default_version(None, Some("nonsense"), false), 2);
108    }
109}