1use std::{
2 io::{BufRead, BufReader},
3 path::{Path, PathBuf},
4};
5
6use crate::{file, File, Graph, MAX_COMMITS};
7
8#[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
37impl Graph {
39 pub fn at(path: &Path) -> Result<Self, Error> {
41 Self::try_from(path)
42 }
43
44 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 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 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 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 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}