1use crate::utils::paths::clean_path;
4use anyhow::Context as _;
5use git2::{Commit, Repository, Sort};
6use std::{
7 fmt,
8 path::{Path, PathBuf},
9};
10
11pub const GIT_REQUEST_NOT_FOUND: &str = "Git object doesn't exist";
13
14pub struct Repo {
18 pub archive_path: String,
20 pub path: PathBuf,
22 pub org: String,
24 pub name: String,
26 pub repo: Repository,
28}
29
30#[derive(Debug)]
32pub struct Blob {
33 pub content: Vec<u8>,
35 pub path: String,
37}
38
39impl fmt::Debug for Repo {
40 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
41 write!(
42 formatter,
43 "Repo for {}/{} in the archive at {}",
44 self.org, self.name, self.archive_path
45 )
46 }
47}
48
49#[expect(
50 clippy::missing_trait_methods,
51 clippy::unwrap_used,
52 reason = "Expect to have git repo on disk"
53)]
54impl Clone for Repo {
55 fn clone(&self) -> Self {
56 Self {
57 archive_path: self.archive_path.clone(),
58 org: self.org.clone(),
59 name: self.name.clone(),
60 path: self.path.clone(),
61 repo: Repository::open(self.path.clone()).unwrap(),
62 }
63 }
64}
65
66impl Repo {
67 fn find(&self, query: &str) -> anyhow::Result<Vec<u8>> {
69 tracing::trace!(query, "Git reverse parse search");
70 let obj = self.repo.revparse_single(query)?;
71 let blob = obj.as_blob().context("Couldn't cast Git object to blob")?;
72 Ok(blob.content().to_owned())
73 }
74
75 pub fn find_blob(
81 archive_path: &Path,
82 namespace: &str,
83 name: &str,
84 remainder: &str,
85 commitish: &str,
86 ) -> anyhow::Result<Blob> {
87 let repo = Self::new(archive_path, namespace, name)?;
88 let blob_path = clean_path(remainder);
89 let blob = repo.get_bytes_at_path(commitish, &blob_path)?;
90 Ok(blob)
91 }
92 pub fn new(archive_path: &Path, org: &str, name: &str) -> anyhow::Result<Self> {
100 let archive_path_str = archive_path.to_string_lossy();
101 tracing::trace!(org, name, "Creating new Repo at {archive_path_str}");
102 let repo_path = format!("{archive_path_str}/{org}/{name}");
103 Ok(Self {
104 archive_path: archive_path_str.into(),
105 org: org.into(),
106 name: name.into(),
107 path: PathBuf::from(repo_path.clone()),
108 repo: Repository::open(repo_path)?,
109 })
110 }
111
112 pub fn from_path(path: &Path) -> anyhow::Result<Self> {
119 let components: Vec<&str> = path
120 .components()
121 .filter_map(|component| component.as_os_str().to_str())
122 .collect();
123 if components.len() < 2 {
124 anyhow::bail!("Path must contain at least org and name");
125 }
126 let name = (*components
127 .last()
128 .ok_or_else(|| anyhow::anyhow!("Missing repo name"))?)
129 .to_owned();
130 let org = (*components
131 .get(components.len() - 2)
132 .ok_or_else(|| anyhow::anyhow!("Missing repo org"))?)
133 .to_owned();
134 let archive_path_slice = components.get(..components.len() - 2).ok_or_else(|| {
135 anyhow::anyhow!("Path does not contain enough components for archive_path")
136 })?;
137 let archive_path = archive_path_slice.iter().collect::<PathBuf>();
138 Self::new(&archive_path, &org, &name)
139 }
140
141 pub fn get_bytes_at_path(&self, commitish: &str, path: &str) -> anyhow::Result<Blob> {
156 let base_revision = format!("{commitish}:{path}");
157 for postfix in ["", "/index.html", ".html", "index.html"] {
158 let query = format!("{base_revision}{postfix}");
159 let blob = self.find(&query);
160 if let Ok(content) = blob {
161 let filepath = format!("{path}{postfix}");
162 tracing::trace!(query, "Found Git object");
163 return Ok(Blob {
164 content,
165 path: filepath,
166 });
167 }
168 }
169 tracing::debug!(base_revision, "Couldn't find requested Git object");
170 anyhow::bail!(GIT_REQUEST_NOT_FOUND)
171 }
172
173 pub fn iter_commits(&self) -> anyhow::Result<impl Iterator<Item = Commit>> {
179 let mut revwalk = self.repo.revwalk()?;
180 revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::REVERSE)?;
181 revwalk.push_head()?;
182 Ok(revwalk
183 .filter_map(|found_oid| {
184 let oid = found_oid.ok()?;
185 self.repo.find_commit(oid).ok()
186 })
187 .collect::<Vec<Commit>>()
188 .into_iter())
189 }
190}