diff --git a/src/git/repository.rs b/src/git/repository.rs new file mode 100644 index 0000000..13a0b30 --- /dev/null +++ b/src/git/repository.rs @@ -0,0 +1,167 @@ +use anyhow::{anyhow, Context, Result}; +use git2::{BranchType, Repository as Git2Repository}; +use std::path::PathBuf; + +pub struct Repository { + path: PathBuf, + repo: Git2Repository, +} + +impl Repository { + pub fn new(path: Option) -> Result { + let path = path.unwrap_or_else(|| PathBuf::from(".")); + let repo = Git2Repository::open(&path) + .with_context(|| format!("Failed to open git repository at '{}'", path.display()))?; + Ok(Self { path, repo }) + } + + pub fn path(&self) -> &PathBuf { + &self.path + } + + pub fn workdir(&self) -> Option { + self.repo.workdir().map(|p| p.to_path_buf()) + } + + pub fn is_empty(&self) -> bool { + self.repo.is_empty() + } + + pub fn head(&self) -> Result { + self.repo.head().with_context(|| "Failed to get HEAD reference") + } + + pub fn head_name(&self) -> Result> { + let head = self.head()?; + Ok(head.shorthand().map(|s| s.to_string())) + } + + pub fn branch_name(&self) -> Result> { + let head = self.head()?; + if head.is_branch() { + return Ok(head.shorthand().map(|s| s.to_string())); + } + Ok(None) + } + + pub fn remote_url(&self) -> Result> { + if let Some(remote) = self.repo.find_remote("origin").ok() { + return Ok(remote.url().map(|s| s.to_string())); + } + Ok(None) + } + + pub fn is_detached(&self) -> bool { + self.repo.head_detached().unwrap_or(false) + } + + pub fn branches(&self) -> Result> { + let mut branches = Vec::new(); + self.repo.branches(None, |_| true)?.iter().for_each(|b| { + if let Ok(Some(branch)) = b { + if let Some(name) = branch.name() { + if let Some(name) = name { + branches.push(name.to_string()); + } + } + } + }); + Ok(branches) + } + + pub fn local_branches(&self) -> Result> { + let mut branches = Vec::new(); + self.repo.branches(Some(BranchType::Local), |_| true)?.iter().for_each(|b| { + if let Ok(Some(branch)) = b { + if let Some(name) = branch.name() { + if let Some(name) = name { + branches.push(name.to_string()); + } + } + } + }); + Ok(branches) + } + + pub fn remote_branches(&self) -> Result> { + let mut branches = Vec::new(); + self.repo.branches(Some(BranchType::Remote), |_| true)?.iter().for_each(|b| { + if let Ok(Some(branch)) = b { + if let Some(name) = branch.name() { + if let Some(name) = name { + branches.push(name.to_string()); + } + } + } + }); + Ok(branches) + } + + pub fn commit_count(&self) -> Result { + let mut count = 0; + let mut revwalk = self.repo.revwalk()?; + revwalk.set_sorting(git2::Sort::NONE)?; + revwalk.push_head()?; + for _ in revwalk { + count += 1; + } + Ok(count) + } + + pub fn first_commit(&self) -> Result> { + let mut revwalk = self.repo.revwalk()?; + revwalk.set_sorting(git2::Sort::TIME)?; + revwalk.push_head()?; + if let Ok(Some(oid)) = revwalk.nth(0) { + return Ok(self.repo.find_commit(oid).ok()); + } + Ok(None) + } + + pub fn get_commit(&self, oid: git2::Oid) -> Result { + self.repo.find_commit(oid).with_context(|| "Failed to find commit") + } + + pub fn diff_tree_to_tree( + &self, + old_tree: &git2::Tree, + new_tree: &git2::Tree, + ) -> Result { + self.repo + .diff_tree_to_tree(Some(old_tree), Some(new_tree), None) + .with_context(|| "Failed to create diff between trees") + } + + pub fn diff_commit_to_parent(&self, commit: &git2::Commit) -> Result { + let parent = if commit.parent_count() > 0 { + Some(commit.parent(0)?) + } else { + None + }; + + let parent_tree = parent.as_ref().map(|p| p.tree()).transpose()?; + let commit_tree = commit.tree()?; + + self.repo + .diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), None) + .with_context(|| "Failed to create diff for commit") + } + + pub fn diff_find_similar(&self, diff: &mut git2::Diff) -> Result<()> { + diff.find_similar(None)?; + Ok(()) + } + + pub fn stats(&self, diff: &git2::Diff) -> Result { + diff.stats_with_context(git2::DiffStatsFormat::FULL, 0) + .with_context(|| "Failed to get diff stats") + } + + pub fn raw(&self) -> &Git2Repository { + &self.repo + } +} + +pub fn is_git_repository(path: &PathBuf) -> bool { + Git2Repository::open(path).is_ok() +}