Skip to main content

diffler_core/
source.rs

1//! What a review is *of*: the working tree, a single commit, or a contiguous
2//! commit range. A source has a deterministic, filesystem-safe persistence key
3//! and a human-facing label, so review state can be tracked per source and the
4//! agent can be told exactly what the human reviewed.
5
6use serde::{Deserialize, Serialize};
7
8/// Characters of an oid shown in a label; full oids stay in the key.
9const SHORT_OID: usize = 7;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(tag = "kind", rename_all = "snake_case")]
13pub enum ReviewSource {
14    WorkingTree,
15    Commit {
16        oid: String,
17    },
18    Range {
19        oldest: String,
20        newest: String,
21    },
22    /// A forge pull request. The diff renders as a range resolved at open
23    /// time, but review state keys on the PR number so it survives pushes.
24    Pr {
25        number: u64,
26    },
27}
28
29impl ReviewSource {
30    pub fn commit(oid: impl Into<String>) -> Self {
31        Self::Commit { oid: oid.into() }
32    }
33
34    pub fn range(oldest: impl Into<String>, newest: impl Into<String>) -> Self {
35        Self::Range {
36            oldest: oldest.into(),
37            newest: newest.into(),
38        }
39    }
40
41    pub fn pr(number: u64) -> Self {
42        Self::Pr { number }
43    }
44
45    /// Stable persistence key, also the on-disk filename stem. The `-`
46    /// separator is unambiguous because git/jj oids are dash-free hex; every
47    /// character is filesystem-safe.
48    pub fn key(&self) -> String {
49        match self {
50            Self::WorkingTree => "working".to_owned(),
51            Self::Commit { oid } => format!("commit-{oid}"),
52            Self::Range { oldest, newest } => format!("range-{oldest}-{newest}"),
53            Self::Pr { number } => format!("pr-{number}"),
54        }
55    }
56
57    /// Human-facing description of what is being reviewed.
58    pub fn label(&self) -> String {
59        match self {
60            Self::WorkingTree => "working tree".to_owned(),
61            Self::Commit { oid } => format!("commit {}", short(oid)),
62            Self::Range { oldest, newest } => {
63                format!("range {}..{}", short(oldest), short(newest))
64            }
65            Self::Pr { number } => format!("PR #{number}"),
66        }
67    }
68}
69
70fn short(oid: &str) -> &str {
71    oid.get(..SHORT_OID).unwrap_or(oid)
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn keys_are_deterministic_and_distinct_per_source() {
80        assert_eq!(ReviewSource::WorkingTree.key(), "working");
81        assert_eq!(ReviewSource::commit("abc123").key(), "commit-abc123");
82        assert_eq!(ReviewSource::range("aaa", "bbb").key(), "range-aaa-bbb");
83        assert_eq!(ReviewSource::pr(42).key(), "pr-42");
84    }
85
86    #[test]
87    fn labels_shorten_oids() {
88        assert_eq!(ReviewSource::WorkingTree.label(), "working tree");
89        assert_eq!(
90            ReviewSource::commit("0123456789abcdef").label(),
91            "commit 0123456"
92        );
93        assert_eq!(
94            ReviewSource::range("0123456789", "fedcba9876").label(),
95            "range 0123456..fedcba9"
96        );
97    }
98
99    #[test]
100    fn short_oid_tolerates_a_short_string() {
101        assert_eq!(ReviewSource::commit("ab").label(), "commit ab");
102    }
103
104    #[test]
105    fn round_trips_through_json_as_a_tagged_descriptor() {
106        for source in [
107            ReviewSource::WorkingTree,
108            ReviewSource::commit("abc"),
109            ReviewSource::range("aaa", "bbb"),
110        ] {
111            let json = serde_json::to_string(&source).expect("serialize");
112            let back: ReviewSource = serde_json::from_str(&json).expect("deserialize");
113            assert_eq!(source, back);
114        }
115    }
116}