git_commitgraph/graph/
init.rs

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