1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use git2::{Oid, Signature};
7use parking_lot::Mutex;
8
9use crate::{
10 commit_diff_loader::CommitDiffLoader,
11 errors::{GitError, RepositoryLoadKind},
12 Commit,
13 CommitDiff,
14 CommitDiffLoaderOptions,
15 Config,
16 Reference,
17};
18
19#[derive(Clone)]
21pub struct Repository {
22 repository: Arc<Mutex<git2::Repository>>,
23}
24
25impl Repository {
26 #[inline]
33 pub fn open_from_env() -> Result<Self, GitError> {
34 let repository = git2::Repository::open_from_env().map_err(|e| {
35 GitError::RepositoryLoad {
36 kind: RepositoryLoadKind::Environment,
37 cause: e,
38 }
39 })?;
40 Ok(Self {
41 repository: Arc::new(Mutex::new(repository)),
42 })
43 }
44
45 #[inline]
50 pub fn open_from_path(path: &Path) -> Result<Self, GitError> {
51 let repository = git2::Repository::open(path).map_err(|e| {
52 GitError::RepositoryLoad {
53 kind: RepositoryLoadKind::Path,
54 cause: e,
55 }
56 })?;
57 Ok(Self {
58 repository: Arc::new(Mutex::new(repository)),
59 })
60 }
61
62 #[inline]
67 pub fn load_config(&self) -> Result<Config, GitError> {
68 self.repository
69 .lock()
70 .config()
71 .map_err(|e| GitError::ConfigLoad { cause: e })
72 }
73
74 #[inline]
79 pub fn load_commit_diff(&self, hash: &str, config: &CommitDiffLoaderOptions) -> Result<CommitDiff, GitError> {
80 let oid = self
81 .repository
82 .lock()
83 .revparse_single(hash)
84 .map_err(|e| GitError::CommitLoad { cause: e })?
85 .id();
86 let diff_loader_repository = Arc::clone(&self.repository);
87 let loader = CommitDiffLoader::new(diff_loader_repository, config);
88 Ok(loader
90 .load_from_hash(oid)
91 .map_err(|e| GitError::CommitLoad { cause: e })?
92 .remove(0))
93 }
94
95 #[inline]
100 pub fn find_reference(&self, reference: &str) -> Result<Reference, GitError> {
101 let repo = self.repository.lock();
102 let git2_reference = repo
103 .find_reference(reference)
104 .map_err(|e| GitError::ReferenceNotFound { cause: e })?;
105 Ok(Reference::from(&git2_reference))
106 }
107
108 #[inline]
113 pub fn find_commit(&self, reference: &str) -> Result<Commit, GitError> {
114 let repo = self.repository.lock();
115 let git2_reference = repo
116 .find_reference(reference)
117 .map_err(|e| GitError::ReferenceNotFound { cause: e })?;
118 Commit::try_from(&git2_reference)
119 }
120
121 pub(crate) fn repo_path(&self) -> PathBuf {
122 self.repository.lock().path().to_path_buf()
123 }
124
125 pub(crate) fn head_id(&self, head_name: &str) -> Result<Oid, git2::Error> {
126 let repo = self.repository.lock();
127 let ref_name = format!("refs/heads/{head_name}");
128 let revision = repo.revparse_single(ref_name.as_str())?;
129 Ok(revision.id())
130 }
131
132 pub(crate) fn commit_id_from_ref(&self, reference: &str) -> Result<Oid, git2::Error> {
133 let repo = self.repository.lock();
134 let commit = repo.find_reference(reference)?.peel_to_commit()?;
135 Ok(commit.id())
136 }
137
138 pub(crate) fn add_path_to_index(&self, path: &Path) -> Result<(), git2::Error> {
139 let repo = self.repository.lock();
140 let mut index = repo.index()?;
141 index.add_path(path)
142 }
143
144 pub(crate) fn remove_path_from_index(&self, path: &Path) -> Result<(), git2::Error> {
145 let repo = self.repository.lock();
146 let mut index = repo.index()?;
147 index.remove_path(path)
148 }
149
150 pub(crate) fn create_commit_on_index(
151 &self,
152 reference: &str,
153 author: &Signature<'_>,
154 committer: &Signature<'_>,
155 message: &str,
156 ) -> Result<(), git2::Error> {
157 let repo = self.repository.lock();
158 let tree = repo.find_tree(repo.index()?.write_tree()?)?;
159 let head = repo.find_reference(reference)?.peel_to_commit()?;
160 _ = repo.commit(Some("HEAD"), author, committer, message, &tree, &[&head])?;
161 Ok(())
162 }
163
164 #[cfg(test)]
165 pub(crate) fn repository(&self) -> Arc<Mutex<git2::Repository>> {
166 Arc::clone(&self.repository)
167 }
168}
169
170impl From<git2::Repository> for Repository {
171 #[inline]
172 fn from(repository: git2::Repository) -> Self {
173 Self {
174 repository: Arc::new(Mutex::new(repository)),
175 }
176 }
177}
178
179impl std::fmt::Debug for Repository {
180 #[inline]
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
182 f.debug_struct("Repository")
183 .field("[path]", &self.repository.lock().path())
184 .finish()
185 }
186}
187
188#[cfg(all(unix, test))]
190mod tests {
191 use std::env::set_var;
192
193 use claim::assert_ok;
194 use git2::{ErrorClass, ErrorCode};
195 use testutils::assert_err_eq;
196
197 use super::*;
198 use crate::testutil::{commit_id_from_ref, create_commit, with_temp_bare_repository, with_temp_repository};
199
200 #[test]
201 #[serial_test::serial]
202 fn open_from_env() {
203 let path = Path::new(env!("CARGO_MANIFEST_DIR"))
204 .join("test")
205 .join("fixtures")
206 .join("simple");
207 set_var("GIT_DIR", path.to_str().unwrap());
208 assert_ok!(Repository::open_from_env());
209 }
210
211 #[test]
212 #[serial_test::serial]
213 fn open_from_env_error() {
214 let path = Path::new(env!("CARGO_MANIFEST_DIR"))
215 .join("test")
216 .join("fixtures")
217 .join("does-not-exist");
218 set_var("GIT_DIR", path.to_str().unwrap());
219 assert_err_eq!(Repository::open_from_env(), GitError::RepositoryLoad {
220 kind: RepositoryLoadKind::Environment,
221 cause: git2::Error::new(
222 ErrorCode::NotFound,
223 ErrorClass::Os,
224 format!(
225 "failed to resolve path '{}': No such file or directory",
226 path.to_string_lossy()
227 )
228 ),
229 });
230 }
231
232 #[test]
233 fn open_from_path() {
234 let path = Path::new(env!("CARGO_MANIFEST_DIR"))
235 .join("test")
236 .join("fixtures")
237 .join("simple");
238 assert_ok!(Repository::open_from_path(&path));
239 }
240
241 #[test]
242 fn open_from_path_error() {
243 let path = Path::new(env!("CARGO_MANIFEST_DIR"))
244 .join("test")
245 .join("fixtures")
246 .join("does-not-exist");
247 assert_err_eq!(Repository::open_from_path(&path), GitError::RepositoryLoad {
248 kind: RepositoryLoadKind::Path,
249 cause: git2::Error::new(
250 ErrorCode::NotFound,
251 ErrorClass::Os,
252 format!(
253 "failed to resolve path '{}': No such file or directory",
254 path.to_string_lossy()
255 )
256 ),
257 });
258 }
259
260 #[test]
261 fn load_config() {
262 with_temp_bare_repository(|repo| {
263 assert_ok!(repo.load_config());
264 });
265 }
266
267 #[test]
268 fn load_commit_diff() {
269 with_temp_repository(|repository| {
270 create_commit(&repository, None);
271 let id = commit_id_from_ref(&repository, "refs/heads/main");
272 assert_ok!(repository.load_commit_diff(id.to_string().as_str(), &CommitDiffLoaderOptions::new()));
273 });
274 }
275
276 #[test]
277 fn load_commit_diff_with_non_commit() {
278 with_temp_repository(|repository| {
279 let blob_ref = {
280 let git2_repository = repository.repository();
281 let git2_lock = git2_repository.lock();
282 let blob = git2_lock.blob(b"foo").unwrap();
283 _ = git2_lock.reference("refs/blob", blob, false, "blob").unwrap();
284 blob.to_string()
285 };
286
287 assert_err_eq!(
288 repository.load_commit_diff(blob_ref.as_str(), &CommitDiffLoaderOptions::new()),
289 GitError::CommitLoad {
290 cause: git2::Error::new(
291 ErrorCode::NotFound,
292 ErrorClass::Invalid,
293 "the requested type does not match the type in the ODB",
294 ),
295 }
296 );
297 });
298 }
299
300 #[test]
301 fn find_reference() {
302 with_temp_repository(|repository| {
303 assert_ok!(repository.find_reference("refs/heads/main"));
304 });
305 }
306
307 #[test]
308 fn find_reference_error() {
309 with_temp_repository(|repository| {
310 assert_err_eq!(
311 repository.find_reference("refs/heads/invalid"),
312 GitError::ReferenceNotFound {
313 cause: git2::Error::new(
314 ErrorCode::NotFound,
315 ErrorClass::Reference,
316 "reference 'refs/heads/invalid' not found",
317 ),
318 }
319 );
320 });
321 }
322
323 #[test]
324 fn find_commit() {
325 with_temp_repository(|repository| {
326 assert_ok!(repository.find_commit("refs/heads/main"));
327 });
328 }
329
330 #[test]
331 fn find_commit_error() {
332 with_temp_repository(|repository| {
333 assert_err_eq!(
334 repository.find_commit("refs/heads/invalid"),
335 GitError::ReferenceNotFound {
336 cause: git2::Error::new(
337 ErrorCode::NotFound,
338 ErrorClass::Reference,
339 "reference 'refs/heads/invalid' not found",
340 ),
341 }
342 );
343 });
344 }
345
346 #[test]
347 fn fmt() {
348 with_temp_bare_repository(|repository| {
349 let formatted = format!("{repository:?}");
350 let path = repository.repo_path().canonicalize().unwrap();
351 assert_eq!(
352 formatted,
353 format!("Repository {{ [path]: \"{}/\" }}", path.to_str().unwrap())
354 );
355 });
356 }
357}