use chrono::Local;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ReviewStatus {
#[default]
Pending,
Approved,
Rejected,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewItem {
pub id: String,
pub commit_hash: String,
pub requested_by: String,
pub review_points: Vec<String>,
pub status: ReviewStatus,
pub created_at: String,
pub resolved_at: Option<String>,
pub response: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReviewQueue {
pub items: Vec<ReviewItem>,
}
impl ReviewQueue {
fn queue_path() -> PathBuf {
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("gitstack");
config_dir.join("review_queue.json")
}
pub fn load() -> Self {
let path = Self::queue_path();
if !path.exists() {
return Self::default();
}
fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self) -> anyhow::Result<()> {
let path = Self::queue_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(self)?;
fs::write(&path, json)?;
Ok(())
}
pub fn add_review(&mut self, item: ReviewItem) -> anyhow::Result<()> {
self.items.push(item);
self.save()
}
pub fn resolve_review(
&mut self,
id: &str,
status: ReviewStatus,
response: &str,
) -> anyhow::Result<()> {
if let Some(item) = self.items.iter_mut().find(|i| i.id == id) {
item.status = status;
item.response = Some(response.to_string());
item.resolved_at = Some(Local::now().to_rfc3339());
}
self.save()
}
pub fn list_pending(&self) -> Vec<&ReviewItem> {
self.items
.iter()
.filter(|i| i.status == ReviewStatus::Pending)
.collect()
}
pub fn list_resolved(&self, limit: usize) -> Vec<&ReviewItem> {
self.items
.iter()
.filter(|i| i.status != ReviewStatus::Pending)
.rev()
.take(limit)
.collect()
}
pub fn pending_count(&self) -> usize {
self.items
.iter()
.filter(|i| i.status == ReviewStatus::Pending)
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_pending_item(id: &str, hash: &str) -> ReviewItem {
ReviewItem {
id: id.to_string(),
commit_hash: hash.to_string(),
requested_by: "claude".to_string(),
review_points: vec!["Check error handling".to_string()],
status: ReviewStatus::Pending,
created_at: Local::now().to_rfc3339(),
resolved_at: None,
response: None,
}
}
fn make_resolved_item(id: &str, status: ReviewStatus) -> ReviewItem {
ReviewItem {
id: id.to_string(),
commit_hash: "abc1234".to_string(),
requested_by: "claude".to_string(),
review_points: vec![],
status,
created_at: Local::now().to_rfc3339(),
resolved_at: Some(Local::now().to_rfc3339()),
response: Some("Done".to_string()),
}
}
#[test]
fn test_review_status_default() {
assert_eq!(ReviewStatus::default(), ReviewStatus::Pending);
}
#[test]
fn test_review_status_eq() {
assert_eq!(ReviewStatus::Pending, ReviewStatus::Pending);
assert_ne!(ReviewStatus::Pending, ReviewStatus::Approved);
assert_ne!(ReviewStatus::Approved, ReviewStatus::Rejected);
}
#[test]
fn test_review_queue_default_empty() {
let queue = ReviewQueue::default();
assert!(queue.items.is_empty());
assert_eq!(queue.pending_count(), 0);
}
#[test]
fn test_review_queue_list_pending() {
let queue = ReviewQueue {
items: vec![
make_pending_item("1", "abc1234"),
make_resolved_item("2", ReviewStatus::Approved),
],
};
assert_eq!(queue.list_pending().len(), 1);
assert_eq!(queue.list_pending()[0].id, "1");
assert_eq!(queue.pending_count(), 1);
}
#[test]
fn test_review_queue_list_pending_multiple() {
let queue = ReviewQueue {
items: vec![
make_pending_item("1", "abc"),
make_pending_item("2", "def"),
make_pending_item("3", "ghi"),
],
};
assert_eq!(queue.list_pending().len(), 3);
assert_eq!(queue.pending_count(), 3);
}
#[test]
fn test_review_queue_list_resolved() {
let queue = ReviewQueue {
items: vec![make_resolved_item("1", ReviewStatus::Approved)],
};
assert_eq!(queue.list_resolved(20).len(), 1);
}
#[test]
fn test_review_queue_list_resolved_limit() {
let queue = ReviewQueue {
items: vec![
make_resolved_item("1", ReviewStatus::Approved),
make_resolved_item("2", ReviewStatus::Rejected),
make_resolved_item("3", ReviewStatus::Approved),
],
};
assert_eq!(queue.list_resolved(2).len(), 2);
assert_eq!(queue.list_resolved(20).len(), 3);
}
#[test]
fn test_review_queue_list_resolved_excludes_pending() {
let queue = ReviewQueue {
items: vec![
make_pending_item("1", "abc"),
make_resolved_item("2", ReviewStatus::Approved),
],
};
let resolved = queue.list_resolved(20);
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].id, "2");
}
#[test]
fn test_review_queue_serialize_deserialize() {
let queue = ReviewQueue {
items: vec![
make_pending_item("1", "abc1234"),
make_resolved_item("2", ReviewStatus::Approved),
],
};
let json = serde_json::to_string(&queue).unwrap();
let deserialized: ReviewQueue = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.items.len(), 2);
assert_eq!(deserialized.items[0].id, "1");
assert_eq!(deserialized.items[0].status, ReviewStatus::Pending);
assert_eq!(deserialized.items[1].status, ReviewStatus::Approved);
}
#[test]
fn test_review_item_serialize_roundtrip() {
let item = make_pending_item("test-id", "commit-hash");
let json = serde_json::to_string(&item).unwrap();
let deserialized: ReviewItem = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, "test-id");
assert_eq!(deserialized.commit_hash, "commit-hash");
assert_eq!(deserialized.requested_by, "claude");
assert_eq!(deserialized.review_points.len(), 1);
assert_eq!(deserialized.status, ReviewStatus::Pending);
assert!(deserialized.resolved_at.is_none());
assert!(deserialized.response.is_none());
}
#[test]
fn test_review_queue_pending_count_all_resolved() {
let queue = ReviewQueue {
items: vec![
make_resolved_item("1", ReviewStatus::Approved),
make_resolved_item("2", ReviewStatus::Rejected),
],
};
assert_eq!(queue.pending_count(), 0);
}
#[test]
fn test_review_status_clone_copy() {
let status = ReviewStatus::Approved;
let copied = status;
assert_eq!(status, copied);
}
#[test]
fn test_review_queue_clone() {
let queue = ReviewQueue {
items: vec![make_pending_item("1", "abc")],
};
let cloned = queue.clone();
assert_eq!(cloned.items.len(), 1);
assert_eq!(cloned.items[0].id, "1");
}
}