Skip to main content

borg/why/
linked.rs

1//! `borg why`, both directions of a decision → fix link (notes/regress.rs): on a decision, the later
2//! commits that fixed (or revisited) its lines; on a fix, the decisions it fixed. From the commit's
3//! note (`borg fixed in:` / `borg fixes:`) and the collective's link records (those `borg notes
4//! fetch` brought from the mesh too), one line per other commit, the note's first. Both are visible
5//! in the collective already; the text is redacted again here.
6
7use serde::Serialize;
8
9use crate::notes::links::{self, FIX, Link, LinkRef};
10use crate::notes::{Excerpt, redact};
11
12/// At most this many links per direction in the compact form. Display, as why.rs `MAX_LESSONS`.
13const MAX_LINKS: usize = 3;
14use super::TASK_CAP;
15
16/// The other commit of a link.
17#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
18pub struct LinkLine {
19    pub sha: String,
20    pub short: String,
21    pub date: String,
22    /// "fix" or "revisited".
23    pub strength: String,
24    /// Its session's task (first line).
25    pub task: String,
26}
27
28fn line(r: &LinkRef) -> LinkLine {
29    LinkLine {
30        sha: r.commit.clone(),
31        short: r.commit.chars().take(7).collect(),
32        date: r.date.clone(),
33        strength: r.strength.clone(),
34        task: redact(r.task.lines().next().unwrap_or_default().trim()),
35    }
36}
37
38fn merged<'a>(noted: &'a [LinkRef], stored: impl Iterator<Item = LinkRef> + 'a) -> Vec<LinkLine> {
39    let mut out: Vec<LinkLine> = noted.iter().map(line).collect();
40    for r in stored {
41        if !out.iter().any(|x| x.sha == r.commit) {
42            out.push(line(&r));
43        }
44    }
45    out
46}
47
48/// The links of `sha`: (fixed in, fixes), from its note (`noted`) and the store's records.
49#[must_use]
50pub fn of(sha: &str, noted: Option<&Excerpt>, stored: &[Link]) -> (Vec<LinkLine>, Vec<LinkLine>) {
51    let (by, fixes) = noted.map_or((&[][..], &[][..]), |e| (e.fixed_by.as_slice(), e.fixes.as_slice()));
52    let fixed_in = merged(by, stored.iter().filter(|l| l.commit == sha).map(|l| links::refs(l).0));
53    let fixes = merged(fixes, stored.iter().filter(|l| l.fix == sha).map(|l| links::refs(l).1));
54    (fixed_in, fixes)
55}
56
57/// The compact form: at most `MAX_LINKS`, tasks cut.
58pub fn compact(lines: &mut Vec<LinkLine>) {
59    lines.truncate(MAX_LINKS);
60    for l in lines {
61        if l.task.chars().count() > TASK_CAP {
62            l.task = format!("{}…", l.task.chars().take(TASK_CAP).collect::<String>());
63        }
64    }
65}
66
67fn verb(l: &LinkLine) -> &'static str {
68    if l.strength == FIX { "fixed" } else { "revisited" }
69}
70
71fn task(l: &LinkLine) -> String {
72    if l.task.is_empty() { String::new() } else { format!(" (task: {})", l.task) }
73}
74
75/// `→ fixed in Y` under a decision, `← decided in X … → fixed here` under a fix.
76#[must_use]
77pub fn render(fixed_in: &[LinkLine], fixes: &[LinkLine]) -> String {
78    let later = fixed_in.iter().map(|l| format!("  → {} in {} {}{}\n", verb(l), l.short, l.date, task(l)));
79    let earlier = fixes.iter().map(|l| format!("  ← decided in {} {}{} → {} here\n", l.short, l.date, task(l), verb(l)));
80    later.chain(earlier).collect()
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::notes::links::{REVISITED, Side};
87
88    fn r(commit: &str, strength: &str, task: &str) -> LinkRef {
89        LinkRef { commit: commit.into(), session: "s".into(), task: task.into(), date: "2026-10-12".into(), strength: strength.into() }
90    }
91
92    #[test]
93    fn note_and_store_merge_one_line_per_commit() {
94        let note = Excerpt { fixed_by: vec![r("aaaaaaaa1", FIX, "fix the race")], ..Excerpt::default() };
95        let stored = |commit: &str, fix: &str| Link {
96            commit: commit.into(),
97            fix: fix.into(),
98            strength: REVISITED.into(),
99            decision: Side { task: "decide".into(), date: "2026-09-30".into(), ..Side::default() },
100            fixed: Side { task: "rename token=sk-abcdefghijklmnopqrstuvwx".into(), date: "2026-10-14".into(), ..Side::default() },
101            ..Link::default()
102        };
103        let links = [stored("d", "aaaaaaaa1"), stored("d", "bbbbbbbb2"), stored("cccccccc3", "d")];
104        let (fixed_in, fixes) = of("d", Some(&note), &links);
105        assert_eq!(fixed_in.iter().map(|l| (l.short.as_str(), l.strength.as_str())).collect::<Vec<_>>(), [("aaaaaaa", FIX), ("bbbbbbb", REVISITED)]);
106        assert!(!fixed_in[1].task.contains("sk-abc"), "redacted: {}", fixed_in[1].task);
107        assert_eq!((fixes.len(), fixes[0].task.as_str()), (1, "decide"));
108        let text = render(&fixed_in[..1], &fixes);
109        assert_eq!(text, "  → fixed in aaaaaaa 2026-10-12 (task: fix the race)\n  ← decided in ccccccc 2026-09-30 (task: decide) → revisited here\n");
110        let (later, earlier) = of("x", None, &links);
111        assert!(later.is_empty() && earlier.is_empty());
112        let mut many: Vec<LinkLine> = (0..5).map(|i| line(&r(&format!("c{i}"), FIX, &"t".repeat(300)))).collect();
113        compact(&mut many);
114        assert_eq!((many.len(), many[0].task.chars().count()), (3, TASK_CAP + 1));
115    }
116}