use crate::errors::GitError;
use crate::git::cli::GitCli;
use std::sync::Arc;
pub struct GitService {
cli: Arc<GitCli>,
}
trait ToGitError<T> {
fn to_git_error(self) -> Result<T, GitError>;
}
impl<T> ToGitError<T> for Result<T, anyhow::Error> {
fn to_git_error(self) -> Result<T, GitError> {
self.map_err(GitError::OperationError)
}
}
impl GitService {
pub fn new() -> Self {
Self {
cli: Arc::new(GitCli::new()),
}
}
pub fn status_porcelain(&self, path: &str) -> Result<String, GitError> {
self.cli.status_porcelain(path).to_git_error()
}
pub fn stage_file(&self, repo_path: &str, path: &str) -> Result<(), GitError> {
self.cli.stage_file(repo_path, path).to_git_error()
}
pub fn unstage_file(&self, repo_path: &str, path: &str) -> Result<(), GitError> {
self.cli.unstage_file(repo_path, path).to_git_error()
}
pub fn current_branch(&self, repo_path: &str) -> Result<String, GitError> {
self.cli.current_branch(repo_path).to_git_error()
}
pub fn branches(&self, repo_path: &str) -> Result<Vec<crate::app::state::BranchEntry>, GitError> {
self.cli.branches(repo_path).to_git_error()
}
pub fn branch_names(&self, repo_path: &str) -> Result<Vec<String>, GitError> {
let branches = self.branches(repo_path)?;
Ok(branches.into_iter().map(|b| b.name).collect())
}
pub fn ahead_behind(&self, repo_path: &str) -> Result<(i32, i32), GitError> {
self.cli.ahead_behind(repo_path).to_git_error()
}
pub fn log(&self, repo_path: &str) -> Result<Vec<crate::app::state::CommitEntry>, GitError> {
self.cli.log(repo_path).to_git_error()
}
pub fn show_commit(&self, repo_path: &str, hash: &str) -> Result<String, GitError> {
self.cli.show_commit(repo_path, hash).to_git_error()
}
pub fn log_with_graph(&self, repo_path: &str, limit: usize) -> Result<String, GitError> {
self.cli.log_with_graph(repo_path, limit).to_git_error()
}
pub fn stash_list(&self, repo_path: &str) -> Result<Vec<crate::app::state::StashEntry>, GitError> {
self.cli.stash_list(repo_path).to_git_error()
}
pub fn stash_apply(&self, repo_path: &str, name: &str, pop: bool) -> Result<(), GitError> {
self.cli.stash_apply(repo_path, name, pop).to_git_error()
}
pub fn stash_push(&self, repo_path: &str, message: &str) -> Result<(), GitError> {
self.cli.stash_push(repo_path, message).to_git_error()
}
pub fn stash_drop(&self, repo_path: &str, selector: &str) -> Result<(), GitError> {
self.cli.stash_drop(repo_path, selector).to_git_error()
}
pub fn checkout_branch(&self, repo_path: &str, branch: &str) -> Result<(), GitError> {
self.cli.checkout_branch(repo_path, branch).to_git_error()
}
pub fn merge(&self, repo_path: &str, branch: &str) -> Result<(), GitError> {
self.cli.merge(repo_path, branch).to_git_error()
}
pub fn rebase(&self, repo_path: &str, branch: &str) -> Result<(), GitError> {
self.cli.rebase(repo_path, branch).to_git_error()
}
pub fn create_branch(&self, repo_path: &str, branch: &str) -> Result<(), GitError> {
self.cli.create_branch(repo_path, branch).to_git_error()
}
pub fn tags(&self, repo_path: &str) -> Result<Vec<crate::app::state::TagEntry>, GitError> {
self.cli.tags(repo_path).to_git_error()
}
pub fn reflog(&self, repo_path: &str) -> Result<Vec<crate::app::state::ReflogEntry>, GitError> {
self.cli.reflog(repo_path, 100).to_git_error() }
pub fn diff(
&self,
repo_path: &str,
path: &str,
staged: bool,
context: usize,
) -> Result<String, GitError> {
self.cli.diff(repo_path, path, staged, context).to_git_error()
}
pub fn last_commit_info(&self, repo_path: &str) -> Result<crate::app::state::LastCommitInfo, GitError> {
self.cli.last_commit_info(repo_path).to_git_error()
}
pub fn commit(&self, repo_path: &str, message: &str, amend: bool, author_name: Option<&str>, author_email: Option<&str>) -> Result<(), GitError> {
self.cli.commit(repo_path, message, amend, author_name, author_email).to_git_error()
}
pub fn push(&self, repo_path: &str, force_with_lease: bool) -> Result<(), GitError> {
self.cli.push(repo_path, force_with_lease).to_git_error()
}
pub fn cherry_pick(&self, repo_path: &str, hash: &str) -> Result<(), GitError> {
self.cli.cherry_pick(repo_path, hash).to_git_error()
}
pub fn revert(&self, repo_path: &str, hash: &str) -> Result<(), GitError> {
self.cli.revert(repo_path, hash).to_git_error()
}
pub fn rebase_continue(
&self,
repo_path: &str,
message: Option<&str>,
author_name: Option<&str>,
author_email: Option<&str>,
) -> Result<(), GitError> {
self.cli.rebase_continue(repo_path, message, author_name, author_email).to_git_error()
}
pub fn rebase_start_root(&self, repo_path: &str) -> Result<String, GitError> {
self.cli.start_rebase_interactive_root(repo_path).to_git_error()
}
pub fn root_commit(&self, repo_path: &str) -> Result<String, GitError> {
self.cli.root_commit(repo_path).to_git_error()
}
pub fn rebase_abort(&self, repo_path: &str) -> Result<(), GitError> {
self.cli.rebase_abort(repo_path).to_git_error()
}
pub fn rebase_skip(&self, repo_path: &str) -> Result<(), GitError> {
self.cli.rebase_skip(repo_path).to_git_error()
}
pub fn reset(&self, repo_path: &str, mode: &str, target: &str) -> Result<(), GitError> {
self.cli.reset(repo_path, mode, target).to_git_error()
}
pub fn fetch_dry_run(&self, repo_path: &str, remote: &str) -> Result<String, GitError> {
self.cli.fetch_dry_run(repo_path, remote).to_git_error()
}
pub fn ahead_behind_remote(&self, repo_path: &str, branch: &str) -> Result<(i32, i32), GitError> {
self.cli.ahead_behind_remote(repo_path, branch).to_git_error()
}
pub fn merged_local_branches(&self, repo_path: &str, base: &str) -> Result<Vec<String>, GitError> {
self.cli.merged_local_branches(repo_path, base).to_git_error()
}
pub fn delete_local_branch(&self, repo_path: &str, branch: &str) -> Result<(), GitError> {
self.cli.delete_local_branch(repo_path, branch).to_git_error()
}
pub fn remote_prune(&self, repo_path: &str, remote: &str) -> Result<String, GitError> {
self.cli.remote_prune(repo_path, remote).to_git_error()
}
pub fn remote_url(&self, repo_path: &str, remote: &str) -> Result<String, GitError> {
self.cli.remote_url(repo_path, remote).to_git_error()
}
pub fn set_remote_url(&self, repo_path: &str, remote: &str, url: &str) -> Result<(), GitError> {
self.cli.set_remote_url(repo_path, remote, url).to_git_error()
}
pub fn list_pull_requests(&self, repo_path: &str, remote: &str) -> Result<Vec<(String, String)>, GitError> {
self.cli.list_pull_requests(repo_path, remote).to_git_error()
}
pub fn fetch_pr(&self, repo_path: &str, remote: &str, pr: &str) -> Result<(), GitError> {
self.cli.fetch_pr(repo_path, remote, pr).to_git_error()
}
pub fn checkout_pr_branch(&self, repo_path: &str, pr: &str) -> Result<(), GitError> {
self.cli.checkout_pr_branch(repo_path, pr).to_git_error()
}
pub fn log_brief(&self, repo_path: &str, limit: usize) -> Result<String, GitError> {
self.cli.log_brief(repo_path, limit).to_git_error()
}
pub fn log_stats(&self, repo_path: &str, limit: usize) -> Result<String, GitError> {
self.cli.log_stats(repo_path, limit).to_git_error()
}
pub fn pull_ff_only(&self, repo_path: &str, ff_only: bool, timeout_secs: u64) -> Result<(), GitError> {
self.cli.pull_ff_only(repo_path, ff_only, timeout_secs).to_git_error()
}
pub fn pull_rebase(&self, repo_path: &str, autostash: bool, timeout_secs: u64) -> Result<(), GitError> {
self.cli.pull_rebase(repo_path, autostash, timeout_secs).to_git_error()
}
pub fn pull(&self, repo_path: &str, timeout_secs: u64) -> Result<(), GitError> {
self.cli.pull(repo_path, timeout_secs).to_git_error()
}
pub fn fetch_all_prune(&self, repo_path: &str) -> Result<(), GitError> {
self.cli.fetch_all_prune(repo_path).to_git_error()
}
pub fn apply_patch(&self, repo_path: &str, patch: &str, to_index: bool, reverse: bool) -> Result<(), GitError> {
self.cli.apply_patch(repo_path, patch, to_index, reverse).to_git_error()
}
pub fn read_rebase_todo(&self, repo_path: &str) -> Result<(Vec<String>, String), GitError> {
self.cli.read_rebase_todo(repo_path).to_git_error()
}
pub fn start_rebase_interactive(&self, repo_path: &str, base: &str) -> Result<String, GitError> {
self.cli.start_rebase_interactive(repo_path, base).to_git_error()
}
pub fn start_rebase_with_todo(&self, repo_path: &str, base: &str, todo_content: &str, use_root: bool) -> Result<String, GitError> {
self.cli.start_rebase_with_todo(repo_path, base, todo_content, use_root).to_git_error()
}
pub fn get_config(&self, repo_path: &str, key: &str) -> Result<Option<String>, GitError> {
self.cli.get_config(repo_path, key).to_git_error()
}
}
impl Default for GitService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::fs;
use std::process::Command;
fn setup_test_repo() -> std::path::PathBuf {
let temp_dir = tempdir().expect("Failed to create temp directory");
let repo_path = temp_dir.path().to_path_buf();
Command::new("git")
.arg("init")
.arg(&repo_path)
.output()
.expect("Failed to init repo");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "config", "user.name", "Test"])
.output()
.expect("Failed to set user.name");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "config", "user.email", "test@test.com"])
.output()
.expect("Failed to set user.email");
std::mem::forget(temp_dir);
repo_path
}
#[test]
fn test_git_service_wrapper() {
let git_service = GitService::new();
assert!(true); }
#[test]
fn test_status_operations() {
let repo_path = setup_test_repo();
let git_service = GitService::new();
let test_file = repo_path.join("test.txt");
fs::write(&test_file, "content").expect("Failed to write");
let status = git_service.status_porcelain(repo_path.to_str().unwrap())
.expect("Failed to get status");
assert!(!status.is_empty());
let _ = fs::remove_dir_all(&repo_path);
}
#[test]
fn test_diff_operations() {
let repo_path = setup_test_repo();
let git_service = GitService::new();
let test_file = repo_path.join("test.txt");
fs::write(&test_file, "original").expect("Failed to write");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "add", "test.txt"])
.output()
.expect("Failed to stage");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "commit", "-m", "Initial"])
.output()
.expect("Failed to commit");
fs::write(&test_file, "modified").expect("Failed to modify");
let diff = git_service.diff(repo_path.to_str().unwrap(), "test.txt", false, 3)
.expect("Failed to get diff");
assert!(!diff.is_empty());
let _ = fs::remove_dir_all(&repo_path);
}
#[test]
fn test_commit_operations() {
let repo_path = setup_test_repo();
let git_service = GitService::new();
let test_file = repo_path.join("test.txt");
fs::write(&test_file, "content").expect("Failed to write");
git_service.stage_file(repo_path.to_str().unwrap(), "test.txt")
.expect("Failed to stage");
git_service.commit(repo_path.to_str().unwrap(), "Test commit", false, None, None)
.expect("Failed to commit");
let output = Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "log", "--oneline", "-1"])
.output()
.expect("Failed to get log");
assert!(output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).contains("Test commit"));
let _ = fs::remove_dir_all(&repo_path);
}
#[test]
fn test_branch_operations() {
let repo_path = setup_test_repo();
let git_service = GitService::new();
let test_file = repo_path.join("file.txt");
fs::write(&test_file, "content").expect("Failed to write");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "add", "file.txt"])
.output()
.expect("Failed to stage");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "commit", "-m", "Initial"])
.output()
.expect("Failed to commit");
git_service.create_branch(repo_path.to_str().unwrap(), "test-branch")
.expect("Failed to create branch");
let branches = git_service.branch_names(repo_path.to_str().unwrap())
.expect("Failed to list branches");
assert!(branches.contains(&"test-branch".to_string()));
let _ = fs::remove_dir_all(&repo_path);
}
#[test]
fn test_rebase_operations() {
let repo_path = setup_test_repo();
let git_service = GitService::new();
for i in 0..3 {
let file = repo_path.join(format!("file{}.txt", i));
fs::write(&file, format!("content {}", i)).expect("Failed to write");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "add", &format!("file{}.txt", i)])
.output()
.expect("Failed to stage");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "commit", "-m", &format!("Commit {}", i)])
.output()
.expect("Failed to commit");
}
let output = Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "rev-parse", "HEAD~2"])
.output()
.expect("Failed to get commit");
let base = String::from_utf8_lossy(&output.stdout).trim().to_string();
let todo_path = git_service.start_rebase_interactive(repo_path.to_str().unwrap(), &base)
.expect("Failed to start rebase");
assert!(std::path::Path::new(&todo_path).exists());
let (lines, _) = git_service.read_rebase_todo(repo_path.to_str().unwrap())
.expect("Failed to read todo");
assert!(!lines.is_empty());
git_service.rebase_abort(repo_path.to_str().unwrap())
.expect("Failed to abort");
let _ = fs::remove_dir_all(&repo_path);
}
}