use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitMetadata {
pub hash: String,
pub message: String,
pub stack_entry_id: Uuid,
pub stack_id: Uuid,
pub branch: String,
pub source_branch: String,
pub dependencies: Vec<String>,
pub dependents: Vec<String>,
pub is_pushed: bool,
pub is_submitted: bool,
#[serde(default)]
pub is_merged: bool,
pub pull_request_id: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackMetadata {
pub stack_id: Uuid,
pub name: String,
pub description: Option<String>,
pub base_branch: String,
pub current_branch: Option<String>,
pub total_commits: usize,
pub submitted_commits: usize,
pub merged_commits: usize,
pub branches: Vec<String>,
pub commit_hashes: Vec<String>,
pub has_conflicts: bool,
pub is_up_to_date: bool,
pub last_sync: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl CommitMetadata {
pub fn new(
hash: String,
message: String,
stack_entry_id: Uuid,
stack_id: Uuid,
branch: String,
source_branch: String,
) -> Self {
let now = Utc::now();
Self {
hash,
message,
stack_entry_id,
stack_id,
branch,
source_branch,
dependencies: Vec::new(),
dependents: Vec::new(),
is_pushed: false,
is_submitted: false,
is_merged: false,
pull_request_id: None,
created_at: now,
updated_at: now,
}
}
pub fn add_dependency(&mut self, commit_hash: String) {
if !self.dependencies.contains(&commit_hash) {
self.dependencies.push(commit_hash);
self.updated_at = Utc::now();
}
}
pub fn add_dependent(&mut self, commit_hash: String) {
if !self.dependents.contains(&commit_hash) {
self.dependents.push(commit_hash);
self.updated_at = Utc::now();
}
}
pub fn mark_pushed(&mut self) {
self.is_pushed = true;
self.updated_at = Utc::now();
}
pub fn mark_submitted(&mut self, pull_request_id: String) {
self.is_submitted = true;
self.pull_request_id = Some(pull_request_id);
self.updated_at = Utc::now();
}
pub fn mark_merged(&mut self, merged: bool) {
self.is_merged = merged;
self.updated_at = Utc::now();
}
pub fn short_hash(&self) -> String {
if self.hash.len() >= 8 {
self.hash[..8].to_string()
} else {
self.hash.clone()
}
}
}
impl StackMetadata {
pub fn new(
stack_id: Uuid,
name: String,
base_branch: String,
description: Option<String>,
) -> Self {
let now = Utc::now();
Self {
stack_id,
name,
description,
base_branch,
current_branch: None,
total_commits: 0,
submitted_commits: 0,
merged_commits: 0,
branches: Vec::new(),
commit_hashes: Vec::new(),
has_conflicts: false,
is_up_to_date: true,
last_sync: None,
created_at: now,
updated_at: now,
}
}
pub fn update_stats(&mut self, total: usize, submitted: usize, merged: usize) {
self.total_commits = total;
self.submitted_commits = submitted;
self.merged_commits = merged;
self.updated_at = Utc::now();
}
pub fn add_branch(&mut self, branch: String) {
if !self.branches.contains(&branch) {
self.branches.push(branch);
self.updated_at = Utc::now();
}
}
pub fn remove_branch(&mut self, branch: &str) {
if let Some(pos) = self.branches.iter().position(|b| b == branch) {
self.branches.remove(pos);
self.updated_at = Utc::now();
}
}
pub fn set_current_branch(&mut self, branch: Option<String>) {
self.current_branch = branch;
self.updated_at = Utc::now();
}
pub fn add_commit(&mut self, commit_hash: String) {
if !self.commit_hashes.contains(&commit_hash) {
self.commit_hashes.push(commit_hash);
self.total_commits = self.commit_hashes.len();
self.updated_at = Utc::now();
}
}
pub fn remove_commit(&mut self, commit_hash: &str) {
if let Some(pos) = self.commit_hashes.iter().position(|h| h == commit_hash) {
self.commit_hashes.remove(pos);
self.total_commits = self.commit_hashes.len();
self.updated_at = Utc::now();
}
}
pub fn set_conflicts(&mut self, has_conflicts: bool) {
self.has_conflicts = has_conflicts;
self.updated_at = Utc::now();
}
pub fn set_up_to_date(&mut self, is_up_to_date: bool) {
self.is_up_to_date = is_up_to_date;
if is_up_to_date {
self.last_sync = Some(Utc::now());
}
self.updated_at = Utc::now();
}
pub fn completion_percentage(&self) -> f64 {
if self.total_commits == 0 {
0.0
} else {
(self.submitted_commits as f64 / self.total_commits as f64) * 100.0
}
}
pub fn merge_percentage(&self) -> f64 {
if self.total_commits == 0 {
0.0
} else {
(self.merged_commits as f64 / self.total_commits as f64) * 100.0
}
}
pub fn is_complete(&self) -> bool {
self.total_commits > 0 && self.submitted_commits == self.total_commits
}
pub fn is_fully_merged(&self) -> bool {
self.total_commits > 0 && self.merged_commits == self.total_commits
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditModeState {
pub is_active: bool,
pub target_entry_id: Option<Uuid>,
pub target_stack_id: Option<Uuid>,
pub original_commit_hash: String,
pub started_at: DateTime<Utc>,
}
impl EditModeState {
pub fn new(stack_id: Uuid, entry_id: Uuid, commit_hash: String) -> Self {
Self {
is_active: true,
target_entry_id: Some(entry_id),
target_stack_id: Some(stack_id),
original_commit_hash: commit_hash,
started_at: Utc::now(),
}
}
pub fn clear() -> Self {
Self {
is_active: false,
target_entry_id: None,
target_stack_id: None,
original_commit_hash: String::new(),
started_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepositoryMetadata {
pub stacks: HashMap<Uuid, StackMetadata>,
pub commits: HashMap<String, CommitMetadata>,
pub active_stack_id: Option<Uuid>,
pub default_base_branch: String,
pub edit_mode: Option<EditModeState>,
pub updated_at: DateTime<Utc>,
}
impl RepositoryMetadata {
pub fn new(default_base_branch: String) -> Self {
Self {
stacks: HashMap::new(),
commits: HashMap::new(),
active_stack_id: None,
default_base_branch,
edit_mode: None,
updated_at: Utc::now(),
}
}
pub fn add_stack(&mut self, stack_metadata: StackMetadata) {
self.stacks.insert(stack_metadata.stack_id, stack_metadata);
self.updated_at = Utc::now();
}
pub fn remove_stack(&mut self, stack_id: &Uuid) -> Option<StackMetadata> {
let removed = self.stacks.remove(stack_id);
if removed.is_some() {
self.updated_at = Utc::now();
}
removed
}
pub fn get_stack(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
self.stacks.get(stack_id)
}
pub fn get_stack_mut(&mut self, stack_id: &Uuid) -> Option<&mut StackMetadata> {
self.stacks.get_mut(stack_id)
}
pub fn set_active_stack(&mut self, stack_id: Option<Uuid>) {
self.active_stack_id = stack_id;
self.updated_at = Utc::now();
}
pub fn get_active_stack(&self) -> Option<&StackMetadata> {
self.active_stack_id.and_then(|id| self.stacks.get(&id))
}
pub fn add_commit(&mut self, commit_metadata: CommitMetadata) {
self.commits
.insert(commit_metadata.hash.clone(), commit_metadata);
self.updated_at = Utc::now();
}
pub fn remove_commit(&mut self, commit_hash: &str) -> Option<CommitMetadata> {
let removed = self.commits.remove(commit_hash);
if removed.is_some() {
self.updated_at = Utc::now();
}
removed
}
pub fn get_commit(&self, commit_hash: &str) -> Option<&CommitMetadata> {
self.commits.get(commit_hash)
}
pub fn get_all_stacks(&self) -> Vec<&StackMetadata> {
self.stacks.values().collect()
}
pub fn get_stack_commits(&self, stack_id: &Uuid) -> Vec<&CommitMetadata> {
self.commits
.values()
.filter(|commit| &commit.stack_id == stack_id)
.collect()
}
pub fn find_stack_by_name(&self, name: &str) -> Option<&StackMetadata> {
self.stacks.values().find(|stack| stack.name == name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_commit_metadata() {
let stack_id = Uuid::new_v4();
let entry_id = Uuid::new_v4();
let mut commit = CommitMetadata::new(
"abc123".to_string(),
"Test commit".to_string(),
entry_id,
stack_id,
"feature-branch".to_string(),
"main".to_string(),
);
assert_eq!(commit.hash, "abc123");
assert_eq!(commit.message, "Test commit");
assert_eq!(commit.short_hash(), "abc123");
assert!(!commit.is_pushed);
assert!(!commit.is_submitted);
assert!(!commit.is_merged);
commit.add_dependency("def456".to_string());
assert_eq!(commit.dependencies, vec!["def456"]);
commit.mark_pushed();
assert!(commit.is_pushed);
commit.mark_submitted("PR-123".to_string());
assert!(commit.is_submitted);
assert_eq!(commit.pull_request_id, Some("PR-123".to_string()));
commit.mark_merged(true);
assert!(commit.is_merged);
}
#[test]
fn test_stack_metadata() {
let stack_id = Uuid::new_v4();
let mut stack = StackMetadata::new(
stack_id,
"test-stack".to_string(),
"main".to_string(),
Some("Test stack".to_string()),
);
assert_eq!(stack.name, "test-stack");
assert_eq!(stack.base_branch, "main");
assert_eq!(stack.total_commits, 0);
assert_eq!(stack.completion_percentage(), 0.0);
stack.add_branch("feature-1".to_string());
stack.add_commit("abc123".to_string());
stack.update_stats(2, 1, 0);
assert_eq!(stack.branches, vec!["feature-1"]);
assert_eq!(stack.total_commits, 2);
assert_eq!(stack.submitted_commits, 1);
assert_eq!(stack.completion_percentage(), 50.0);
assert!(!stack.is_complete());
assert!(!stack.is_fully_merged());
stack.update_stats(2, 2, 2);
assert!(stack.is_complete());
assert!(stack.is_fully_merged());
}
#[test]
fn test_repository_metadata() {
let mut repo = RepositoryMetadata::new("main".to_string());
let stack_id = Uuid::new_v4();
let stack =
StackMetadata::new(stack_id, "test-stack".to_string(), "main".to_string(), None);
assert!(repo.get_active_stack().is_none());
assert_eq!(repo.get_all_stacks().len(), 0);
repo.add_stack(stack);
assert_eq!(repo.get_all_stacks().len(), 1);
assert!(repo.get_stack(&stack_id).is_some());
repo.set_active_stack(Some(stack_id));
assert!(repo.get_active_stack().is_some());
assert_eq!(repo.get_active_stack().unwrap().stack_id, stack_id);
let found = repo.find_stack_by_name("test-stack");
assert!(found.is_some());
assert_eq!(found.unwrap().stack_id, stack_id);
}
}