use std::error::Error; use serde::{Deserialize, Serialize}; use crate::cli::{cli_exec, cli_exec_from_dir}; #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] pub struct GitOptions { bin: String, workdir: Option, } #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] pub struct CheckoutOptions { // Checkout with -b if branch doesn't exist pub create: bool, #[serde(alias = "ref")] pub git_ref: String, } #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] pub struct PushOptions { pub rebase_to: Option, // If rebase, should be always set to true pub force: bool, pub brahcn: String, } #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] pub struct CommitOptions { pub message: String, pub add: bool, } impl GitOptions { pub fn new(bin: String, workdir: Option) -> Self { Self { bin, workdir } } } #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] pub struct Git { pub url: String, pub repo_path: String, } impl Git { pub fn new(url: String, repo_path: String) -> Self { Self { url, repo_path } } pub fn clone(&self, git_opts: GitOptions) -> Result<(), Box> { let cmd = format!("{} clone {} {}", git_opts.bin, self.url, self.repo_path); match self.exec(cmd, git_opts.workdir.clone()) { Ok(_) => Ok(()), Err(err) => Err(err), } } pub fn checkout( &self, git_opts: GitOptions, opts: CheckoutOptions, ) -> Result<(), Box> { let cmd = format!( "{} -C {} checkout {}", git_opts.bin, self.repo_path, opts.git_ref ); match self.exec(cmd, git_opts.workdir.clone()) { Ok(_) => Ok(()), Err(err) => match opts.create { true => { let cmd = format!( "{} -C {} checkout -b {}", git_opts.bin, self.repo_path, opts.git_ref ); match self.exec(cmd, git_opts.workdir.clone()) { Ok(_) => Ok(()), Err(err) => Err(err), } } false => Err(err), }, } } pub fn commit(&self, git_opts: GitOptions, opts: CommitOptions) -> Result<(), Box> { if opts.add { let cmd = format!("{} -C {} add .", git_opts.bin, self.repo_path); let _ = self.exec(cmd, git_opts.workdir.clone())?; } let cmd = format!( "{} diff --cached --quiet || {} -C {} commit -m \"{}\"", git_opts.bin, git_opts.bin, self.repo_path, opts.message ); match self.exec(cmd, git_opts.workdir.clone()) { Ok(_) => Ok(()), Err(err) => Err(err), } } // TODO: Add tests for rebase and force pub fn push(&self, git_opts: GitOptions, opts: PushOptions) -> Result<(), Box> { if let Some(branch) = opts.rebase_to { let cmd = format!("{} -C {} rebase {}", git_opts.bin, self.repo_path, branch); let _ = self.exec(cmd, git_opts.workdir.clone())?; } let mut args = String::new(); if opts.force { args = format!("{} --force", args); } let cmd = format!( "{} -C {} push --set-upstream origin {} {}", git_opts.bin, self.repo_path, opts.brahcn, args ); let _ = self.exec(cmd, git_opts.workdir.clone()); Ok(()) } // TODO: Add tests pub fn init(&self, git_opts: GitOptions) -> Result<(), Box> { let cmd = format!("{} -C {} init", git_opts.bin, self.repo_path); let _ = self.exec(cmd, git_opts.workdir.clone())?; let cmd = format!( "{} -C {} remote add origin {}", git_opts.bin, self.repo_path, self.url ); let _ = self.exec(cmd, git_opts.workdir.clone())?; Ok(()) } fn exec(&self, cmd: String, workdir: Option) -> Result> { match workdir { Some(workdir) => cli_exec_from_dir(cmd, workdir), None => cli_exec(cmd), } } } #[cfg(test)] mod tests { use crate::cli::cli_exec_from_dir; use crate::git::{CheckoutOptions, CommitOptions, Git, PushOptions}; use std::error::Error; use std::path::Path; use tempfile::tempdir; use super::GitOptions; fn prepare_a_repo() -> Result> { let tmp_dir = tempdir()? .into_path() .into_os_string() .into_string() .unwrap(); cli_exec_from_dir("git init".to_string(), tmp_dir.clone())?; Ok(tmp_dir) } fn prepare_a_workdir() -> Result> { let tmp_dir = tempdir()? .into_path() .into_os_string() .into_string() .unwrap(); Ok(tmp_dir) } #[test] fn test_pull_no_wd() -> Result<(), Box> { let tmp_dir = prepare_a_workdir()?; let git_dir = format!("{}/test", tmp_dir); let git = Git::new(prepare_a_repo()?, git_dir.clone()); let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); git.clone(git_opts)?; let result = Path::new(&git_dir).exists(); assert!(result); Ok(()) } #[test] fn test_pull_wd() -> Result<(), Box> { let tmp_dir = prepare_a_workdir()?; let git_dir = "test".to_string(); println!("{}", tmp_dir.clone()); let git = Git::new(prepare_a_repo()?, git_dir.clone()); let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); git.clone(git_opts)?; let result = Path::new(&format!("{}/{}", tmp_dir, git_dir)).exists(); assert!(result); Ok(()) } #[test] fn test_checkout_no_create() -> Result<(), Box> { let tmp_dir = prepare_a_workdir()?; let git_dir = "test".to_string(); let git = Git::new(prepare_a_repo()?, git_dir.clone()); let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); cli_exec_from_dir("git checkout -b test".to_string(), git.url.clone())?; cli_exec_from_dir( "touch test.txt && git add . && git commit -m test".to_string(), git.url.clone(), )?; cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?; let checkout_options = CheckoutOptions { create: false, git_ref: "test".to_string(), }; git.clone(git_opts.clone())?; git.checkout(git_opts, checkout_options)?; let result = cli_exec_from_dir( "git rev-parse --abbrev-ref HEAD".to_string(), format!("{}/{}", tmp_dir.clone(), git_dir.clone()), )?; assert_eq!(result, "test"); Ok(()) } #[test] fn test_checkout_no_create_err() -> Result<(), Box> { let tmp_dir = prepare_a_workdir()?; let git_dir = "test".to_string(); let git = Git::new(prepare_a_repo()?, git_dir.clone()); let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?; cli_exec_from_dir( "touch test.txt && git add . && git commit -m test".to_string(), git.url.clone(), )?; git.clone(git_opts.clone())?; let checkout_options = CheckoutOptions { create: false, git_ref: "test".to_string(), }; let res = git.checkout(git_opts, checkout_options); assert!(res.is_err()); Ok(()) } #[test] fn test_checkout_create() -> Result<(), Box> { let tmp_dir = prepare_a_workdir()?; let git_dir = "test".to_string(); let git = Git::new(prepare_a_repo()?, git_dir.clone()); let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?; cli_exec_from_dir( "touch test.txt && git add . && git commit -m test".to_string(), git.url.clone(), )?; git.clone(git_opts.clone())?; let checkout_options = CheckoutOptions { create: true, git_ref: "test".to_string(), }; git.checkout(git_opts, checkout_options)?; let result = cli_exec_from_dir( "git rev-parse --abbrev-ref HEAD".to_string(), format!("{}/{}", tmp_dir.clone(), git_dir.clone()), )?; assert_eq!(result, "test"); Ok(()) } #[test] fn test_commit() -> Result<(), Box> { let tmp_dir = prepare_a_workdir()?; let git_dir = "test".to_string(); let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone()); let git = Git::new(prepare_a_repo()?, git_dir.clone()); let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); git.clone(git_opts.clone())?; let commit_options = CommitOptions { message: "test commit".to_string(), add: false, }; cli_exec_from_dir("touch test.txt && git add .".to_string(), full_path.clone())?; git.commit(git_opts, commit_options)?; let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), full_path)?; assert_eq!(result, "test commit\n"); Ok(()) } #[test] fn test_commit_add() -> Result<(), Box> { let tmp_dir = prepare_a_workdir()?; let git_dir = "test".to_string(); let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone()); let git = Git::new(prepare_a_repo()?, git_dir.clone()); let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); git.clone(git_opts.clone())?; let commit_options = CommitOptions { message: "test commit".to_string(), add: true, }; cli_exec_from_dir("touch test.txt".to_string(), full_path.clone())?; git.commit(git_opts, commit_options)?; let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), full_path)?; assert_eq!(result, "test commit\n"); Ok(()) } #[test] fn test_push_no_rebase() -> Result<(), Box> { let tmp_dir = prepare_a_workdir()?; let git_dir = "test".to_string(); let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone()); let git = Git::new(prepare_a_repo()?, git_dir.clone()); let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone())); git.clone(git_opts.clone())?; let push_options = PushOptions { rebase_to: None, force: false, brahcn: "main".to_string(), }; cli_exec_from_dir("git checkout -b main".to_string(), full_path.clone())?; cli_exec_from_dir( "touch test.txt && git add . && git commit -m 'test commit'".to_string(), full_path.clone(), )?; git.push(git_opts, push_options)?; cli_exec_from_dir("git checkout main".to_string(), git.url.clone())?; let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), git.url)?; assert_eq!(result, "test commit\n"); Ok(()) } }