mod pr_cache;
mod storage;
pub use pr_cache::{
CachedPr, PR_CACHE_VERSION, PrCache, load_pr_cache, pr_cache_path, save_pr_cache,
};
pub use storage::{load_tracking, save_tracking, tracking_path};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const TRACKING_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TrackedBookmark {
pub name: String,
pub change_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub remote: Option<String>,
pub tracked_at: DateTime<Utc>,
}
impl TrackedBookmark {
pub fn new(name: String, change_id: String) -> Self {
Self {
name,
change_id,
remote: None,
tracked_at: Utc::now(),
}
}
pub fn with_remote(name: String, change_id: String, remote: String) -> Self {
Self {
name,
change_id,
remote: Some(remote),
tracked_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TrackingState {
pub version: u32,
#[serde(default)]
pub bookmarks: Vec<TrackedBookmark>,
}
impl TrackingState {
pub const fn new() -> Self {
Self {
version: TRACKING_VERSION,
bookmarks: Vec::new(),
}
}
pub fn is_tracked(&self, name: &str) -> bool {
self.bookmarks.iter().any(|b| b.name == name)
}
pub fn get(&self, name: &str) -> Option<&TrackedBookmark> {
self.bookmarks.iter().find(|b| b.name == name)
}
pub fn track(&mut self, bookmark: TrackedBookmark) {
if !self.is_tracked(&bookmark.name) {
self.bookmarks.push(bookmark);
}
}
pub fn untrack(&mut self, name: &str) -> bool {
let len_before = self.bookmarks.len();
self.bookmarks.retain(|b| b.name != name);
self.bookmarks.len() < len_before
}
pub fn tracked_names(&self) -> Vec<&str> {
self.bookmarks.iter().map(|b| b.name.as_str()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tracked_bookmark_new() {
let bookmark = TrackedBookmark::new("feat-auth".to_string(), "abc123".to_string());
assert_eq!(bookmark.name, "feat-auth");
assert_eq!(bookmark.change_id, "abc123");
assert!(bookmark.remote.is_none());
}
#[test]
fn test_tracked_bookmark_with_remote() {
let bookmark = TrackedBookmark::with_remote(
"feat-auth".to_string(),
"abc123".to_string(),
"upstream".to_string(),
);
assert_eq!(bookmark.remote, Some("upstream".to_string()));
}
#[test]
fn test_tracking_state_track_untrack() {
let mut state = TrackingState::new();
assert!(!state.is_tracked("feat-auth"));
state.track(TrackedBookmark::new(
"feat-auth".to_string(),
"abc123".to_string(),
));
assert!(state.is_tracked("feat-auth"));
assert_eq!(state.tracked_names(), vec!["feat-auth"]);
state.track(TrackedBookmark::new(
"feat-auth".to_string(),
"def456".to_string(),
));
assert_eq!(state.bookmarks.len(), 1);
assert!(state.untrack("feat-auth"));
assert!(!state.is_tracked("feat-auth"));
assert!(!state.untrack("feat-auth")); }
#[test]
fn test_tracking_state_serialization() {
let mut state = TrackingState::new();
state.track(TrackedBookmark::new(
"feat-auth".to_string(),
"abc123".to_string(),
));
let toml_str = toml::to_string_pretty(&state).unwrap();
assert!(toml_str.contains("feat-auth"));
assert!(toml_str.contains("abc123"));
let deserialized: TrackingState = toml::from_str(&toml_str).unwrap();
assert_eq!(deserialized.bookmarks.len(), 1);
assert_eq!(deserialized.bookmarks[0].name, "feat-auth");
}
}