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.
30fn 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        // Use 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            anyhow::bail!("git mv failed for {}", src.display());
42        }
43    } else {
44        // Fall back to filesystem rename
45        std::fs::rename(src, dst).context(format!(
46            "Failed to move file from {} to {}",
47            src.display(),
48            dst.display()
49        ))?;
50    }
51
52    Ok(())
53}
54
55/// Archive a completed spec by moving it to the archive directory.
56///
57/// This operation:
58/// - Verifies the spec is completed
59/// - Creates date-based subdirectory in archive (YYYY-MM-DD)
60/// - Moves the spec file using git mv (or fs::rename if no_stage is true)
61///
62/// # Arguments
63/// * `specs_dir` - Path to the specs directory
64/// * `spec_id` - ID of the spec to archive
65/// * `options` - Archive operation options
66///
67/// # Returns
68/// * `Ok(PathBuf)` with the destination path if the spec was successfully archived
69/// * `Err(_)` if the spec doesn't exist, is not completed, or can't be moved
70pub fn archive_spec(specs_dir: &Path, spec_id: &str, options: &ArchiveOptions) -> Result<PathBuf> {
71    use crate::spec;
72
73    // Resolve and load the spec
74    let spec = spec::resolve_spec(specs_dir, spec_id)?;
75
76    // Check if completed (unless allow_non_completed is set)
77    if spec.frontmatter.status != SpecStatus::Completed && !options.allow_non_completed {
78        anyhow::bail!(
79            "Spec '{}' must be completed to archive (current: {:?})",
80            spec.id,
81            spec.frontmatter.status
82        );
83    }
84
85    let archive_dir = PathBuf::from(ARCHIVE_DIR);
86
87    // Extract date from spec ID (format: YYYY-MM-DD-XXX-abc)
88    let date_part = &spec.id[..10]; // First 10 chars: YYYY-MM-DD
89    let date_dir = archive_dir.join(date_part);
90
91    // Create date-based subdirectory if it doesn't exist
92    if !date_dir.exists() {
93        std::fs::create_dir_all(&date_dir)?;
94    }
95
96    let source_path = specs_dir.join(format!("{}.md", spec.id));
97    let dest_path = date_dir.join(format!("{}.md", spec.id));
98
99    // Move the spec file
100    move_spec_file(&source_path, &dest_path, options.no_stage)?;
101
102    Ok(dest_path)
103}