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() }