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