1mod pr_cache;
7mod storage;
8
9pub use pr_cache::{
10 CachedPr, PR_CACHE_VERSION, PrCache, load_pr_cache, pr_cache_path, save_pr_cache,
11};
12pub use storage::{load_tracking, save_tracking, tracking_path};
13
14use chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16
17pub const TRACKING_VERSION: u32 = 1;
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct TrackedBookmark {
23 pub name: String,
25 pub change_id: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub remote: Option<String>,
30 pub tracked_at: DateTime<Utc>,
32}
33
34impl TrackedBookmark {
35 pub fn new(name: String, change_id: String) -> Self {
37 Self {
38 name,
39 change_id,
40 remote: None,
41 tracked_at: Utc::now(),
42 }
43 }
44
45 pub fn with_remote(name: String, change_id: String, remote: String) -> Self {
47 Self {
48 name,
49 change_id,
50 remote: Some(remote),
51 tracked_at: Utc::now(),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, Default)]
58pub struct TrackingState {
59 pub version: u32,
61 #[serde(default)]
63 pub bookmarks: Vec<TrackedBookmark>,
64}
65
66impl TrackingState {
67 pub const fn new() -> Self {
69 Self {
70 version: TRACKING_VERSION,
71 bookmarks: Vec::new(),
72 }
73 }
74
75 pub fn is_tracked(&self, name: &str) -> bool {
77 self.bookmarks.iter().any(|b| b.name == name)
78 }
79
80 pub fn get(&self, name: &str) -> Option<&TrackedBookmark> {
82 self.bookmarks.iter().find(|b| b.name == name)
83 }
84
85 pub fn track(&mut self, bookmark: TrackedBookmark) {
87 if !self.is_tracked(&bookmark.name) {
88 self.bookmarks.push(bookmark);
89 }
90 }
91
92 pub fn untrack(&mut self, name: &str) -> bool {
94 let len_before = self.bookmarks.len();
95 self.bookmarks.retain(|b| b.name != name);
96 self.bookmarks.len() < len_before
97 }
98
99 pub fn tracked_names(&self) -> Vec<&str> {
101 self.bookmarks.iter().map(|b| b.name.as_str()).collect()
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_tracked_bookmark_new() {
111 let bookmark = TrackedBookmark::new("feat-auth".to_string(), "abc123".to_string());
112 assert_eq!(bookmark.name, "feat-auth");
113 assert_eq!(bookmark.change_id, "abc123");
114 assert!(bookmark.remote.is_none());
115 }
116
117 #[test]
118 fn test_tracked_bookmark_with_remote() {
119 let bookmark = TrackedBookmark::with_remote(
120 "feat-auth".to_string(),
121 "abc123".to_string(),
122 "upstream".to_string(),
123 );
124 assert_eq!(bookmark.remote, Some("upstream".to_string()));
125 }
126
127 #[test]
128 fn test_tracking_state_track_untrack() {
129 let mut state = TrackingState::new();
130 assert!(!state.is_tracked("feat-auth"));
131
132 state.track(TrackedBookmark::new(
133 "feat-auth".to_string(),
134 "abc123".to_string(),
135 ));
136 assert!(state.is_tracked("feat-auth"));
137 assert_eq!(state.tracked_names(), vec!["feat-auth"]);
138
139 state.track(TrackedBookmark::new(
141 "feat-auth".to_string(),
142 "def456".to_string(),
143 ));
144 assert_eq!(state.bookmarks.len(), 1);
145
146 assert!(state.untrack("feat-auth"));
147 assert!(!state.is_tracked("feat-auth"));
148 assert!(!state.untrack("feat-auth")); }
150
151 #[test]
152 fn test_tracking_state_serialization() {
153 let mut state = TrackingState::new();
154 state.track(TrackedBookmark::new(
155 "feat-auth".to_string(),
156 "abc123".to_string(),
157 ));
158
159 let toml_str = toml::to_string_pretty(&state).unwrap();
160 assert!(toml_str.contains("feat-auth"));
161 assert!(toml_str.contains("abc123"));
162
163 let deserialized: TrackingState = toml::from_str(&toml_str).unwrap();
164 assert_eq!(deserialized.bookmarks.len(), 1);
165 assert_eq!(deserialized.bookmarks[0].name, "feat-auth");
166 }
167}