~james/core

core/crates/gix-commitgraph/src/init.rs -rw-r--r-- 5.5 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 14 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use crate::{BloomFilterSettings, File, Graph, MAX_COMMITS};
use gix_error::{ErrorExt, Exn, Message, ResultExt, message};
use std::{
    io::{BufRead, BufReader},
    path::Path,
};

/// Instantiate a `Graph` from various sources.
impl Graph {
    /// Instantiate a commit graph from `path` which may be a directory containing graph files or the graph file itself.
    pub fn at(path: &Path) -> Result<Self, Exn<Message>> {
        Self::try_from(path)
    }

    /// Instantiate a commit graph from the directory containing all of its files.
    ///
    /// Loading stops at the first layer that can't be opened or parsed, whose checksum doesn't match its
    /// name in the chain, or whose `BASE` chunk doesn't list the layers below it in chain order, keeping only the
    /// layers loaded so far. It's an error if no layer could be loaded.
    pub fn from_commit_graphs_dir(path: &Path) -> Result<Self, Exn<Message>> {
        let commit_graphs_dir = path;
        let chain_file_path = commit_graphs_dir.join("commit-graph-chain");
        let chain_file = std::fs::File::open(&chain_file_path).or_raise(|| {
            message!(
                "Could not open commit-graph chain file at '{}'",
                chain_file_path.display()
            )
        })?;
        let mut files = Vec::new();
        for line in BufReader::new(chain_file).lines() {
            let hash = line.or_raise(|| {
                message!(
                    "Could not read from commit-graph file at '{}'",
                    chain_file_path.display()
                )
            })?;
            let Ok(id) = gix_hash::ObjectId::from_hex(hash.as_bytes()) else {
                break;
            };
            let Ok(file) = File::at(commit_graphs_dir.join(format!("graph-{hash}.graph"))) else {
                break;
            };
            if file.checksum() != id || !base_graphs_match(&file, &files) {
                break;
            }

            files.push(file);
        }
        Ok(Self::new(files)?)
    }

    /// Instantiate a commit graph from a `.git/objects/info/commit-graph` or
    /// `.git/objects/info/commit-graphs/graph-*.graph` file.
    pub fn from_file(path: &Path) -> Result<Self, Exn<Message>> {
        let file = File::at(path)
            .or_raise(|| message!("Could not open commit-graph file at '{}'", path.display()))?;
        Ok(Self::new(vec![file])?)
    }

    /// Instantiate a commit graph from an `.git/objects/info` directory.
    pub fn from_info_dir(info_dir: &Path) -> Result<Self, Exn<Message>> {
        Self::from_file(&info_dir.join("commit-graph"))
            .or_else(|_| Self::from_commit_graphs_dir(&info_dir.join("commit-graphs")))
    }

    /// Create a new commit graph from a list of `files`.
    pub fn new(files: Vec<File>) -> Result<Self, Message> {
        let mut files = nonempty::NonEmpty::from_vec(files)
            .ok_or_else(|| message!("Commit-graph must contain at least one file"))?;
        let num_commits: u64 = files.iter().map(|f| u64::from(f.num_commits())).sum();
        if num_commits > u64::from(MAX_COMMITS) {
            return Err(message!(
                "Commit-graph files contain {num_commits} commits altogether, but only {MAX_COMMITS} commits are allowed"
            ));
        }

        let mut f1 = files.first();
        for f2 in files.tail() {
            if f1.object_hash() != f2.object_hash() {
                return Err(message!(
                    "Commit-graph files mismatch: '{path1}' uses hash {hash1:?}, but '{path2}' uses hash {hash2:?}",
                    path1 = f1.path().display(),
                    hash1 = f1.object_hash(),
                    path2 = f2.path().display(),
                    hash2 = f2.object_hash(),
                ));
            }
            f1 = f2;
        }

        disable_incompatible_bloom_layers(&mut files);
        if !files.iter().all(File::has_generation_data) {
            files.iter_mut().for_each(File::disable_generation_data);
        }

        Ok(Self { files })
    }
}

/// Return `true` if `file`'s `BASE` chunk lists the layers `below` it in chain order.
fn base_graphs_match(file: &File, below: &[File]) -> bool {
    usize::from(file.base_graph_count()) >= below.len()
        && file
            .iter_base_graph_ids()
            .zip(below)
            .all(|(base, below)| base == below.checksum())
}

fn disable_incompatible_bloom_layers(files: &mut nonempty::NonEmpty<File>) {
    let mut preferred: Option<BloomFilterSettings> = None;
    for file in files.iter_mut().rev() {
        let Some(settings) = file.bloom_filter_settings else {
            continue;
        };
        if preferred.is_none() {
            preferred = Some(settings);
            continue;
        }
        if preferred != Some(settings) {
            file.clear_bloom_filters();
        }
    }
}

impl TryFrom<&Path> for Graph {
    type Error = Exn<Message>;

    fn try_from(path: &Path) -> Result<Self, Self::Error> {
        if path.is_file() {
            // Assume we are looking at `.git/objects/info/commit-graph` or
            // `.git/objects/info/commit-graphs/graph-*.graph`.
            Self::from_file(path)
        } else if path.is_dir() {
            if path.join("commit-graph-chain").is_file() {
                Self::from_commit_graphs_dir(path)
            } else {
                Self::from_info_dir(path)
            }
        } else {
            Err(message!(
                "Did not find any files that look like commit graphs at '{}'",
                path.display()
            )
            .raise())
        }
    }
}