use crate::error::{ActionError, RepoLensError};
use chrono::Local;
use std::path::Path;
use std::process::Command;
pub fn create_branch(root: &Path) -> Result<String, RepoLensError> {
let timestamp = Local::now().format("%Y%m%d-%H%M%S");
let branch_name = format!("repolens/apply-{}", timestamp);
let output = Command::new("git")
.args(["checkout", "-b", &branch_name])
.current_dir(root)
.output()
.map_err(|e| {
RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to create branch: {}", e),
})
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to create branch '{}': {}", branch_name, stderr),
}));
}
Ok(branch_name)
}
pub fn has_changes(root: &Path) -> Result<bool, RepoLensError> {
let status_output = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(root)
.output()
.map_err(|e| {
RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to check git status: {}", e),
})
})?;
if !status_output.status.success() {
return Err(RepoLensError::Action(ActionError::ExecutionFailed {
message: "Failed to check git status".to_string(),
}));
}
let status = String::from_utf8_lossy(&status_output.stdout);
Ok(!status.trim().is_empty())
}
#[allow(dead_code)]
pub fn stage_all_changes(root: &Path) -> Result<(), RepoLensError> {
let output = Command::new("git")
.args(["add", "-A"])
.current_dir(root)
.output()
.map_err(|e| {
RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to stage changes: {}", e),
})
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to stage changes: {}", stderr),
}));
}
Ok(())
}
pub fn stage_files(root: &Path, files: &[String]) -> Result<(), RepoLensError> {
if files.is_empty() {
return Ok(());
}
let mut args = vec!["add", "--"];
let file_refs: Vec<&str> = files.iter().map(|s| s.as_str()).collect();
args.extend(file_refs);
let output = Command::new("git")
.args(&args)
.current_dir(root)
.output()
.map_err(|e| {
RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to stage files: {}", e),
})
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to stage files: {}", stderr),
}));
}
Ok(())
}
pub fn create_commit(root: &Path, message: &str) -> Result<(), RepoLensError> {
let output = Command::new("git")
.args(["commit", "-m", message])
.current_dir(root)
.output()
.map_err(|e| {
RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to create commit: {}", e),
})
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to create commit: {}", stderr),
}));
}
Ok(())
}
pub fn push_branch(root: &Path, branch_name: &str) -> Result<(), RepoLensError> {
let output = Command::new("git")
.args(["push", "-u", "origin", branch_name])
.current_dir(root)
.output()
.map_err(|e| {
RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to push branch: {}", e),
})
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(RepoLensError::Action(ActionError::ExecutionFailed {
message: format!("Failed to push branch '{}': {}", branch_name, stderr),
}));
}
Ok(())
}
#[allow(dead_code)]
pub fn get_current_branch(root: &Path) -> Option<String> {
let output = Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(root)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
if branch.is_empty() {
None
} else {
Some(branch)
}
}
pub fn get_default_branch(root: &Path) -> Option<String> {
let output = Command::new("git")
.args(["symbolic-ref", "refs/remotes/origin/HEAD", "--short"])
.current_dir(root)
.output()
.ok()?;
if output.status.success() {
let branch = String::from_utf8_lossy(&output.stdout)
.trim()
.trim_start_matches("origin/")
.to_string();
if !branch.is_empty() {
return Some(branch);
}
}
let branches = ["main", "master"];
for branch in branches {
let output = Command::new("git")
.args([
"show-ref",
"--verify",
"--quiet",
&format!("refs/heads/{}", branch),
])
.current_dir(root)
.output()
.ok()?;
if output.status.success() {
return Some(branch.to_string());
}
}
None
}
pub fn is_git_repository(root: &Path) -> bool {
root.join(".git").exists()
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
fn init_git_repo(root: &Path) -> Result<(), Box<dyn std::error::Error>> {
Command::new("git")
.args(["init"])
.current_dir(root)
.output()?;
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(root)
.output()?;
Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(root)
.output()?;
fs::write(root.join("README.md"), "# Test Repo")?;
Command::new("git")
.args(["add", "README.md"])
.current_dir(root)
.output()?;
Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(root)
.output()?;
Ok(())
}
#[test]
fn test_is_git_repository() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
assert!(!is_git_repository(root));
fs::create_dir(root.join(".git")).unwrap();
assert!(is_git_repository(root));
}
#[test]
#[serial]
fn test_create_branch() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(&root_abs).expect("Failed to change to temp directory");
init_git_repo(&root_abs).expect("Failed to init git repo");
let branch_name = create_branch(&root_abs).expect("Failed to create branch");
assert!(branch_name.starts_with("repolens/apply-"));
let output = Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(&root_abs)
.output()
.unwrap();
let current_branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(current_branch, branch_name);
let _ = std::env::set_current_dir(&original_dir);
}
#[test]
#[serial]
fn test_has_changes() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(&root_abs).expect("Failed to change to temp directory");
init_git_repo(&root_abs).expect("Failed to init git repo");
assert!(!has_changes(&root_abs).expect("Failed to check changes"));
fs::write(root_abs.join("test.txt"), "test content").unwrap();
assert!(has_changes(&root_abs).expect("Failed to check changes"));
let _ = std::env::set_current_dir(&original_dir);
}
#[test]
#[serial]
fn test_stage_all_changes() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(&root_abs).expect("Failed to change to temp directory");
init_git_repo(&root_abs).expect("Failed to init git repo");
fs::write(root_abs.join("test.txt"), "test content").unwrap();
stage_all_changes(&root_abs).expect("Failed to stage changes");
let output = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(&root_abs)
.output()
.unwrap();
let status = String::from_utf8_lossy(&output.stdout);
assert!(status.contains("test.txt"));
assert!(status.starts_with("A"));
let _ = std::env::set_current_dir(&original_dir);
}
#[test]
#[serial]
fn test_create_commit() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(&root_abs).expect("Failed to change to temp directory");
init_git_repo(&root_abs).expect("Failed to init git repo");
fs::write(root_abs.join("test.txt"), "test content").unwrap();
stage_all_changes(&root_abs).expect("Failed to stage changes");
let commit_message = "Test commit";
create_commit(&root_abs, commit_message).expect("Failed to create commit");
let output = Command::new("git")
.args(["log", "--oneline", "-1"])
.current_dir(&root_abs)
.output()
.unwrap();
let log = String::from_utf8_lossy(&output.stdout);
assert!(log.contains(commit_message));
let _ = std::env::set_current_dir(&original_dir);
}
#[test]
#[serial]
fn test_stage_files() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(&root_abs).expect("Failed to change to temp directory");
init_git_repo(&root_abs).expect("Failed to init git repo");
fs::write(root_abs.join("file1.txt"), "content 1").unwrap();
fs::write(root_abs.join("file2.txt"), "content 2").unwrap();
fs::write(root_abs.join("file3.txt"), "content 3").unwrap();
let files = vec!["file1.txt".to_string(), "file2.txt".to_string()];
stage_files(&root_abs, &files).expect("Failed to stage files");
let output = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(&root_abs)
.output()
.unwrap();
let status = String::from_utf8_lossy(&output.stdout);
assert!(
status.contains("A file1.txt"),
"file1.txt should be staged"
);
assert!(
status.contains("A file2.txt"),
"file2.txt should be staged"
);
assert!(
status.contains("?? file3.txt"),
"file3.txt should be untracked, not staged"
);
let _ = std::env::set_current_dir(&original_dir);
}
#[test]
#[serial]
fn test_stage_files_empty() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(&root_abs).expect("Failed to change to temp directory");
init_git_repo(&root_abs).expect("Failed to init git repo");
let files: Vec<String> = vec![];
stage_files(&root_abs, &files).expect("Staging empty file list should succeed");
let _ = std::env::set_current_dir(&original_dir);
}
#[test]
#[serial]
fn test_get_default_branch() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(&root_abs).expect("Failed to change to temp directory");
init_git_repo(&root_abs).expect("Failed to init git repo");
let default_branch = get_default_branch(&root_abs);
assert!(default_branch.is_some());
let branch = default_branch.unwrap();
assert!(branch == "main" || branch == "master");
let _ = std::env::set_current_dir(&original_dir);
}
#[test]
#[serial]
fn test_get_current_branch() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let root_abs = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
if std::env::current_dir().is_err() {
let _ = std::env::set_current_dir("/tmp");
}
std::env::set_current_dir(&root_abs).expect("Failed to change to temp directory");
init_git_repo(&root_abs).expect("Failed to init git repo");
let current_branch = get_current_branch(&root_abs);
assert!(current_branch.is_some());
let branch = current_branch.unwrap();
assert!(branch == "main" || branch == "master");
let _ = std::env::set_current_dir(&original_dir);
}
}