Skip to main content

chant/operations/
archive.rs

1//! Archive operation for specs.
2//!
3//! Provides the canonical implementation for archiving completed specs.
4
5use anyhow::{Context, Result};
6use std::path::{Path, PathBuf};
7
8use crate::paths::ARCHIVE_DIR;
9use crate::spec::SpecStatus;
10
11/// Options for the archive operation.
12#[derive(Debug, Clone, Default)]
13pub struct ArchiveOptions {
14    /// Whether to skip git staging (use fs::rename instead of git mv).
15    pub no_stage: bool,
16    /// Whether to allow archiving non-completed specs.
17    pub allow_non_completed: bool,
18}
19
20/// Check if we're in a git repository.
21fn is_git_repo() -> bool {
22    std::process::Command::new("git")
23        .args(["rev-parse", "--git-dir"])
24        .output()
25        .map(|output| output.status.success())
26        .unwrap_or(false)
27}
28
29/// Move a file using git mv, falling back to fs::rename if not in a git repo or if no_stage is true.
30pub fn move_spec_file(src: &PathBuf, dst: &PathBuf, no_stage: bool) -> Result<()> {
31    let use_git = !no_stage && is_git_repo();
32
33    if use_git {
34        // Try git mv to stage the move
35        let status = std::process::Command::new("git")
36            .args(["mv", &src.to_string_lossy(), &dst.to_string_lossy()])
37            .status()
38            .context("Failed to run git mv")?;
39
40        if !status.success() {
41            // git mv failed (likely untracked file) - fall back to filesystem rename
42            eprintln!(
43                "Warning: git mv failed for {} (file may be untracked), using filesystem move",
44                src.display()
45            );
46            std::fs::rename(src, dst).context(format!(
47                "Failed to move file from {} to {}",
48                src.display(),
49                dst.display()
50            ))?;
51        }
52    } else {
53        // Fall back to filesystem rename
54        std::fs::rename(src, dst).context(format!(
55            "Failed to move file from {} to {}",
56            src.display(),
57            dst.display()
58        ))?;
59    }
60
61    Ok(())
62}
63
64/// Archive a completed spec by moving it to the archive directory.
65///
66/// This operation:
67/// - Verifies the spec is completed
68/// - Creates date-based subdirectory in archive (YYYY-MM-DD)
69/// - Moves the spec file using git mv (or fs::rename if no_stage is true)
70///
71/// # Arguments
72/// * `specs_dir` - Path to the specs directory
73/// * `spec_id` - ID of the spec to archive
74/// * `options` - Archive operation options
75///
76/// # Returns
77/// * `Ok(PathBuf)` with the destination path if the spec was successfully archived
78/// * `Err(_)` if the spec doesn't exist, is not completed, or can't be moved
79pub fn archive_spec(specs_dir: &Path, spec_id: &str, options: &ArchiveOptions) -> Result<PathBuf> {
80    use crate::spec;
81
82    // Resolve and load the spec
83    let spec = spec::resolve_spec(specs_dir, spec_id)?;
84
85    // Check if completed (unless allow_non_completed is set)
86    if spec.frontmatter.status != SpecStatus::Completed && !options.allow_non_completed {
87        anyhow::bail!(
88            "Spec '{}' must be completed to archive (current: {:?})",
89            spec.id,
90            spec.frontmatter.status
91        );
92    }
93
94    let archive_dir = PathBuf::from(ARCHIVE_DIR);
95
96    // Extract date from spec ID (format: YYYY-MM-DD-XXX-abc)
97    let date_part = &spec.id[..10]; // First 10 chars: YYYY-MM-DD
98    let date_dir = archive_dir.join(date_part);
99
100    // Create date-based subdirectory if it doesn't exist
101    if !date_dir.exists() {
102        std::fs::create_dir_all(&date_dir)?;
103    }
104
105    let source_path = specs_dir.join(format!("{}.md", spec.id));
106    let dest_path = date_dir.join(format!("{}.md", spec.id));
107
108    // Move the spec file
109    move_spec_file(&source_path, &dest_path, options.no_stage)?;
110
111    Ok(dest_path)
112}