auths_cli/commands/
sign.rs1use anyhow::{Context, Result, anyhow};
4use clap::Parser;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use auths_core::config::EnvironmentConfig;
9use auths_core::signing::PassphraseProvider;
10
11use super::artifact::sign::handle_sign as handle_artifact_sign;
12
13pub enum SignTarget {
15 Artifact(PathBuf),
16 CommitRange(String),
17}
18
19pub fn parse_sign_target(raw_target: &str) -> SignTarget {
32 let path = Path::new(raw_target);
33 if path.exists() {
34 SignTarget::Artifact(path.to_path_buf())
35 } else {
36 SignTarget::CommitRange(raw_target.to_string())
37 }
38}
39
40fn execute_git_rebase(base: &str) -> Result<()> {
45 use std::process::Command;
46 let output = Command::new("git")
47 .args(["rebase", "--exec", "git commit --amend --no-edit", base])
48 .output()
49 .context("Failed to spawn git rebase")?;
50 if !output.status.success() {
51 let stderr = String::from_utf8_lossy(&output.stderr);
52 return Err(anyhow!("git rebase failed: {}", stderr.trim()));
53 }
54 Ok(())
55}
56
57fn sign_commit_range(range: &str) -> Result<()> {
62 use std::process::Command;
63 let is_range = range.contains("..");
64 if is_range {
65 let parts: Vec<&str> = range.splitn(2, "..").collect();
66 let base = parts[0];
67 execute_git_rebase(base)?;
68 } else {
69 let output = Command::new("git")
70 .args(["commit", "--amend", "--no-edit", "--no-verify"])
71 .output()
72 .context("Failed to spawn git commit --amend")?;
73 if !output.status.success() {
74 let stderr = String::from_utf8_lossy(&output.stderr);
75 return Err(anyhow!("git commit --amend failed: {}", stderr.trim()));
76 }
77 }
78 println!("✔ Signed: {}", range);
79 Ok(())
80}
81
82#[derive(Parser, Debug, Clone)]
84#[command(about = "Sign a Git commit or artifact file.")]
85pub struct SignCommand {
86 #[arg(help = "Commit ref, range, or artifact file path")]
88 pub target: String,
89
90 #[arg(long = "sig-output", value_name = "PATH")]
92 pub sig_output: Option<PathBuf>,
93
94 #[arg(long)]
96 pub identity_key_alias: Option<String>,
97
98 #[arg(long)]
100 pub device_key_alias: Option<String>,
101
102 #[arg(long, value_name = "N")]
104 pub expires_in_days: Option<i64>,
105
106 #[arg(long)]
108 pub note: Option<String>,
109}
110
111pub fn handle_sign_unified(
118 cmd: SignCommand,
119 repo_opt: Option<PathBuf>,
120 passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
121 env_config: &EnvironmentConfig,
122) -> Result<()> {
123 match parse_sign_target(&cmd.target) {
124 SignTarget::Artifact(path) => {
125 let device_key_alias = cmd.device_key_alias.as_deref().ok_or_else(|| {
126 anyhow!(
127 "artifact signing requires --device-key-alias\n\nRun: auths sign <file> --device-key-alias <alias>"
128 )
129 })?;
130 handle_artifact_sign(
131 &path,
132 cmd.sig_output,
133 cmd.identity_key_alias.as_deref(),
134 device_key_alias,
135 cmd.expires_in_days,
136 cmd.note,
137 repo_opt,
138 passphrase_provider,
139 env_config,
140 )
141 }
142 SignTarget::CommitRange(range) => sign_commit_range(&range),
143 }
144}
145
146impl crate::commands::executable::ExecutableCommand for SignCommand {
147 fn execute(&self, ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
148 handle_sign_unified(
149 self.clone(),
150 ctx.repo_path.clone(),
151 ctx.passphrase_provider.clone(),
152 &ctx.env_config,
153 )
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_parse_sign_target_commit_ref() {
163 let target = parse_sign_target("HEAD");
164 assert!(matches!(target, SignTarget::CommitRange(_)));
165 }
166
167 #[test]
168 fn test_parse_sign_target_range() {
169 let target = parse_sign_target("main..HEAD");
170 assert!(matches!(target, SignTarget::CommitRange(_)));
171 }
172
173 #[test]
174 fn test_parse_sign_target_nonexistent_path_is_commit_range() {
175 let target = parse_sign_target("/nonexistent/artifact.tar.gz");
176 assert!(matches!(target, SignTarget::CommitRange(_)));
177 }
178
179 #[test]
180 fn test_parse_sign_target_file() {
181 use std::fs::File;
182 use tempfile::tempdir;
183 let dir = tempdir().unwrap();
184 let file_path = dir.path().join("artifact.tar.gz");
185 File::create(&file_path).unwrap();
186 let target = parse_sign_target(file_path.to_str().unwrap());
187 assert!(matches!(target, SignTarget::Artifact(_)));
188 }
189}