helmzoo/src/git.rs
Nikolai Rodionov 1ecee01b17
Some fixes and updates
Signed-off-by: Nikolai Rodionov <nikolai.rodionov@onpier.de>
2025-05-09 16:39:12 +02:00

328 lines
11 KiB
Rust

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<String>,
}
#[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<String>,
// 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<String>) -> 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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<String>) -> Result<String, Box<dyn Error>> {
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<String, Box<dyn Error>> {
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<String, Box<dyn Error>> {
let tmp_dir = tempdir()?
.into_path()
.into_os_string()
.into_string()
.unwrap();
Ok(tmp_dir)
}
#[test]
fn test_pull_no_wd() -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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(())
}
}