use std::collections::HashSet;
use std::io;
use std::path::Path;
use super::Catalog;
use crate::git;
use crate::task::Task;
use crate::taskfile::invalid;
pub(crate) struct Dead {
pub id: String,
pub task: Task,
pub retired_at: i64,
}
const SEP: char = '\u{1f}';
pub(crate) fn resolve_dead(store: &Path, id: &str) -> io::Result<Option<Dead>> {
let path = format!("tasks/{id}.md");
let fmt = format!("--format=%H{SEP}%ct");
let log = git::run(store, &["log", "-1", "--diff-filter=D", &fmt, "--", &path], None)?;
let log = log.trim_end_matches('\n');
if log.is_empty() {
return Ok(None); }
let mut fields = log.splitn(2, SEP);
let sha = fields.next().expect("splitn always yields a first field");
let ct = fields.next().expect("git --format emitted a %ct field");
let retired_at = ct.parse().expect("git %ct is an integer unix timestamp");
let content = git::run(store, &["show", &format!("{sha}^:{path}")], None)?;
let task = Task::parse(&content).map_err(|e| invalid(e.to_string()))?;
Ok(Some(Dead { id: id.to_string(), task, retired_at }))
}
pub(crate) fn dead_balls(store: &Path, live: &Catalog) -> io::Result<Vec<Dead>> {
let log = git::run(store, &["log", "--diff-filter=D", "--format=", "--name-only", "--", "tasks"], None)?;
let ids = log.lines().filter_map(|l| l.strip_prefix("tasks/").and_then(|f| f.strip_suffix(".md")));
let mut seen = HashSet::new();
let mut dead = Vec::new();
for id in ids {
if !seen.insert(id) || !live.is_resolved(id) {
continue;
}
if let Some(d) = resolve_dead(store, id)? {
dead.push(d);
}
}
Ok(dead)
}
#[cfg(test)]
#[path = "history_tests.rs"]
mod tests;