use std::path::Path;
use anyhow::{Context, Result};
use clap::Parser;
#[derive(Parser)]
pub struct AmendCommand {
#[arg(value_name = "YAML_FILE")]
pub yaml_file: String,
#[arg(long)]
pub allow_pushed: bool,
}
impl AmendCommand {
pub fn execute(self, repo: Option<&std::path::Path>) -> Result<()> {
use crate::git::AmendmentHandler;
let repo_root = match repo {
Some(p) => p.to_path_buf(),
None => std::env::current_dir().context("Failed to determine current directory")?,
};
crate::utils::check_git_repository_at(&repo_root)?;
crate::utils::check_working_directory_clean_at(&repo_root)?;
println!("🔄 Starting commit amendment process...");
println!("📄 Loading amendments from: {}", self.yaml_file);
let handler = AmendmentHandler::new(&repo_root)
.context("Failed to initialize amendment handler")?
.with_allow_pushed(self.allow_pushed);
handler
.apply_amendments(&self.yaml_file)
.context("Failed to apply amendments")?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AmendOutcome {
pub applied: bool,
pub amendment_count: usize,
}
pub fn run_amend(
amendments_yaml: &str,
allow_pushed: bool,
repo_path: Option<&Path>,
) -> Result<AmendOutcome> {
use crate::data::amendments::AmendmentFile;
use crate::git::AmendmentHandler;
let repo_root = match repo_path {
Some(p) => p.to_path_buf(),
None => std::env::current_dir().context("Failed to determine current directory")?,
};
crate::utils::check_git_repository_at(&repo_root)?;
crate::utils::check_working_directory_clean_at(&repo_root)?;
let amendment_file =
AmendmentFile::from_yaml_str(amendments_yaml).context("Failed to parse amendments YAML")?;
let amendment_count = amendment_file.amendments.len();
if amendment_count == 0 {
return Ok(AmendOutcome {
applied: false,
amendment_count: 0,
});
}
AmendmentHandler::new(&repo_root)
.context("Failed to initialize amendment handler")?
.with_allow_pushed(allow_pushed)
.apply_amendment_file(&amendment_file)
.context("Failed to apply amendments")?;
Ok(AmendOutcome {
applied: true,
amendment_count,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod run_amend_tests {
use super::*;
use std::process::Command;
fn git_in(dir: &Path, args: &[&str]) {
let output = Command::new("git")
.current_dir(dir)
.args([
"-c",
"user.email=test@example.com",
"-c",
"user.name=Test",
"-c",
"commit.gpgsign=false",
])
.args(args)
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "git {args:?} failed: {stderr}");
}
fn repo_with_local_commit() -> tempfile::TempDir {
let tmp_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tmp");
std::fs::create_dir_all(&tmp_root).unwrap();
let work = tempfile::tempdir_in(&tmp_root).unwrap();
git_in(work.path(), &["init", "-b", "main"]);
git_in(work.path(), &["config", "user.email", "test@example.com"]);
git_in(work.path(), &["config", "user.name", "Test"]);
git_in(work.path(), &["config", "commit.gpgsign", "false"]);
std::fs::write(work.path().join("file.txt"), "content").unwrap();
git_in(work.path(), &["add", "."]);
git_in(work.path(), &["commit", "-m", "original message"]);
work
}
fn head_sha(dir: &Path) -> String {
let out = Command::new("git")
.current_dir(dir)
.args(["rev-parse", "HEAD"])
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn head_message(dir: &Path) -> String {
let out = Command::new("git")
.current_dir(dir)
.args(["log", "-1", "--format=%B"])
.output()
.unwrap();
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[test]
fn run_amend_applies_inline_yaml() {
let work = repo_with_local_commit();
let sha = head_sha(work.path());
let yaml = format!(
"amendments:\n - commit: {sha}\n message: \"feat: rewritten inline\"\n summary: \"\"\n"
);
let outcome = run_amend(&yaml, false, Some(work.path())).unwrap();
assert!(outcome.applied);
assert_eq!(outcome.amendment_count, 1);
assert_eq!(head_message(work.path()), "feat: rewritten inline");
}
#[test]
fn run_amend_empty_list_is_noop() {
let work = repo_with_local_commit();
let before = head_sha(work.path());
let outcome = run_amend("amendments: []\n", false, Some(work.path())).unwrap();
assert!(!outcome.applied);
assert_eq!(outcome.amendment_count, 0);
assert_eq!(head_sha(work.path()), before);
}
#[test]
fn run_amend_rejects_invalid_yaml() {
let work = repo_with_local_commit();
let err = run_amend("not: [valid", false, Some(work.path())).unwrap_err();
assert!(format!("{err:#}").contains("amendments"));
}
}