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 { oid: String },
16    Range { oldest: String, newest: String },
17}
18
19impl ReviewSource {
20    pub fn commit(oid: impl Into<String>) -> Self {
21        Self::Commit { oid: oid.into() }
22    }
23
24    pub fn range(oldest: impl Into<String>, newest: impl Into<String>) -> Self {
25        Self::Range {
26            oldest: oldest.into(),
27            newest: newest.into(),
28        }
29    }
30
31    /// Stable persistence key, also the on-disk filename stem. The `-`
32    /// separator is unambiguous because git/jj oids are dash-free hex; every
33    /// character is filesystem-safe.
34    pub fn key(&self) -> String {
35        match self {
36            Self::WorkingTree => "working".to_owned(),
37            Self::Commit { oid } => format!("commit-{oid}"),
38            Self::Range { oldest, newest } => format!("range-{oldest}-{newest}"),
39        }
40    }
41
42    /// Human-facing description of what is being reviewed.
43    pub fn label(&self) -> String {
44        match self {
45            Self::WorkingTree => "working tree".to_owned(),
46            Self::Commit { oid } => format!("commit {}", short(oid)),
47            Self::Range { oldest, newest } => {
48                format!("range {}..{}", short(oldest), short(newest))
49            }
50        }
51    }
52}
53
54fn short(oid: &str) -> &str {
55    oid.get(..SHORT_OID).unwrap_or(oid)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn keys_are_deterministic_and_distinct_per_source() {
64        assert_eq!(ReviewSource::WorkingTree.key(), "working");
65        assert_eq!(ReviewSource::commit("abc123").key(), "commit-abc123");
66        assert_eq!(ReviewSource::range("aaa", "bbb").key(), "range-aaa-bbb");
67    }
68
69    #[test]
70    fn labels_shorten_oids() {
71        assert_eq!(ReviewSource::WorkingTree.label(), "working tree");
72        assert_eq!(
73            ReviewSource::commit("0123456789abcdef").label(),
74            "commit 0123456"
75        );
76        assert_eq!(
77            ReviewSource::range("0123456789", "fedcba9876").label(),
78            "range 0123456..fedcba9"
79        );
80    }
81
82    #[test]
83    fn short_oid_tolerates_a_short_string() {
84        assert_eq!(ReviewSource::commit("ab").label(), "commit ab");
85    }
86
87    #[test]
88    fn round_trips_through_json_as_a_tagged_descriptor() {
89        for source in [
90            ReviewSource::WorkingTree,
91            ReviewSource::commit("abc"),
92            ReviewSource::range("aaa", "bbb"),
93        ] {
94            let json = serde_json::to_string(&source).expect("serialize");
95            let back: ReviewSource = serde_json::from_str(&json).expect("deserialize");
96            assert_eq!(source, back);
97        }
98    }
99}