1use std::{
19 collections::BTreeSet,
20 convert::TryFrom,
21 path::{Path, PathBuf},
22 str,
23};
24
25use git_ext::{
26 ref_format::{refspec::QualifiedPattern, Qualified, RefStr, RefString},
27 Oid,
28};
29
30use crate::{
31 blob::{Blob, BlobRef},
32 diff::{Diff, FileDiff},
33 fs::{Directory, File, FileContent},
34 refs::{BranchNames, Branches, Categories, Namespaces, TagNames, Tags},
35 tree::{Entry, Tree},
36 Branch, Commit, Error, Glob, History, Namespace, Revision, Signature, Stats, Tag, ToCommit,
37};
38
39pub mod error {
41 use std::path::PathBuf;
42 use thiserror::Error;
43
44 #[derive(Debug, Error)]
45 #[non_exhaustive]
46 pub enum Repo {
47 #[error("path not found for: {0}")]
48 PathNotFound(PathBuf),
49 }
50}
51
52pub struct Repository {
56 inner: git2::Repository,
60}
61
62impl Repository {
66 pub fn open(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
72 let repo = git2::Repository::open(repo_uri)?;
73 Ok(Self { inner: repo })
74 }
75
76 pub fn discover(repo_uri: impl AsRef<std::path::Path>) -> Result<Self, Error> {
79 let repo = git2::Repository::discover(repo_uri)?;
80 Ok(Self { inner: repo })
81 }
82
83 pub fn which_namespace(&self) -> Result<Option<Namespace>, Error> {
85 self.inner
86 .namespace_bytes()
87 .map(|ns| Namespace::try_from(ns).map_err(Error::from))
88 .transpose()
89 }
90
91 pub fn switch_namespace(&self, namespace: &RefString) -> Result<(), Error> {
93 Ok(self.inner.set_namespace(namespace.as_str())?)
94 }
95
96 pub fn with_namespace<T, F>(&self, namespace: &RefString, f: F) -> Result<T, Error>
97 where
98 F: FnOnce() -> Result<T, Error>,
99 {
100 self.switch_namespace(namespace)?;
101 let res = f();
102 self.inner.remove_namespace()?;
103 res
104 }
105
106 pub fn branches<G>(&self, pattern: G) -> Result<Branches, Error>
108 where
109 G: Into<Glob<Branch>>,
110 {
111 let pattern = pattern.into();
112 let mut branches = Branches::default();
113 for glob in pattern.globs() {
114 let namespaced = self.namespaced_pattern(glob)?;
115 let references = self.inner.references_glob(&namespaced)?;
116 branches.push(references);
117 }
118 Ok(branches)
119 }
120
121 pub fn branch_names<G>(&self, filter: G) -> Result<BranchNames, Error>
123 where
124 G: Into<Glob<Branch>>,
125 {
126 Ok(self.branches(filter)?.names())
127 }
128
129 pub fn tags(&self, pattern: &Glob<Tag>) -> Result<Tags, Error> {
131 let mut tags = Tags::default();
132 for glob in pattern.globs() {
133 let namespaced = self.namespaced_pattern(glob)?;
134 let references = self.inner.references_glob(&namespaced)?;
135 tags.push(references);
136 }
137 Ok(tags)
138 }
139
140 pub fn tag_names(&self, filter: &Glob<Tag>) -> Result<TagNames, Error> {
142 Ok(self.tags(filter)?.names())
143 }
144
145 pub fn categories(&self, pattern: &Glob<Qualified<'_>>) -> Result<Categories, Error> {
146 let mut cats = Categories::default();
147 for glob in pattern.globs() {
148 let namespaced = self.namespaced_pattern(glob)?;
149 let references = self.inner.references_glob(&namespaced)?;
150 cats.push(references);
151 }
152 Ok(cats)
153 }
154
155 pub fn namespaces(&self, pattern: &Glob<Namespace>) -> Result<Namespaces, Error> {
157 let mut set = BTreeSet::new();
158 for glob in pattern.globs() {
159 let new_set = self
160 .inner
161 .references_glob(glob)?
162 .map(|reference| {
163 reference
164 .map_err(Error::Git)
165 .and_then(|r| Namespace::try_from(&r).map_err(Error::from))
166 })
167 .collect::<Result<BTreeSet<Namespace>, Error>>()?;
168 set.extend(new_set);
169 }
170 Ok(Namespaces::new(set))
171 }
172
173 pub fn diff(&self, from: impl Revision, to: impl Revision) -> Result<Diff, Error> {
175 let from_commit = self.find_commit(self.object_id(&from)?)?;
176 let to_commit = self.find_commit(self.object_id(&to)?)?;
177 self.diff_commits(None, Some(&from_commit), &to_commit)
178 .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
179 }
180
181 pub fn diff_commit(&self, commit: impl ToCommit) -> Result<Diff, Error> {
187 let commit = commit
188 .to_commit(self)
189 .map_err(|err| Error::ToCommit(err.into()))?;
190 match commit.parents.first() {
191 Some(parent) => self.diff(*parent, commit.id),
192 None => self.initial_diff(commit.id),
193 }
194 }
195
196 pub fn diff_file<P: AsRef<Path>, R: Revision>(
201 &self,
202 path: &P,
203 from: R,
204 to: R,
205 ) -> Result<FileDiff, Error> {
206 let from_commit = self.find_commit(self.object_id(&from)?)?;
207 let to_commit = self.find_commit(self.object_id(&to)?)?;
208 let diff = self
209 .diff_commits(Some(path.as_ref()), Some(&from_commit), &to_commit)
210 .and_then(|diff| Diff::try_from(diff).map_err(Error::from))?;
211 let file_diff = diff
212 .into_files()
213 .pop()
214 .ok_or(error::Repo::PathNotFound(path.as_ref().to_path_buf()))?;
215 Ok(file_diff)
216 }
217
218 pub fn oid(&self, oid: &str) -> Result<Oid, Error> {
220 Ok(self.inner.revparse_single(oid)?.id().into())
221 }
222
223 pub fn root_dir<C: ToCommit>(&self, commit: C) -> Result<Directory, Error> {
228 let commit = commit
229 .to_commit(self)
230 .map_err(|err| Error::ToCommit(err.into()))?;
231 let git2_commit = self.inner.find_commit((commit.id).into())?;
232 let tree = git2_commit.as_object().peel_to_tree()?;
233 Ok(Directory::root(tree.id().into()))
234 }
235
236 pub fn directory<C: ToCommit, P: AsRef<Path>>(
238 &self,
239 commit: C,
240 path: &P,
241 ) -> Result<Directory, Error> {
242 let root = self.root_dir(commit)?;
243 Ok(root.find_directory(path, self)?)
244 }
245
246 pub fn file<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<File, Error> {
248 let root = self.root_dir(commit)?;
249 Ok(root.find_file(path, self)?)
250 }
251
252 pub fn tree<C: ToCommit, P: AsRef<Path>>(&self, commit: C, path: &P) -> Result<Tree, Error> {
254 let commit = commit
255 .to_commit(self)
256 .map_err(|e| Error::ToCommit(e.into()))?;
257 let dir = self.directory(commit.id, path)?;
258 let mut entries = dir
259 .entries(self)?
260 .map(|en| {
261 let name = en.name().to_string();
262 let path = en.path();
263 let commit = self
264 .last_commit(&path, commit.id)?
265 .ok_or(error::Repo::PathNotFound(path))?;
266 Ok(Entry::new(name, en.into(), commit))
267 })
268 .collect::<Result<Vec<Entry>, Error>>()?;
269 entries.sort();
270
271 let last_commit = self
272 .last_commit(path, commit)?
273 .ok_or_else(|| error::Repo::PathNotFound(path.as_ref().to_path_buf()))?;
274 Ok(Tree::new(dir.id(), entries, last_commit))
275 }
276
277 pub fn blob<'a, C: ToCommit, P: AsRef<Path>>(
279 &'a self,
280 commit: C,
281 path: &P,
282 ) -> Result<Blob<BlobRef<'a>>, Error> {
283 let commit = commit
284 .to_commit(self)
285 .map_err(|e| Error::ToCommit(e.into()))?;
286 let file = self.file(commit.id, path)?;
287 let last_commit = self
288 .last_commit(path, commit)?
289 .ok_or_else(|| error::Repo::PathNotFound(path.as_ref().to_path_buf()))?;
290 let git2_blob = self.find_blob(file.id())?;
291 Ok(Blob::<BlobRef<'a>>::new(file.id(), git2_blob, last_commit))
292 }
293
294 pub fn blob_ref(&self, oid: Oid) -> Result<BlobRef<'_>, Error> {
295 Ok(BlobRef {
296 inner: self.find_blob(oid)?,
297 })
298 }
299
300 pub fn last_commit<P, C>(&self, path: &P, rev: C) -> Result<Option<Commit>, Error>
303 where
304 P: AsRef<Path>,
305 C: ToCommit,
306 {
307 let history = self.history(rev)?;
308 history.by_path(path).next().transpose()
309 }
310
311 pub fn commit<R: Revision>(&self, rev: R) -> Result<Commit, Error> {
313 rev.to_commit(self)
314 }
315
316 pub fn stats(&self) -> Result<Stats, Error> {
319 self.stats_from(&self.head()?)
320 }
321
322 pub fn stats_from<R>(&self, rev: &R) -> Result<Stats, Error>
325 where
326 R: Revision,
327 {
328 let branches = self.branches(Glob::all_heads())?.count();
329 let mut history = self.history(rev)?;
330 let (commits, contributors) = history.try_fold(
331 (0, BTreeSet::new()),
332 |(commits, mut contributors), commit| {
333 let commit = commit?;
334 contributors.insert((commit.author.name, commit.author.email));
335 Ok::<_, Error>((commits + 1, contributors))
336 },
337 )?;
338 Ok(Stats {
339 branches,
340 commits,
341 contributors: contributors.len(),
342 })
343 }
344
345 pub fn get_commit_file<P, R>(&self, rev: &R, path: &P) -> Result<FileContent, Error>
349 where
350 P: AsRef<Path>,
351 R: Revision,
352 {
353 let path = path.as_ref();
354 let id = self.object_id(rev)?;
355 let commit = self.find_commit(id)?;
356 let tree = commit.tree()?;
357 let entry = tree.get_path(path)?;
358 let object = entry.to_object(&self.inner)?;
359 let blob = object
360 .into_blob()
361 .map_err(|_| error::Repo::PathNotFound(path.to_path_buf()))?;
362 Ok(FileContent::new(blob))
363 }
364
365 pub fn head(&self) -> Result<Oid, Error> {
367 let head = self.inner.head()?;
368 let head_commit = head.peel_to_commit()?;
369 Ok(head_commit.id().into())
370 }
371
372 pub fn extract_signature(
379 &self,
380 commit: impl ToCommit,
381 field: Option<&str>,
382 ) -> Result<Option<Signature>, Error> {
383 let commit = commit
388 .to_commit(self)
389 .map_err(|e| Error::ToCommit(e.into()))?;
390
391 match self.inner.extract_signature(&commit.id, field) {
392 Err(error) => {
393 if error.code() == git2::ErrorCode::NotFound {
394 Ok(None)
395 } else {
396 Err(error.into())
397 }
398 }
399 Ok(sig) => Ok(Some(Signature::from(sig.0))),
400 }
401 }
402
403 pub fn history<C: ToCommit>(&self, head: C) -> Result<History, Error> {
405 History::new(self, head)
406 }
407
408 pub fn revision_branches(
410 &self,
411 rev: impl Revision,
412 glob: Glob<Branch>,
413 ) -> Result<Vec<Branch>, Error> {
414 let oid = self.object_id(&rev)?;
415 let mut contained_branches = vec![];
416 for branch in self.branches(glob)? {
417 let branch = branch?;
418 let namespaced = self.namespaced_refname(&branch.refname())?;
419 let reference = self.inner.find_reference(namespaced.as_str())?;
420 if self.reachable_from(&reference, &oid)? {
421 contained_branches.push(branch);
422 }
423 }
424
425 Ok(contained_branches)
426 }
427}
428
429impl Repository {
433 pub(crate) fn is_bare(&self) -> bool {
434 self.inner.is_bare()
435 }
436
437 pub(crate) fn find_submodule(&self, name: &str) -> Result<git2::Submodule, git2::Error> {
438 self.inner.find_submodule(name)
439 }
440
441 pub(crate) fn find_blob(&self, oid: Oid) -> Result<git2::Blob<'_>, git2::Error> {
442 self.inner.find_blob(oid.into())
443 }
444
445 pub(crate) fn find_commit(&self, oid: Oid) -> Result<git2::Commit<'_>, git2::Error> {
446 self.inner.find_commit(oid.into())
447 }
448
449 pub(crate) fn find_tree(&self, oid: Oid) -> Result<git2::Tree<'_>, git2::Error> {
450 self.inner.find_tree(oid.into())
451 }
452
453 pub(crate) fn refname_to_id<R>(&self, name: &R) -> Result<Oid, git2::Error>
454 where
455 R: AsRef<RefStr>,
456 {
457 self.inner
458 .refname_to_id(name.as_ref().as_str())
459 .map(Oid::from)
460 }
461
462 pub(crate) fn revwalk(&self) -> Result<git2::Revwalk<'_>, git2::Error> {
463 self.inner.revwalk()
464 }
465
466 pub(super) fn object_id<R: Revision>(&self, r: &R) -> Result<Oid, Error> {
467 r.object_id(self).map_err(|err| Error::Revision(err.into()))
468 }
469
470 fn initial_diff<R: Revision>(&self, rev: R) -> Result<Diff, Error> {
472 let commit = self.find_commit(self.object_id(&rev)?)?;
473 self.diff_commits(None, None, &commit)
474 .and_then(|diff| Diff::try_from(diff).map_err(Error::from))
475 }
476
477 fn reachable_from(&self, reference: &git2::Reference, oid: &Oid) -> Result<bool, Error> {
478 let git2_oid = (*oid).into();
479 let other = reference.peel_to_commit()?.id();
480 let is_descendant = self.inner.graph_descendant_of(other, git2_oid)?;
481
482 Ok(other == git2_oid || is_descendant)
483 }
484
485 pub(crate) fn diff_commit_and_parents<P>(
486 &self,
487 path: &P,
488 commit: &git2::Commit,
489 ) -> Result<Option<PathBuf>, Error>
490 where
491 P: AsRef<Path>,
492 {
493 let mut parents = commit.parents();
494
495 let diff = self.diff_commits(Some(path.as_ref()), parents.next().as_ref(), commit)?;
496 if let Some(_delta) = diff.deltas().next() {
497 Ok(Some(path.as_ref().to_path_buf()))
498 } else {
499 Ok(None)
500 }
501 }
502
503 fn diff_commits(
514 &self,
515 path: Option<&Path>,
516 from: Option<&git2::Commit>,
517 to: &git2::Commit,
518 ) -> Result<git2::Diff, Error> {
519 let new_tree = to.tree()?;
520 let old_tree = from.map_or(Ok(None), |c| c.tree().map(Some))?;
521
522 let mut opts = git2::DiffOptions::new();
523 if let Some(path) = path {
524 opts.pathspec(path.to_string_lossy().to_string());
525 opts.skip_binary_check(false);
526 }
527
528 let mut diff =
529 self.inner
530 .diff_tree_to_tree(old_tree.as_ref(), Some(&new_tree), Some(&mut opts))?;
531
532 let mut find_opts = git2::DiffFindOptions::new();
534 find_opts.renames(true);
535 find_opts.copies(true);
536 diff.find_similar(Some(&mut find_opts))?;
537
538 Ok(diff)
539 }
540
541 pub(crate) fn namespaced_refname<'a>(
543 &'a self,
544 refname: &Qualified<'a>,
545 ) -> Result<Qualified<'a>, Error> {
546 let fullname = match self.which_namespace()? {
547 Some(namespace) => namespace.to_namespaced(refname).into_qualified(),
548 None => refname.clone(),
549 };
550 Ok(fullname)
551 }
552
553 fn namespaced_pattern<'a>(
555 &'a self,
556 refname: &QualifiedPattern<'a>,
557 ) -> Result<QualifiedPattern<'a>, Error> {
558 let fullname = match self.which_namespace()? {
559 Some(namespace) => namespace.to_namespaced_pattern(refname).into_qualified(),
560 None => refname.clone(),
561 };
562 Ok(fullname)
563 }
564}
565
566impl From<git2::Repository> for Repository {
567 fn from(repo: git2::Repository) -> Self {
568 Repository { inner: repo }
569 }
570}
571
572impl std::fmt::Debug for Repository {
573 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574 write!(f, ".git")
575 }
576}