kanbrain_core/cli/
manage.rs1use super::init;
10use crate::model::Task;
11use crate::projects::{self, Repo, RepoCard};
12use crate::store::{self, PROJECT_DIR, Store};
13use anyhow::{Context, Result, bail};
14use std::fs;
15use std::path::Path;
16
17pub fn rename_project(central: &Path, old: &str, new: &str) -> Result<Vec<String>> {
19 projects::check_name(new)?;
20 let repos = projects::load(central)?;
21 if !repos.iter().any(|r| r.project == old) {
22 bail!("no project `{old}`");
23 }
24 if old == new {
25 return Ok(vec![format!("project `{old}` already has that name")]);
26 }
27 if repos.iter().any(|r| r.project == new) {
28 bail!("there's already a project `{new}`: move this one's repositories to it instead");
29 }
30 projects::rename(central, old, new)?;
31 for repo in repos.iter().filter(|r| r.project == old && r.dir.is_dir()) {
32 if let Some(card) = projects::read_repo_card(&repo.dir) {
33 projects::write_repo_card(&repo.dir, &RepoCard { project: new.to_string(), ..card })?;
34 }
35 }
36 Ok(vec![format!("renamed project `{old}` to `{new}`; its repositories and card follow")])
37}
38
39pub fn rename_repo(central: &Path, dir: &Path, new: &str) -> Result<Vec<String>> {
42 let repo = registered(central, dir)?;
43 let old = repo.name();
44 if old == new {
45 return Ok(vec![format!("repository `{old}` already has that name")]);
46 }
47 let new = projects::repo_name(central, &repo.project, &repo.dir, Some(new))?;
48 let card = projects::read_repo_card(&repo.dir).unwrap_or_else(|| RepoCard {
49 project: repo.project.clone(),
50 repo: None,
51 notes: String::new(),
52 });
53 let stored = (new != repo.folder()).then(|| new.clone());
54 projects::write_repo_card(&repo.dir, &RepoCard { repo: stored, ..card })?;
55 let rewritten = rewrite_sibling_links(central, &repo, |link_repo| (link_repo == old).then(|| new.clone()))?;
56 let mut done = vec![format!("renamed repository `{old}` of project `{}` to `{new}`", repo.project)];
57 if rewritten > 0 {
58 done.push(format!("rewrote the links to it in {rewritten} entries of its other repositories"));
59 }
60 Ok(done)
61}
62
63pub fn move_repo(central: &Path, dir: &Path, to: &str) -> Result<Vec<String>> {
67 let repo = registered(central, dir)?;
68 if repo.project == to {
69 return Ok(vec![format!("{} is already a repository of project `{to}`", repo.name())]);
70 }
71 projects::check_name(to)?;
72 let root = repo.dir.parent().filter(|_| repo.dir.is_dir()).context("its folder is gone: remove it instead")?;
73 let dropped = unlink(central, &repo)?;
74 let mut done = init::init(root, Some(to), None, central)?;
75 if dropped > 0 {
76 done.push(format!("dropped {dropped} links between it and project `{}`'s repositories", repo.project));
77 }
78 Ok(done)
79}
80
81pub fn remove_repo(central: &Path, dir: &Path, purge: bool) -> Result<Vec<String>> {
84 let repo = registered(central, dir)?;
85 unlink(central, &repo)?;
86 let mut done = init::deinit(&repo.dir, central)?;
87 if !repo.dir.is_dir() {
88 return Ok(done);
89 }
90 if !purge {
91 done.push(format!(
92 "kept {}: commands run here still use it; `deinit --purge` would have deleted its tasks, notes and \
93 decisions",
94 repo.dir.display()
95 ));
96 return Ok(done);
97 }
98 if repo.dir.file_name().is_none_or(|n| n != PROJECT_DIR) {
100 bail!("{} isn't a `.brain/` folder: deleted nothing", repo.dir.display());
101 }
102 let cache = store::cache_of(&repo.dir)?;
103 fs::remove_dir_all(&repo.dir).with_context(|| format!("deleting {}", repo.dir.display()))?;
104 done.push(format!("deleted {}: its tasks, notes and decisions are gone", repo.dir.display()));
105 for path in cache {
107 let _ = if path.is_dir() { fs::remove_dir_all(&path) } else { fs::remove_file(&path) };
108 }
109 Ok(done)
110}
111
112pub fn remove_project(central: &Path, project: &str, purge: bool) -> Result<Vec<String>> {
115 let repos: Vec<Repo> = projects::load(central)?.into_iter().filter(|r| r.project == project).collect();
116 if repos.is_empty() {
117 bail!("no project `{project}`");
118 }
119 let mut done = Vec::new();
120 for repo in repos {
121 done.extend(remove_repo(central, &repo.dir, purge)?);
122 }
123 Ok(done)
124}
125
126fn registered(central: &Path, dir: &Path) -> Result<Repo> {
128 projects::load(central)?
129 .into_iter()
130 .find(|r| projects::same_dir(&r.dir, dir))
131 .with_context(|| format!("{} isn't a registered repository", dir.display()))
132}
133
134fn unlink(central: &Path, repo: &Repo) -> Result<usize> {
137 let name = repo.name();
138 let siblings: Vec<String> = siblings(central, repo)?.iter().map(Repo::name).collect();
139 let mut changed = rewrite_sibling_links(central, repo, |link_repo| (link_repo == name).then(String::new))?;
140 if repo.dir.is_dir() {
141 changed += rewrite_links(&repo.dir, |link_repo| siblings.contains(&link_repo.to_string()).then(String::new))?;
142 }
143 Ok(changed)
144}
145
146fn siblings(central: &Path, repo: &Repo) -> Result<Vec<Repo>> {
148 Ok(projects::load(central)?
149 .into_iter()
150 .filter(|r| r.project == repo.project && !projects::same_dir(&r.dir, &repo.dir) && r.dir.is_dir())
151 .collect())
152}
153
154fn rewrite_sibling_links(central: &Path, repo: &Repo, change: impl Fn(&str) -> Option<String>) -> Result<usize> {
157 siblings(central, repo)?.iter().map(|sibling| rewrite_links(&sibling.dir, &change)).sum()
158}
159
160fn rewrite_links(dir: &Path, change: impl Fn(&str) -> Option<String>) -> Result<usize> {
162 let store = Store::at(dir)?;
163 let _lock = store.lock()?;
164 let (tasks, _) = store.load()?;
165 let mut changed = 0;
166 for mut task in tasks {
167 if rewrite(&mut task, &change) {
168 store.save(&mut task)?;
169 changed += 1;
170 }
171 }
172 Ok(changed)
173}
174
175fn rewrite(task: &mut Task, change: impl Fn(&str) -> Option<String>) -> bool {
177 let before = task.repo_links.clone();
178 task.repo_links = before
179 .iter()
180 .filter_map(|link| match change(&link.repo) {
181 Some(name) if name.is_empty() => None,
182 Some(name) => Some(crate::model::RepoLink { repo: name, id: link.id }),
183 None => Some(link.clone()),
184 })
185 .collect();
186 task.repo_links != before
187}
188
189#[cfg(test)]
190#[path = "../../tests/cli/manage.rs"]
191mod tests;