git_adr/cli/
attach.rs

1//! Attach a file to an ADR.
2
3use anyhow::Result;
4use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
5use clap::Args as ClapArgs;
6use colored::Colorize;
7use std::path::Path;
8
9use crate::core::{ConfigManager, Git, NotesManager, ARTIFACTS_NOTES_REF};
10
11/// Arguments for the attach command.
12#[derive(ClapArgs, Debug)]
13pub struct Args {
14    /// ADR ID.
15    pub adr_id: String,
16
17    /// File to attach.
18    pub file: String,
19
20    /// Override filename.
21    #[arg(long)]
22    pub name: Option<String>,
23
24    /// Description/alt text for the attachment.
25    #[arg(long)]
26    pub description: Option<String>,
27}
28
29/// Run the attach command.
30///
31/// # Errors
32///
33/// Returns an error if attachment fails.
34pub fn run(args: Args) -> Result<()> {
35    let git = Git::new();
36    git.check_repository()?;
37
38    let config = ConfigManager::new(git.clone()).load()?;
39    let notes = NotesManager::new(git.clone(), config);
40
41    // Find the ADR
42    let adrs = notes.list()?;
43    let adr = adrs
44        .into_iter()
45        .find(|a| a.id == args.adr_id || a.id.contains(&args.adr_id))
46        .ok_or_else(|| anyhow::anyhow!("ADR not found: {}", args.adr_id))?;
47
48    // Check file exists
49    let file_path = Path::new(&args.file);
50    if !file_path.exists() {
51        anyhow::bail!("File not found: {}", args.file);
52    }
53
54    // Get file name
55    let filename = args.name.clone().unwrap_or_else(|| {
56        file_path.file_name().map_or_else(
57            || "attachment".to_string(),
58            |s| s.to_string_lossy().to_string(),
59        )
60    });
61
62    eprintln!(
63        "{} Attaching {} to ADR {}",
64        "→".blue(),
65        filename.cyan(),
66        adr.id.cyan()
67    );
68
69    // Read file content
70    let content = std::fs::read(file_path)?;
71    let encoded = BASE64.encode(&content);
72
73    // Get file metadata
74    let metadata = std::fs::metadata(file_path)?;
75    let size = metadata.len();
76
77    // Create artifact metadata
78    let artifact = serde_json::json!({
79        "filename": filename,
80        "size": size,
81        "adr_id": adr.id,
82        "description": args.description,
83        "content": encoded,
84    });
85
86    // Store as a note on the ADR's commit
87    // Format: JSON blob with filename, size, content (base64)
88    let artifact_content = serde_json::to_string_pretty(&artifact)?;
89
90    git.notes_add(ARTIFACTS_NOTES_REF, &adr.commit, &artifact_content)?;
91
92    eprintln!(
93        "{} Attached {} ({} bytes) to ADR {}",
94        "✓".green(),
95        filename.cyan(),
96        size,
97        adr.id.cyan()
98    );
99
100    Ok(())
101}