use std::io::IsTerminal;
use crate::commands::parse_selector;
use crate::error::{Result, TixError};
use crate::storage::Store;
pub fn run(store: &Store, raw: &str, yes: bool) -> Result<()> {
let selector = parse_selector(store, raw)?;
let issue = store.get(selector)?;
let mut confirmed = yes || !std::io::stdin().is_terminal();
if !confirmed {
eprint!("Delete '{}'? [y/N] ", issue.filename);
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.map_err(TixError::Io)?;
confirmed = line.trim().eq_ignore_ascii_case("y");
if !confirmed {
eprintln!("aborted");
}
}
if confirmed {
eprint!("{}", impact_notice(store, issue.number));
}
remove(store, raw, confirmed)?;
if confirmed {
println!("Deleted {}", issue.filename);
}
Ok(())
}
pub fn impact_notice(store: &Store, number: u32) -> String {
let issues = match store.list() {
Ok(issues) => issues,
Err(_) => return String::new(),
};
let affected = crate::hierarchy::affected_by_deletion(&issues, number);
if affected.is_empty() {
return String::new();
}
let plural = if affected.len() == 1 {
"child"
} else {
"children"
};
let mut out = format!(
"Note: {} {plural} will keep an unresolved parent reference.\n\
They are not deleted; `tix lint` reports the dangling reference until it is \
repaired with `tix edit <issue> --parent <issue>`.\n",
affected.len()
);
for child in &affected {
out.push_str(&format!(
" {} - keeps `parent: {}`\n",
child.display, number
));
}
out
}
pub fn remove(store: &Store, raw: &str, yes: bool) -> Result<()> {
if !yes {
return Ok(());
}
let selector = parse_selector(store, raw)?;
let issue = store.get(selector)?;
store.delete(&issue.filename)?;
Ok(())
}
#[cfg(test)]
mod tests {
#[allow(clippy::wildcard_imports)]
use super::*;
use crate::commands::create::create;
use crate::model::Issue;
use std::fs;
use tempfile::TempDir;
fn make_store() -> (TempDir, Store) {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("issues");
fs::create_dir_all(&dir).unwrap();
let config_path = tmp.path().join("tix.toml");
let config = crate::config::Config {
issues_dir: dir.clone(),
..crate::config::Config::default()
};
config.save(&config_path).unwrap();
let config = crate::config::Config::load_with_warning(&config_path, |_| {}).unwrap();
(
tmp,
Store {
issues_dir: dir,
config,
},
)
}
fn seed(store: &Store) -> Issue {
create(
&store.config,
store,
"Delete me",
Some("body"),
None,
None,
Some(""),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
.unwrap()
}
#[test]
fn remove_with_yes_deletes_the_file() {
let (tmp, store) = make_store();
let _ = tmp;
let seeded = seed(&store);
remove(&store, &seeded.number.to_string(), true).unwrap();
assert!(
!store.issues_dir.join(&seeded.filename).exists(),
"confirmed removal must delete the issue"
);
assert_eq!(store.list().unwrap(), [] as [crate::model::Issue; 0]);
}
#[test]
fn remove_without_yes_is_a_no_op() {
let (tmp, store) = make_store();
let _ = tmp;
let seeded = seed(&store);
remove(&store, &seeded.number.to_string(), false).unwrap();
assert!(
store.issues_dir.join(&seeded.filename).is_file(),
"unconfirmed removal must not touch the issue"
);
}
#[test]
fn remove_selector_miss_is_not_found() {
let (tmp, store) = make_store();
let _ = tmp;
let _ = seed(&store);
let err = remove(&store, "999", true).unwrap_err();
assert!(matches!(err, TixError::NotFound(_)));
}
}