gix_commitgraph/
init.rs

1use std::{
2    io::{BufRead, BufReader},
3    path::{Path, PathBuf},
4};
5
6use crate::{file, File, Graph, MAX_COMMITS};
7
8/// The error returned by initializations functions like [`Graph::at()`].
9#[derive(thiserror::Error, Debug)]
10#[allow(missing_docs)]
11pub enum Error {
12    #[error("{}", .path.display())]
13    File {
14        #[source]
15        err: file::Error,
16        path: PathBuf,
17    },
18    #[error("Commit-graph files mismatch: '{}' uses hash {hash1:?}, but '{}' uses hash {hash2:?}", .path1.display(), .path2.display())]
19    HashVersionMismatch {
20        path1: PathBuf,
21        hash1: gix_hash::Kind,
22        path2: PathBuf,
23        hash2: gix_hash::Kind,
24    },
25    #[error("Did not find any files that look like commit graphs at '{}'", .0.display())]
26    InvalidPath(PathBuf),
27    #[error("Could not open commit-graph file at '{}'", .path.display())]
28    Io {
29        #[source]
30        err: std::io::Error,
31        path: PathBuf,
32    },
33    #[error("Commit-graph files contain {0} commits altogether, but only {MAX_COMMITS} commits are allowed")]
34    TooManyCommits(u64),
35}
36
37/// Instantiate a `Graph` from various sources.
38impl Graph {
39    /// Instantiate a commit graph from `path` which may be a directory containing graph files or the graph file itself.
40    pub fn at(path: &Path) -> Result<Self, Error> {
41        Self::try_from(path)
42    }
43
44    /// Instantiate a commit graph from the directory containing all of its files.
45    pub fn from_commit_graphs_dir(path: &Path) -> Result<Self, Error> {
46        let commit_graphs_dir = path;
47        let chain_file_path = commit_graphs_dir.join("commit-graph-chain");
48        let chain_file = std::fs::File::open(&chain_file_path).map_err(|e| Error::Io {
49            err: e,
50            path: chain_file_path.clone(),
51        })?;
52        let mut files = Vec::new();
53        for line in BufReader::new(chain_file).lines() {
54            let hash = line.map_err(|e| Error::Io {
55                err: e,
56                path: chain_file_path.clone(),
57            })?;
58            let graph_file_path = commit_graphs_dir.join(format!("graph-{hash}.graph"));
59            files.push(File::at(&graph_file_path).map_err(|e| Error::File {
60                err: e,
61                path: graph_file_path.clone(),
62            })?);
63        }
64        Self::new(files)
65    }
66
67    /// Instantiate a commit graph from a `.git/objects/info/commit-graph` or
68    /// `.git/objects/info/commit-graphs/graph-*.graph` file.
69    pub fn from_file(path: &Path) -> Result<Self, Error> {
70        let file = File::at(path).map_err(|e| Error::File {
71            err: e,
72            path: path.to_owned(),
73        })?;
74        Self::new(vec![file])
75    }
76
77    /// Instantiate a commit graph from an `.git/objects/info` directory.
78    pub fn from_info_dir(info_dir: &Path) -> Result<Self, Error> {
79        Self::from_file(&info_dir.join("commit-graph"))
80            .or_else(|_| Self::from_commit_graphs_dir(&info_dir.join("commit-graphs")))
81    }
82
83    /// Create a new commit graph from a list of `files`.
84    pub fn new(files: Vec<File>) -> Result<Self, Error> {
85        let num_commits: u64 = files.iter().map(|f| u64::from(f.num_commits())).sum();
86        if num_commits > u64::from(MAX_COMMITS) {
87            return Err(Error::TooManyCommits(num_commits));
88        }
89
90        for window in files.windows(2) {
91            let f1 = &window[0];
92            let f2 = &window[1];
93            if f1.object_hash() != f2.object_hash() {
94                return Err(Error::HashVersionMismatch {
95                    path1: f1.path().to_owned(),
96                    hash1: f1.object_hash(),
97                    path2: f2.path().to_owned(),
98                    hash2: f2.object_hash(),
99                });
100            }
101        }
102
103        Ok(Self { files })
104    }
105}
106
107impl TryFrom<&Path> for Graph {
108    type Error = Error;
109
110    fn try_from(path: &Path) -> Result<Self, Self::Error> {
111        if path.is_file() {
112            // Assume we are looking at `.git/objects/info/commit-graph` or
113            // `.git/objects/info/commit-graphs/graph-*.graph`.
114            Self::from_file(path)
115        } else if path.is_dir() {
116            if path.join("commit-graph-chain").is_file() {
117                Self::from_commit_graphs_dir(path)
118            } else {
119                Self::from_info_dir(path)
120            }
121        } else {
122            Err(Error::InvalidPath(path.to_owned()))
123        }
124    }
125}