chant/operations/
archive.rs1use anyhow::{Context, Result};
6use std::path::{Path, PathBuf};
7
8use crate::paths::ARCHIVE_DIR;
9use crate::spec::SpecStatus;
10
11#[derive(Debug, Clone, Default)]
13pub struct ArchiveOptions {
14 pub no_stage: bool,
16 pub allow_non_completed: bool,
18}
19
20fn 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
29pub 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 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 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 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
64pub fn archive_spec(specs_dir: &Path, spec_id: &str, options: &ArchiveOptions) -> Result<PathBuf> {
81 use crate::spec;
82
83 let spec = spec::resolve_spec(specs_dir, spec_id)?;
85
86 if spec.frontmatter.status != SpecStatus::Completed && !options.allow_non_completed {
88 anyhow::bail!(
89 "Spec '{}' must be completed to archive (current: {:?})",
90 spec.id,
91 spec.frontmatter.status
92 );
93 }
94
95 if !options.allow_non_completed {
97 if let Some(ref branch_name) = spec.frontmatter.branch {
98 use crate::git_ops;
99
100 let main_branch = {
102 let config = crate::config::Config::load()?;
103 crate::merge::load_main_branch(&config)
104 };
105
106 if branch_name != &main_branch {
108 let branch_exists = git_ops::branch_exists(branch_name)?;
110
111 if branch_exists {
112 let is_merged = git_ops::is_branch_merged(branch_name, &main_branch)?;
113
114 if !is_merged {
115 anyhow::bail!(
116 "Cannot archive spec '{}': feature branch '{}' has not been merged to {}\n\
117 \n\
118 Options:\n\
119 - Merge the branch: chant merge {} or chant finalize {} --merge\n\
120 - Force archive anyway: chant archive {} --force",
121 spec.id,
122 branch_name,
123 main_branch,
124 spec.id,
125 spec.id,
126 spec.id
127 );
128 }
129 }
130 }
131 }
132 }
133
134 let archive_dir = PathBuf::from(ARCHIVE_DIR);
135
136 let date_part = &spec.id[..10]; let date_dir = archive_dir.join(date_part);
139
140 if !date_dir.exists() {
142 std::fs::create_dir_all(&date_dir)?;
143 }
144
145 let source_path = specs_dir.join(format!("{}.md", spec.id));
146 let dest_path = date_dir.join(format!("{}.md", spec.id));
147
148 move_spec_file(&source_path, &dest_path, options.no_stage)?;
150
151 Ok(dest_path)
152}