use crate::error::{GitError, Result};
use git2::{BranchType, Cred, ObjectType, PushOptions, RemoteCallbacks, Repository};
use std::path::{Path, PathBuf};
pub struct GitRepo {
repo: Repository,
}
pub struct CommitInfo {
pub sha: String,
pub message: String,
pub author: String,
pub timestamp: i64,
}
impl GitRepo {
pub fn open() -> Result<Self> {
let repo = Repository::open_from_env()
.or_else(|_| Repository::discover("."))
.map_err(|_| GitError::NotARepository)?;
Ok(Self { repo })
}
pub fn open_at(path: &Path) -> Result<Self> {
let repo = Repository::open(path).map_err(|_| GitError::NotARepository)?;
Ok(Self { repo })
}
pub fn current_branch(&self) -> Result<String> {
let head = self
.repo
.head()
.map_err(|e| GitError::GitOperation(format!("Failed to get HEAD: {}", e)))?;
let branch_name = head
.shorthand()
.ok_or_else(|| GitError::GitOperation("Failed to get branch name".to_string()))?;
Ok(branch_name.to_string())
}
pub fn is_working_tree_clean(&self) -> Result<bool> {
let statuses = self
.repo
.statuses(None)
.map_err(|e| GitError::GitOperation(format!("Failed to get status: {}", e)))?;
Ok(statuses.is_empty())
}
pub fn get_uncommitted_changes(&self) -> Result<Vec<String>> {
let statuses = self
.repo
.statuses(None)
.map_err(|e| GitError::GitOperation(format!("Failed to get status: {}", e)))?;
let mut changes = Vec::new();
for entry in statuses.iter() {
if let Some(path) = entry.path() {
changes.push(path.to_string());
}
}
Ok(changes)
}
pub fn check_remote_sync(&self, branch: &str, remote: &str) -> Result<(usize, usize)> {
let local_branch = self
.repo
.find_branch(branch, BranchType::Local)
.map_err(|e| GitError::GitOperation(format!("Failed to find local branch: {}", e)))?;
let local_oid = local_branch.get().target().ok_or_else(|| {
GitError::GitOperation("Failed to get local branch target".to_string())
})?;
let remote_branch_name = format!("{}/{}", remote, branch);
let remote_branch = match self
.repo
.find_branch(&remote_branch_name, BranchType::Remote)
{
Ok(b) => b,
Err(_) => return Ok((0, 0)),
};
let remote_oid = remote_branch.get().target().ok_or_else(|| {
GitError::GitOperation("Failed to get remote branch target".to_string())
})?;
let (ahead, behind) = self
.repo
.graph_ahead_behind(local_oid, remote_oid)
.map_err(|e| GitError::GitOperation(format!("Failed to compare branches: {}", e)))?;
Ok((ahead, behind))
}
pub fn get_latest_tag(&self, pattern: &str) -> Result<Option<String>> {
let tags = self
.repo
.tag_names(Some(pattern))
.map_err(|e| GitError::GitOperation(format!("Failed to get tags: {}", e)))?;
let mut tag_list: Vec<String> = tags.iter().filter_map(|t| t.map(String::from)).collect();
tag_list.sort_by(|a, b| {
let a_ver = semver::Version::parse(a.trim_start_matches('v'));
let b_ver = semver::Version::parse(b.trim_start_matches('v'));
match (a_ver, b_ver) {
(Ok(av), Ok(bv)) => bv.cmp(&av),
_ => b.cmp(a),
}
});
Ok(tag_list.first().cloned())
}
pub fn get_commits_between(&self, from: &str, to: &str) -> Result<Vec<CommitInfo>> {
let from_obj = self
.repo
.revparse_single(from)
.map_err(|e| GitError::GitOperation(format!("Failed to parse from ref: {}", e)))?;
let to_obj = self
.repo
.revparse_single(to)
.map_err(|e| GitError::GitOperation(format!("Failed to parse to ref: {}", e)))?;
let mut revwalk = self
.repo
.revwalk()
.map_err(|e| GitError::GitOperation(format!("Failed to create revwalk: {}", e)))?;
revwalk
.push(to_obj.id())
.map_err(|e| GitError::GitOperation(format!("Failed to push to revwalk: {}", e)))?;
revwalk
.hide(from_obj.id())
.map_err(|e| GitError::GitOperation(format!("Failed to hide from revwalk: {}", e)))?;
let mut commits = Vec::new();
for oid in revwalk {
let oid = oid
.map_err(|e| GitError::GitOperation(format!("Failed to get commit oid: {}", e)))?;
let commit = self
.repo
.find_commit(oid)
.map_err(|e| GitError::GitOperation(format!("Failed to find commit: {}", e)))?;
commits.push(CommitInfo {
sha: commit.id().to_string(),
message: commit.message().unwrap_or("").to_string(),
author: commit.author().name().unwrap_or("").to_string(),
timestamp: commit.time().seconds(),
});
}
Ok(commits)
}
pub fn commit(&self, message: &str) -> Result<String> {
let signature = self
.repo
.signature()
.map_err(|e| GitError::CommitFailed(format!("Failed to get signature: {}", e)))?;
let mut index = self
.repo
.index()
.map_err(|e| GitError::CommitFailed(format!("Failed to get index: {}", e)))?;
let tree_oid = index
.write_tree()
.map_err(|e| GitError::CommitFailed(format!("Failed to write tree: {}", e)))?;
let tree = self
.repo
.find_tree(tree_oid)
.map_err(|e| GitError::CommitFailed(format!("Failed to find tree: {}", e)))?;
let parent_commit = self
.repo
.head()
.map_err(|e| GitError::CommitFailed(format!("Failed to get HEAD: {}", e)))?
.peel_to_commit()
.map_err(|e| GitError::CommitFailed(format!("Failed to peel to commit: {}", e)))?;
let oid = self
.repo
.commit(
Some("HEAD"),
&signature,
&signature,
message,
&tree,
&[&parent_commit],
)
.map_err(|e| GitError::CommitFailed(format!("Failed to create commit: {}", e)))?;
Ok(oid.to_string())
}
pub fn add(&self, path: &Path) -> Result<()> {
let mut index = self
.repo
.index()
.map_err(|e| GitError::GitOperation(format!("Failed to get index: {}", e)))?;
index
.add_path(path)
.map_err(|e| GitError::GitOperation(format!("Failed to add path: {}", e)))?;
index
.write()
.map_err(|e| GitError::GitOperation(format!("Failed to write index: {}", e)))?;
Ok(())
}
pub fn create_tag(&self, name: &str, message: &str) -> Result<()> {
let obj = self
.repo
.head()
.map_err(|e| GitError::TagFailed(format!("Failed to get HEAD: {}", e)))?
.peel(ObjectType::Commit)
.map_err(|e| GitError::TagFailed(format!("Failed to peel to commit: {}", e)))?;
let signature = self
.repo
.signature()
.map_err(|e| GitError::TagFailed(format!("Failed to get signature: {}", e)))?;
self.repo
.tag(name, &obj, &signature, message, false)
.map_err(|e| GitError::TagFailed(format!("Failed to create tag: {}", e)))?;
Ok(())
}
pub fn push(&self, remote: &str, refspec: &str) -> Result<()> {
let mut remote = self
.repo
.find_remote(remote)
.map_err(|_| GitError::RemoteNotFound(remote.to_string()))?;
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_url, username_from_url, _allowed_types| {
Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
});
let mut push_options = PushOptions::new();
push_options.remote_callbacks(callbacks);
remote
.push(&[refspec], Some(&mut push_options))
.map_err(|e| GitError::PushFailed(format!("Failed to push: {}", e)))?;
Ok(())
}
pub fn current_commit_sha(&self) -> Result<String> {
let head = self
.repo
.head()
.map_err(|e| GitError::GitOperation(format!("Failed to get HEAD: {}", e)))?;
let commit = head
.peel_to_commit()
.map_err(|e| GitError::GitOperation(format!("Failed to peel to commit: {}", e)))?;
Ok(commit.id().to_string())
}
pub fn root_path(&self) -> PathBuf {
self.repo
.workdir()
.unwrap_or_else(|| Path::new("."))
.to_path_buf()
}
pub fn remote_exists(&self, remote: &str) -> bool {
self.repo.find_remote(remote).is_ok()
}
pub fn remote_url(&self, remote: &str) -> Result<String> {
let remote = self
.repo
.find_remote(remote)
.map_err(|_| GitError::RemoteNotFound(remote.to_string()))?;
let url = remote
.url()
.ok_or_else(|| GitError::GitOperation("Remote URL not found".to_string()))?;
Ok(url.to_string())
}
pub fn tag_exists(&self, tag_name: &str) -> Result<bool> {
match self.repo.find_reference(&format!("refs/tags/{}", tag_name)) {
Ok(_) => Ok(true),
Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(false),
Err(e) => Err(GitError::GitOperation(format!("Failed to check tag: {}", e)).into()),
}
}
pub fn checkout_file(&self, path: &Path) -> Result<()> {
let head = self
.repo
.head()
.map_err(|e| GitError::GitOperation(format!("Failed to get HEAD: {}", e)))?;
let tree = head
.peel_to_tree()
.map_err(|e| GitError::GitOperation(format!("Failed to get tree: {}", e)))?;
let mut checkout_builder = git2::build::CheckoutBuilder::new();
checkout_builder.path(path);
checkout_builder.force();
self.repo
.checkout_tree(tree.as_object(), Some(&mut checkout_builder))
.map_err(|e| GitError::GitOperation(format!("Failed to checkout file: {}", e)))?;
Ok(())
}
pub fn delete_tag(&self, tag_name: &str) -> Result<()> {
let refname = format!("refs/tags/{}", tag_name);
let mut reference = self
.repo
.find_reference(&refname)
.map_err(|e| GitError::GitOperation(format!("Tag not found: {}", e)))?;
reference
.delete()
.map_err(|e| GitError::GitOperation(format!("Failed to delete tag: {}", e)))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_open_nonexistent_repo() {
let dir = tempdir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let result = GitRepo::open();
assert!(result.is_err());
}
}