1use std::{
2 collections::BTreeSet,
3 convert::TryFrom,
4 path::{Path, PathBuf},
5 str,
6};
7
8use git_ext::{
9 ref_format::{refspec::QualifiedPattern, Qualified, RefStr, RefString},
10 Oid,
11};
12
13use crate::{
14 blob::{Blob, BlobRef},
15 diff::{Diff, FileDiff},
16 fs::{Directory, File, FileContent},
17 refs::{BranchNames, Branches, Categories, Namespaces, TagNames, Tags},
18 tree::{Entry, Tree},
19 Branch, Commit, Error, Glob, History, Namespace, Revision, Signature, Stats, Tag, ToCommit,
20};
21
22pub mod error {
24 use std::path::PathBuf;
25 use thiserror::Error;
26
27 #[derive(Debug, Error)]
28 #[non_exhaustive]
29 pub enum Repo {
30 #[error("path not found for: {0}")]
31 PathNotFound(PathBuf),
32 }
33}
34
35pub struct Repository {
39 inner: git2::Repository,
43}
44
45impl Repository {
49 pub fn open(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
55 let repo = git2::Repository::open(repo_uri)?;
56 Ok(Self { inner: repo })
57 }
58
59 pub fn discover(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
62 let repo = git2::Repository::discover(repo_uri)?;
63 Ok(Self { inner: repo })
64 }
65
66 pub fn which_namespace(&self) -> Result<Option<Namespace>, Error> {
68 self.inner
69 .namespace_bytes()
70 .map(|ns| Namespace::try_from(ns).map_err(Error::from))
71 .transpose()
72 }
73
74 pub fn switch_namespace(&self, namespace: &RefString) -> Result<(), Error> {
76 Ok(self.inner.set_namespace(namespace.as_str())?)
77 }
78
79 pub fn with_namespace<T, F>(&self, namespace: &RefString, f: F) -> Result<T, Error>
80 where
81 F: FnOnce() -> Result<T, Error>,
82 {
83 self.switch_namespace(namespace)?;
84 let res = f();
85 self.inner.remove_namespace()?;
86 res
87 }
88
89 pub fn branches<'a, G>(&'a self, pattern: G) -> Result<Branches<'a>, Error>
91 where
92 G: Into<Glob<Branch>>,
93 {
94 let pattern = pattern.into();
95 let mut branches = Branches::default();
96 for glob in pattern.globs() {
97 let namespaced = self.namespaced_pattern(glob)?;
98 let references = self.inner.references_glob(&namespaced)?;
99 branches.push(references);
100 }
101 Ok(branches)
102 }
103
104 pub fn branch_names<'a, G>(&'a self, filter: G) -> Result<BranchNames<'a>, Error>
106 where
107 G: Into<Glob<Branch>>,
108 {
109 Ok(self.branches(filter)?.names())
110 }
111
112 pub fn tags<'a>(&'a self, pattern: &Glob<Tag>) -> Result<Tags<'a>, Error> {
114 let mut tags = Tags::default();
115 for glob in pattern.globs() {
116 let namespaced = self.namespaced_pattern(glob)?;
117 let references = self.inner.references_glob(&namespaced)?;
118 tags.push(references);
119 }
120 Ok(tags)
121 }
122
123 pub fn tag_names<'a>(&'a self, filter: &Glob<Tag>) -> Result<TagNames<'a>, Error> {
125 Ok(self.tags(filter)?.names())
126 }
127
128 pub fn categories<'a>(
129 &'a self,
130 pattern: &Glob<Qualified<'_>>,
131 ) -> Result<Categories<'a>, Error> {
132 let mut cats = Categories::default();
133 for glob in pattern.globs() {
134 let namespaced = self.namespaced_pattern(glob)?;
135 let references = self.inner.references_glob(&namespaced)?;
136 cats.push(references);
137 }
138 Ok(cats)
139 }
140
141 pub fn namespaces(&self, pattern: &Glob<Namespace>) -> Result<Namespaces, Error> {
143 let mut set = BTreeSet::new();
144 for glob in pattern.globs() {
145 let new_set = self
146 .inner
147 .references_glob(glob)?
148 .map(|reference| {
149 reference
150 .map_err(Error::Git)
151 .and_then(|r| Namespace::try_from(&r).map_err(Error::from))
152 })
153 .collect::<Result<BTreeSet<Namespace>, Error>>()?;
154 set.extend(new_set);
155 }
156 Ok(Namespaces::new(set))
157 }
158
159 pub fn diff(&self, from: impl Revision, to: impl Revision) -> Result<Diff, Error> {
161 let from_commit = self.find_commit(self.object_id(&from)?)?;
162 let to_commit = self.find_commit(self.object_id(&to)?)?;
163 self.diff_commits(None, Some(&from_commit), &to_commit)
164 .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
165 }
166
167 pub fn diff_commit(&self, commit: impl ToCommit) -> Result<Diff, Error> {
173 let commit = commit
174 .to_commit(self)
175 .map_err(|err| Error::ToCommit(err.into()))?;
176 match commit.parents.first() {
177 Some(parent) => self.diff(*parent, commit.id),
178 None => self.initial_diff(commit.id),
179 }
180 }
181
182 pub fn diff_file<P: AsRef<Path>, R: Revision>(
187 &self,
188 path: &P,
189 from: R,
190 to: R,
191 ) -> Result<FileDiff, Error> {
192 let from_commit = self.find_commit(self.object_id(&from)?)?;
193 let to_commit = self.find_commit(self.object_id(&to)?)?;
194 let diff = self
195 .diff_commits(Some(path.as_ref()), Some(&from_commit), &to_commit)
196 .and_then(|diff| Diff::try_from(diff).map_err(Error::from))?;
197 let file_diff = diff
198 .into_files()
199 .pop()
200 .ok_or(error::Repo::PathNotFound(path.as_ref().to_path_buf()))?;
201 Ok(file_diff)
202 }
203
204 pub fn oid(&self, oid: &str) -> Result<Oid, Error> {
206 Ok(self.inner.revparse_single(oid)?.id().into())
207 }
208
209 pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
214 let commit = commit
215 .to_commit(self)
216 .map_err(|err| Error::ToCommit(err.into()))?;
217 let git2_commit = self.inner.find_commit((commit.id).into())?;
218 let tree = git2_commit.as_object().peel_to_tree()?;
219 Ok(Directory::root(tree.id().into()))
220 }
221
222 pub fn directory<C: ToCommit, P: AsRef<Path>>(
224 &self,
225 commit: C,
226 path: &P,
227 ) -> Result<Directory, Error> {
228 let root = self.root_dir(commit)?;
229 Ok(root.find_directory(path, self)?)
230 }
231
232 pub fn file<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<File, Error> {
234 let root = self.root_dir(commit)?;
235 Ok(root.find_file(path, self)?)
236 }
237
238 pub fn tree<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Tree, Error> {
240 let commit = commit
241 .to_commit(self)
242 .map_err(|e| Error::ToCommit(e.into()))?;
243 let dir = self.directory(commit.id, path)?;
244 let mut entries = dir
245 .entries(self)?
246 .map(|en| {
247 let name = en.name().to_string();
248 let path = en.path();
249 Ok(Entry::new(name, path, en.into(), commit.clone()))
250 })
251 .collect::<Result<Vec<Entry>, Error>>()?;
252 entries.sort();
253
254 Ok(Tree::new(
255 dir.id(),
256 entries,
257 commit,
258 path.as_ref().to_path_buf(),
259 ))
260 }
261
262 pub fn blob<'a, C: ToCommit, P: AsRef<Path>>(
264 &'a self,
265 commit: C,
266 path: &P,
267 ) -> Result<Blob<BlobRef<'a>>, Error> {
268 let commit = commit
269 .to_commit(self)
270 .map_err(|e| Error::ToCommit(e.into()))?;
271 let file = self.file(commit.id, path)?;
272 let last_commit = self
273 .last_commit(path, commit)?
274 .ok_or_else(|| error::Repo::PathNotFound(path.as_ref().to_path_buf()))?;
275 let git2_blob = self.find_blob(file.id())?;
276 Ok(Blob::<BlobRef<'a>>::new(file.id(), git2_blob, last_commit))
277 }
278
279 pub fn blob_ref(&self, oid: Oid) -> Result<BlobRef<'_>, Error> {
280 Ok(BlobRef {
281 inner: self.find_blob(oid)?,
282 })
283 }
284
285 pub fn last_commit<P, C>(&self, path: &P, rev: C) -> Result<Option<Commit>, Error>
288 where
289 P: AsRef<Path>,
290 C: ToCommit,
291 {
292 let history = self.history(rev)?;
293 history.by_path(path).next().transpose()
294 }
295
296 pub fn commit<R: Revision>(&self, rev: R) -> Result<Commit, Error> {
298 rev.to_commit(self)
299 }
300
301 pub fn stats(&self) -> Result<Stats, Error> {
304 self.stats_from(&self.head()?)
305 }
306
307 pub fn stats_from<R>(&self, rev: &R) -> Result<Stats, Error>
310 where
311 R: Revision,
312 {
313 let branches = self.branches(Glob::all_heads())?.count();
314 let mut history = self.history(rev)?;
315 let (commits, contributors) = history.try_fold(
316 (0, BTreeSet::new()),
317 |(commits, mut contributors), commit| {
318 let commit = commit?;
319 contributors.insert((commit.author.name, commit.author.email));
320 Ok::<_, Error>((commits + 1, contributors))
321 },
322 )?;
323 Ok(Stats {
324 branches,
325 commits,
326 contributors: contributors.len(),
327 })
328 }
329
330 pub fn get_commit_file<'a, P, R>(&'a self, rev: &R, path: &P) -> Result<FileContent<'a>, Error>
334 where
335 P: AsRef<Path>,
336 R: Revision,
337 {
338 let path = path.as_ref();
339 let id = self.object_id(rev)?;
340 let commit = self.find_commit(id)?;
341 let tree = commit.tree()?;
342 let entry = tree.get_path(path)?;
343 let object = entry.to_object(&self.inner)?;
344 let blob = object
345 .into_blob()
346 .map_err(|_| error::Repo::PathNotFound(path.to_path_buf()))?;
347 Ok(FileContent::new(blob))
348 }
349
350 pub fn head(&self) -> Result<Oid, Error> {
352 let head = self.inner.head()?;
353 let head_commit = head.peel_to_commit()?;
354 Ok(head_commit.id().into())
355 }
356
357 pub fn extract_signature(
364 &self,
365 commit: impl ToCommit,
366 field: Option<&str>,
367 ) -> Result<Option<Signature>, Error> {
368 let commit = commit
373 .to_commit(self)
374 .map_err(|e| Error::ToCommit(e.into()))?;
375
376 match self.inner.extract_signature(&commit.id, field) {
377 Err(error) => {
378 if error.code() == git2::ErrorCode::NotFound {
379 Ok(None)
380 } else {
381 Err(error.into())
382 }
383 }
384 Ok(sig) => Ok(Some(Signature::from(sig.0))),
385 }
386 }
387
388 pub fn history<'a, C: ToCommit>(&'a self, head: C) -> Result<History<'a>, Error> {
390 History::new(self, head)
391 }
392
393 pub fn revision_branches(
395 &self,
396 rev: impl Revision,
397 glob: Glob<Branch>,
398 ) -> Result<Vec<Branch>, Error> {
399 let oid = self.object_id(&rev)?;
400 let mut contained_branches = vec![];
401 for branch in self.branches(glob)? {
402 let branch = branch?;
403 let namespaced = self.namespaced_refname(&branch.refname())?;
404 let reference = self.inner.find_reference(namespaced.as_str())?;
405 if self.reachable_from(&reference, &oid)? {
406 contained_branches.push(branch);
407 }
408 }
409
410 Ok(contained_branches)
411 }
412}
413
414impl Repository {
418 pub(crate) fn is_bare(&self) -> bool {
419 self.inner.is_bare()
420 }
421
422 pub(crate) fn find_submodule<'a>(
423 &'a self,
424 name: &str,
425 ) -> Result<git2::Submodule<'a>, git2::Error> {
426 self.inner.find_submodule(name)
427 }
428
429 pub(crate) fn find_blob(&self, oid: Oid) -> Result<git2::Blob<'_>, git2::Error> {
430 self.inner.find_blob(oid.into())
431 }
432
433 pub(crate) fn find_commit(&self, oid: Oid) -> Result<git2::Commit<'_>, git2::Error> {
434 self.inner.find_commit(oid.into())
435 }
436
437 pub(crate) fn find_tree(&self, oid: Oid) -> Result<git2::Tree<'_>, git2::Error> {
438 self.inner.find_tree(oid.into())
439 }
440
441 pub(crate) fn refname_to_id<R>(&self, name: &R) -> Result<Oid, git2::Error>
442 where
443 R: AsRef<RefStr>,
444 {
445 self.inner
446 .refname_to_id(name.as_ref().as_str())
447 .map(Oid::from)
448 }
449
450 pub(crate) fn revwalk(&self) -> Result<git2::Revwalk<'_>, git2::Error> {
451 self.inner.revwalk()
452 }
453
454 pub(super) fn object_id<R: Revision>(&self, r: &R) -> Result<Oid, Error> {
455 r.object_id(self).map_err(|err| Error::Revision(err.into()))
456 }
457
458 fn initial_diff<R: Revision>(&self, rev: R) -> Result<Diff, Error> {
460 let commit = self.find_commit(self.object_id(&rev)?)?;
461 self.diff_commits(None, None, &commit)
462 .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
463 }
464
465 fn reachable_from(&self, reference: &git2::Reference, oid: &Oid) -> Result<bool, Error> {
466 let git2_oid = (*oid).into();
467 let other = reference.peel_to_commit()?.id();
468 let is_descendant = self.inner.graph_descendant_of(other, git2_oid)?;
469
470 Ok(other == git2_oid || is_descendant)
471 }
472
473 pub(crate) fn diff_commit_and_parents<P>(
474 &self,
475 path: &P,
476 commit: &git2::Commit,
477 ) -> Result<Option<PathBuf>, Error>
478 where
479 P: AsRef<Path>,
480 {
481 let mut parents = commit.parents();
482
483 let diff = self.diff_commits(Some(path.as_ref()), parents.next().as_ref(), commit)?;
484 if let Some(_delta) = diff.deltas().next() {
485 Ok(Some(path.as_ref().to_path_buf()))
486 } else {
487 Ok(None)
488 }
489 }
490
491 fn diff_commits<'a>(
502 &'a self,
503 path: Option<&Path>,
504 from: Option<&git2::Commit>,
505 to: &git2::Commit,
506 ) -> Result<git2::Diff<'a>, Error> {
507 let new_tree = to.tree()?;
508 let old_tree = from.map_or(Ok(None), |c| c.tree().map(Some))?;
509
510 let mut opts = git2::DiffOptions::new();
511 if let Some(path) = path {
512 opts.pathspec(path.to_string_lossy().to_string());
513 opts.skip_binary_check(false);
514 }
515
516 let mut diff =
517 self.inner
518 .diff_tree_to_tree(old_tree.as_ref(), Some(&new_tree), Some(&mut opts))?;
519
520 let mut find_opts = git2::DiffFindOptions::new();
522 find_opts.renames(true);
523 find_opts.copies(true);
524 diff.find_similar(Some(&mut find_opts))?;
525
526 Ok(diff)
527 }
528
529 pub(crate) fn namespaced_refname<'a>(
531 &'a self,
532 refname: &Qualified<'a>,
533 ) -> Result<Qualified<'a>, Error> {
534 let fullname = match self.which_namespace()? {
535 Some(namespace) => namespace.to_namespaced(refname).into_qualified(),
536 None => refname.clone(),
537 };
538 Ok(fullname)
539 }
540
541 fn namespaced_pattern<'a>(
543 &'a self,
544 refname: &QualifiedPattern<'a>,
545 ) -> Result<QualifiedPattern<'a>, Error> {
546 let fullname = match self.which_namespace()? {
547 Some(namespace) => namespace.to_namespaced_pattern(refname).into_qualified(),
548 None => refname.clone(),
549 };
550 Ok(fullname)
551 }
552}
553
554impl From<git2::Repository> for Repository {
555 fn from(repo: git2::Repository) -> Self {
556 Repository { inner: repo }
557 }
558}
559
560impl std::fmt::Debug for Repository {
561 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
562 write!(f, ".git")
563 }
564}