Skip to main content

mana/commands/
sync.rs

1use std::path::Path;
2
3use anyhow::Result;
4
5use crate::index::{count_unit_formats, ArchiveIndex, Index};
6
7/// Force rebuild index unconditionally from YAML files
8pub fn cmd_sync(mana_dir: &Path) -> Result<()> {
9    // Check for mixed formats before building
10    let (md_count, yaml_count) = count_unit_formats(mana_dir)?;
11
12    let index = Index::build(mana_dir)?;
13    let count = index.units.len();
14    index.save(mana_dir)?;
15
16    // Rebuild archive index
17    let archive_index = ArchiveIndex::build(mana_dir)?;
18    let archive_count = archive_index.units.len();
19    if archive_count > 0 || mana_dir.join("archive.yaml").exists() {
20        archive_index.save(mana_dir)?;
21    }
22
23    println!("Index rebuilt: {} units indexed.", count);
24    if archive_count > 0 {
25        println!(
26            "Archive index rebuilt: {} archived units indexed.",
27            archive_count
28        );
29    }
30
31    // Warn about mixed formats
32    if md_count > 0 && yaml_count > 0 {
33        eprintln!();
34        eprintln!("Warning: Mixed unit formats detected!");
35        eprintln!("  {} .md files (current format)", md_count);
36        eprintln!("  {} .yaml files (legacy format)", yaml_count);
37        eprintln!();
38        eprintln!("This can cause confusion. Consider migrating legacy files:");
39        eprintln!("  - Remove or archive .yaml files: mkdir -p .mana/legacy && mv .mana/*.yaml .mana/legacy/");
40        eprintln!("  - Or run 'mana doctor' for more details");
41    }
42
43    Ok(())
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use crate::unit::Unit;
50    use crate::util::title_to_slug;
51    use std::fs;
52    use tempfile::TempDir;
53
54    #[test]
55    fn sync_rebuilds_index() {
56        let dir = TempDir::new().unwrap();
57        let mana_dir = dir.path().join(".mana");
58        fs::create_dir(&mana_dir).unwrap();
59
60        let unit1 = Unit::new("1", "Task one");
61        let unit2 = Unit::new("2", "Task two");
62
63        let slug1 = title_to_slug(&unit1.title);
64        let slug2 = title_to_slug(&unit2.title);
65
66        unit1
67            .to_file(mana_dir.join(format!("1-{}.md", slug1)))
68            .unwrap();
69        unit2
70            .to_file(mana_dir.join(format!("2-{}.md", slug2)))
71            .unwrap();
72
73        // Sync should create index with 2 units
74        let result = cmd_sync(&mana_dir);
75        assert!(result.is_ok());
76
77        // Verify index was created
78        assert!(mana_dir.join("index.yaml").exists());
79
80        // Verify index contains both units
81        let index = Index::load(&mana_dir).unwrap();
82        assert_eq!(index.units.len(), 2);
83    }
84
85    #[test]
86    fn sync_counts_units() {
87        let dir = TempDir::new().unwrap();
88        let mana_dir = dir.path().join(".mana");
89        fs::create_dir(&mana_dir).unwrap();
90
91        // Create 5 units
92        for i in 1..=5 {
93            let unit = Unit::new(i.to_string(), format!("Task {}", i));
94            let slug = title_to_slug(&unit.title);
95            unit.to_file(mana_dir.join(format!("{}-{}.md", i, slug)))
96                .unwrap();
97        }
98
99        let result = cmd_sync(&mana_dir);
100        assert!(result.is_ok());
101
102        let index = Index::load(&mana_dir).unwrap();
103        assert_eq!(index.units.len(), 5);
104    }
105
106    #[test]
107    fn sync_empty_project() {
108        let dir = TempDir::new().unwrap();
109        let mana_dir = dir.path().join(".mana");
110        fs::create_dir(&mana_dir).unwrap();
111
112        let result = cmd_sync(&mana_dir);
113        assert!(result.is_ok());
114
115        let index = Index::load(&mana_dir).unwrap();
116        assert_eq!(index.units.len(), 0);
117    }
118
119    #[test]
120    fn sync_rebuilds_archive_yaml() {
121        let dir = TempDir::new().unwrap();
122        let mana_dir = dir.path().join(".mana");
123        fs::create_dir(&mana_dir).unwrap();
124
125        // Create archive structure with units
126        let archive_dir = mana_dir.join("archive").join("2026").join("03");
127        fs::create_dir_all(&archive_dir).unwrap();
128
129        let mut unit1 = Unit::new("10", "Archived ten");
130        unit1.status = crate::unit::Status::Closed;
131        unit1.is_archived = true;
132        let slug1 = title_to_slug(&unit1.title);
133        unit1
134            .to_file(archive_dir.join(format!("10-{}.md", slug1)))
135            .unwrap();
136
137        let mut unit2 = Unit::new("20", "Archived twenty");
138        unit2.status = crate::unit::Status::Closed;
139        unit2.is_archived = true;
140        let slug2 = title_to_slug(&unit2.title);
141        unit2
142            .to_file(archive_dir.join(format!("20-{}.md", slug2)))
143            .unwrap();
144
145        // Sync should rebuild archive.yaml
146        cmd_sync(&mana_dir).unwrap();
147
148        assert!(mana_dir.join("archive.yaml").exists());
149        let archive = ArchiveIndex::load(&mana_dir).unwrap();
150        assert_eq!(archive.units.len(), 2);
151        let ids: Vec<&str> = archive.units.iter().map(|e| e.id.as_str()).collect();
152        assert!(ids.contains(&"10"));
153        assert!(ids.contains(&"20"));
154    }
155
156    #[test]
157    fn sync_does_not_create_archive_yaml_when_no_archive() {
158        let dir = TempDir::new().unwrap();
159        let mana_dir = dir.path().join(".mana");
160        fs::create_dir(&mana_dir).unwrap();
161
162        cmd_sync(&mana_dir).unwrap();
163
164        // Should NOT create archive.yaml when there's no archive dir
165        assert!(!mana_dir.join("archive.yaml").exists());
166    }
167}