omni_dev/cli/git/
amend.rs1use std::path::Path;
4
5use anyhow::{Context, Result};
6use clap::Parser;
7
8#[derive(Parser)]
10pub struct AmendCommand {
11 #[arg(value_name = "YAML_FILE")]
13 pub yaml_file: String,
14
15 #[arg(long)]
17 pub allow_pushed: bool,
18}
19
20impl AmendCommand {
21 pub fn execute(self, repo: Option<&std::path::Path>) -> Result<()> {
26 use crate::git::AmendmentHandler;
27
28 let repo_root = match repo {
31 Some(p) => p.to_path_buf(),
32 None => std::env::current_dir().context("Failed to determine current directory")?,
33 };
34
35 crate::utils::check_git_repository_at(&repo_root)?;
37 crate::utils::check_working_directory_clean_at(&repo_root)?;
38
39 println!("🔄 Starting commit amendment process...");
40 println!("📄 Loading amendments from: {}", self.yaml_file);
41
42 let handler = AmendmentHandler::new(&repo_root)
44 .context("Failed to initialize amendment handler")?
45 .with_allow_pushed(self.allow_pushed);
46
47 handler
48 .apply_amendments(&self.yaml_file)
49 .context("Failed to apply amendments")?;
50
51 Ok(())
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct AmendOutcome {
58 pub applied: bool,
61 pub amendment_count: usize,
63}
64
65pub fn run_amend(
78 amendments_yaml: &str,
79 allow_pushed: bool,
80 repo_path: Option<&Path>,
81) -> Result<AmendOutcome> {
82 use crate::data::amendments::AmendmentFile;
83 use crate::git::AmendmentHandler;
84
85 let repo_root = match repo_path {
86 Some(p) => p.to_path_buf(),
87 None => std::env::current_dir().context("Failed to determine current directory")?,
88 };
89
90 crate::utils::check_git_repository_at(&repo_root)?;
91 crate::utils::check_working_directory_clean_at(&repo_root)?;
92
93 let amendment_file =
94 AmendmentFile::from_yaml_str(amendments_yaml).context("Failed to parse amendments YAML")?;
95 let amendment_count = amendment_file.amendments.len();
96
97 if amendment_count == 0 {
98 return Ok(AmendOutcome {
99 applied: false,
100 amendment_count: 0,
101 });
102 }
103
104 AmendmentHandler::new(&repo_root)
105 .context("Failed to initialize amendment handler")?
106 .with_allow_pushed(allow_pushed)
107 .apply_amendment_file(&amendment_file)
108 .context("Failed to apply amendments")?;
109
110 Ok(AmendOutcome {
111 applied: true,
112 amendment_count,
113 })
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used, clippy::expect_used)]
118mod run_amend_tests {
119 use super::*;
120 use std::process::Command;
121
122 fn git_in(dir: &Path, args: &[&str]) {
124 let output = Command::new("git")
125 .current_dir(dir)
126 .args([
127 "-c",
128 "user.email=test@example.com",
129 "-c",
130 "user.name=Test",
131 "-c",
132 "commit.gpgsign=false",
133 ])
134 .args(args)
135 .output()
136 .unwrap();
137 let stderr = String::from_utf8_lossy(&output.stderr);
138 assert!(output.status.success(), "git {args:?} failed: {stderr}");
139 }
140
141 fn repo_with_local_commit() -> tempfile::TempDir {
149 let tmp_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tmp");
150 std::fs::create_dir_all(&tmp_root).unwrap();
151 let work = tempfile::tempdir_in(&tmp_root).unwrap();
152 git_in(work.path(), &["init", "-b", "main"]);
153 git_in(work.path(), &["config", "user.email", "test@example.com"]);
154 git_in(work.path(), &["config", "user.name", "Test"]);
155 git_in(work.path(), &["config", "commit.gpgsign", "false"]);
156 std::fs::write(work.path().join("file.txt"), "content").unwrap();
157 git_in(work.path(), &["add", "."]);
158 git_in(work.path(), &["commit", "-m", "original message"]);
159 work
160 }
161
162 fn head_sha(dir: &Path) -> String {
163 let out = Command::new("git")
164 .current_dir(dir)
165 .args(["rev-parse", "HEAD"])
166 .output()
167 .unwrap();
168 String::from_utf8_lossy(&out.stdout).trim().to_string()
169 }
170
171 fn head_message(dir: &Path) -> String {
172 let out = Command::new("git")
173 .current_dir(dir)
174 .args(["log", "-1", "--format=%B"])
175 .output()
176 .unwrap();
177 String::from_utf8_lossy(&out.stdout).trim().to_string()
178 }
179
180 #[test]
181 fn run_amend_applies_inline_yaml() {
182 let work = repo_with_local_commit();
183 let sha = head_sha(work.path());
184 let yaml = format!(
185 "amendments:\n - commit: {sha}\n message: \"feat: rewritten inline\"\n summary: \"\"\n"
186 );
187
188 let outcome = run_amend(&yaml, false, Some(work.path())).unwrap();
189 assert!(outcome.applied);
190 assert_eq!(outcome.amendment_count, 1);
191 assert_eq!(head_message(work.path()), "feat: rewritten inline");
192 }
193
194 #[test]
195 fn run_amend_empty_list_is_noop() {
196 let work = repo_with_local_commit();
197 let before = head_sha(work.path());
198
199 let outcome = run_amend("amendments: []\n", false, Some(work.path())).unwrap();
200 assert!(!outcome.applied);
201 assert_eq!(outcome.amendment_count, 0);
202 assert_eq!(head_sha(work.path()), before);
204 }
205
206 #[test]
207 fn run_amend_rejects_invalid_yaml() {
208 let work = repo_with_local_commit();
209 let err = run_amend("not: [valid", false, Some(work.path())).unwrap_err();
210 assert!(format!("{err:#}").contains("amendments"));
211 }
212}