cascade_cli/stack/
metadata.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use uuid::Uuid;
5
6/// Metadata associated with a commit in the stack
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CommitMetadata {
9    /// The commit hash
10    pub hash: String,
11    /// Original commit message
12    pub message: String,
13    /// Stack entry ID this commit belongs to
14    pub stack_entry_id: Uuid,
15    /// Stack ID this commit belongs to
16    pub stack_id: Uuid,
17    /// Branch name where this commit lives
18    pub branch: String,
19    /// Original local branch where this commit was made
20    pub source_branch: String,
21    /// Dependent commit hashes (commits this one depends on)
22    pub dependencies: Vec<String>,
23    /// Commits that depend on this one
24    pub dependents: Vec<String>,
25    /// Whether this commit has been pushed to remote
26    pub is_pushed: bool,
27    /// Whether this commit is part of a submitted PR
28    pub is_submitted: bool,
29    /// Pull request ID if submitted
30    pub pull_request_id: Option<String>,
31    /// When this metadata was created
32    pub created_at: DateTime<Utc>,
33    /// When this metadata was last updated
34    pub updated_at: DateTime<Utc>,
35}
36
37/// High-level metadata for a stack
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct StackMetadata {
40    /// Stack ID
41    pub stack_id: Uuid,
42    /// Stack name
43    pub name: String,
44    /// Stack description
45    pub description: Option<String>,
46    /// Base branch for this stack
47    pub base_branch: String,
48    /// Current active branch in the stack
49    pub current_branch: Option<String>,
50    /// Total number of commits in the stack
51    pub total_commits: usize,
52    /// Number of submitted commits
53    pub submitted_commits: usize,
54    /// Number of merged commits
55    pub merged_commits: usize,
56    /// All branches associated with this stack
57    pub branches: Vec<String>,
58    /// All commit hashes in this stack (in order)
59    pub commit_hashes: Vec<String>,
60    /// Whether this stack has conflicts
61    pub has_conflicts: bool,
62    /// Whether this stack is up to date with base
63    pub is_up_to_date: bool,
64    /// Last time this stack was synced
65    pub last_sync: Option<DateTime<Utc>>,
66    /// When this stack was created
67    pub created_at: DateTime<Utc>,
68    /// When this stack was last updated
69    pub updated_at: DateTime<Utc>,
70}
71
72impl CommitMetadata {
73    /// Create new commit metadata
74    pub fn new(
75        hash: String,
76        message: String,
77        stack_entry_id: Uuid,
78        stack_id: Uuid,
79        branch: String,
80        source_branch: String,
81    ) -> Self {
82        let now = Utc::now();
83        Self {
84            hash,
85            message,
86            stack_entry_id,
87            stack_id,
88            branch,
89            source_branch,
90            dependencies: Vec::new(),
91            dependents: Vec::new(),
92            is_pushed: false,
93            is_submitted: false,
94            pull_request_id: None,
95            created_at: now,
96            updated_at: now,
97        }
98    }
99
100    /// Add a dependency (commit this one depends on)
101    pub fn add_dependency(&mut self, commit_hash: String) {
102        if !self.dependencies.contains(&commit_hash) {
103            self.dependencies.push(commit_hash);
104            self.updated_at = Utc::now();
105        }
106    }
107
108    /// Add a dependent (commit that depends on this one)
109    pub fn add_dependent(&mut self, commit_hash: String) {
110        if !self.dependents.contains(&commit_hash) {
111            self.dependents.push(commit_hash);
112            self.updated_at = Utc::now();
113        }
114    }
115
116    /// Mark as pushed to remote
117    pub fn mark_pushed(&mut self) {
118        self.is_pushed = true;
119        self.updated_at = Utc::now();
120    }
121
122    /// Mark as submitted for review
123    pub fn mark_submitted(&mut self, pull_request_id: String) {
124        self.is_submitted = true;
125        self.pull_request_id = Some(pull_request_id);
126        self.updated_at = Utc::now();
127    }
128
129    /// Get a short version of the commit hash
130    pub fn short_hash(&self) -> String {
131        if self.hash.len() >= 8 {
132            self.hash[..8].to_string()
133        } else {
134            self.hash.clone()
135        }
136    }
137}
138
139impl StackMetadata {
140    /// Create new stack metadata
141    pub fn new(
142        stack_id: Uuid,
143        name: String,
144        base_branch: String,
145        description: Option<String>,
146    ) -> Self {
147        let now = Utc::now();
148        Self {
149            stack_id,
150            name,
151            description,
152            base_branch,
153            current_branch: None,
154            total_commits: 0,
155            submitted_commits: 0,
156            merged_commits: 0,
157            branches: Vec::new(),
158            commit_hashes: Vec::new(),
159            has_conflicts: false,
160            is_up_to_date: true,
161            last_sync: None,
162            created_at: now,
163            updated_at: now,
164        }
165    }
166
167    /// Update commit statistics
168    pub fn update_stats(&mut self, total: usize, submitted: usize, merged: usize) {
169        self.total_commits = total;
170        self.submitted_commits = submitted;
171        self.merged_commits = merged;
172        self.updated_at = Utc::now();
173    }
174
175    /// Add a branch to this stack
176    pub fn add_branch(&mut self, branch: String) {
177        if !self.branches.contains(&branch) {
178            self.branches.push(branch);
179            self.updated_at = Utc::now();
180        }
181    }
182
183    /// Remove a branch from this stack
184    pub fn remove_branch(&mut self, branch: &str) {
185        if let Some(pos) = self.branches.iter().position(|b| b == branch) {
186            self.branches.remove(pos);
187            self.updated_at = Utc::now();
188        }
189    }
190
191    /// Set the current active branch
192    pub fn set_current_branch(&mut self, branch: Option<String>) {
193        self.current_branch = branch;
194        self.updated_at = Utc::now();
195    }
196
197    /// Add a commit hash to the stack
198    pub fn add_commit(&mut self, commit_hash: String) {
199        if !self.commit_hashes.contains(&commit_hash) {
200            self.commit_hashes.push(commit_hash);
201            self.total_commits = self.commit_hashes.len();
202            self.updated_at = Utc::now();
203        }
204    }
205
206    /// Remove a commit hash from the stack
207    pub fn remove_commit(&mut self, commit_hash: &str) {
208        if let Some(pos) = self.commit_hashes.iter().position(|h| h == commit_hash) {
209            self.commit_hashes.remove(pos);
210            self.total_commits = self.commit_hashes.len();
211            self.updated_at = Utc::now();
212        }
213    }
214
215    /// Mark stack as having conflicts
216    pub fn set_conflicts(&mut self, has_conflicts: bool) {
217        self.has_conflicts = has_conflicts;
218        self.updated_at = Utc::now();
219    }
220
221    /// Mark stack sync status
222    pub fn set_up_to_date(&mut self, is_up_to_date: bool) {
223        self.is_up_to_date = is_up_to_date;
224        if is_up_to_date {
225            self.last_sync = Some(Utc::now());
226        }
227        self.updated_at = Utc::now();
228    }
229
230    /// Get completion percentage (submitted/total)
231    pub fn completion_percentage(&self) -> f64 {
232        if self.total_commits == 0 {
233            0.0
234        } else {
235            (self.submitted_commits as f64 / self.total_commits as f64) * 100.0
236        }
237    }
238
239    /// Get merge percentage (merged/total)
240    pub fn merge_percentage(&self) -> f64 {
241        if self.total_commits == 0 {
242            0.0
243        } else {
244            (self.merged_commits as f64 / self.total_commits as f64) * 100.0
245        }
246    }
247
248    /// Check if the stack is complete (all commits submitted)
249    pub fn is_complete(&self) -> bool {
250        self.total_commits > 0 && self.submitted_commits == self.total_commits
251    }
252
253    /// Check if the stack is fully merged
254    pub fn is_fully_merged(&self) -> bool {
255        self.total_commits > 0 && self.merged_commits == self.total_commits
256    }
257}
258
259/// Edit mode state tracking
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct EditModeState {
262    pub is_active: bool,
263    pub target_entry_id: Option<Uuid>,
264    pub target_stack_id: Option<Uuid>,
265    pub original_commit_hash: String,
266    pub started_at: DateTime<Utc>,
267}
268
269impl EditModeState {
270    /// Create new edit mode state
271    pub fn new(stack_id: Uuid, entry_id: Uuid, commit_hash: String) -> Self {
272        Self {
273            is_active: true,
274            target_entry_id: Some(entry_id),
275            target_stack_id: Some(stack_id),
276            original_commit_hash: commit_hash,
277            started_at: Utc::now(),
278        }
279    }
280
281    /// Clear edit mode state
282    pub fn clear() -> Self {
283        Self {
284            is_active: false,
285            target_entry_id: None,
286            target_stack_id: None,
287            original_commit_hash: String::new(),
288            started_at: Utc::now(),
289        }
290    }
291}
292
293/// Repository-wide stack metadata
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct RepositoryMetadata {
296    /// All stacks in this repository
297    pub stacks: HashMap<Uuid, StackMetadata>,
298    /// All commit metadata
299    pub commits: HashMap<String, CommitMetadata>,
300    /// Currently active stack ID
301    pub active_stack_id: Option<Uuid>,
302    /// Default base branch for new stacks
303    pub default_base_branch: String,
304    /// Edit mode state
305    pub edit_mode: Option<EditModeState>,
306    /// When this metadata was last updated
307    pub updated_at: DateTime<Utc>,
308}
309
310impl RepositoryMetadata {
311    /// Create new repository metadata
312    pub fn new(default_base_branch: String) -> Self {
313        Self {
314            stacks: HashMap::new(),
315            commits: HashMap::new(),
316            active_stack_id: None,
317            default_base_branch,
318            edit_mode: None,
319            updated_at: Utc::now(),
320        }
321    }
322
323    /// Add a stack to the repository
324    pub fn add_stack(&mut self, stack_metadata: StackMetadata) {
325        self.stacks.insert(stack_metadata.stack_id, stack_metadata);
326        self.updated_at = Utc::now();
327    }
328
329    /// Remove a stack from the repository
330    pub fn remove_stack(&mut self, stack_id: &Uuid) -> Option<StackMetadata> {
331        let removed = self.stacks.remove(stack_id);
332        if removed.is_some() {
333            // If this was the active stack, clear the active stack
334            if self.active_stack_id == Some(*stack_id) {
335                self.active_stack_id = None;
336            }
337            self.updated_at = Utc::now();
338        }
339        removed
340    }
341
342    /// Get a stack by ID
343    pub fn get_stack(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
344        self.stacks.get(stack_id)
345    }
346
347    /// Get a mutable stack by ID
348    pub fn get_stack_mut(&mut self, stack_id: &Uuid) -> Option<&mut StackMetadata> {
349        self.stacks.get_mut(stack_id)
350    }
351
352    /// Set the active stack
353    pub fn set_active_stack(&mut self, stack_id: Option<Uuid>) {
354        self.active_stack_id = stack_id;
355        self.updated_at = Utc::now();
356    }
357
358    /// Get the active stack
359    pub fn get_active_stack(&self) -> Option<&StackMetadata> {
360        self.active_stack_id.and_then(|id| self.stacks.get(&id))
361    }
362
363    /// Add commit metadata
364    pub fn add_commit(&mut self, commit_metadata: CommitMetadata) {
365        self.commits
366            .insert(commit_metadata.hash.clone(), commit_metadata);
367        self.updated_at = Utc::now();
368    }
369
370    /// Remove commit metadata
371    pub fn remove_commit(&mut self, commit_hash: &str) -> Option<CommitMetadata> {
372        let removed = self.commits.remove(commit_hash);
373        if removed.is_some() {
374            self.updated_at = Utc::now();
375        }
376        removed
377    }
378
379    /// Get commit metadata
380    pub fn get_commit(&self, commit_hash: &str) -> Option<&CommitMetadata> {
381        self.commits.get(commit_hash)
382    }
383
384    /// Get all stacks
385    pub fn get_all_stacks(&self) -> Vec<&StackMetadata> {
386        self.stacks.values().collect()
387    }
388
389    /// Get all commits for a stack
390    pub fn get_stack_commits(&self, stack_id: &Uuid) -> Vec<&CommitMetadata> {
391        self.commits
392            .values()
393            .filter(|commit| &commit.stack_id == stack_id)
394            .collect()
395    }
396
397    /// Find stack by name
398    pub fn find_stack_by_name(&self, name: &str) -> Option<&StackMetadata> {
399        self.stacks.values().find(|stack| stack.name == name)
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn test_commit_metadata() {
409        let stack_id = Uuid::new_v4();
410        let entry_id = Uuid::new_v4();
411
412        let mut commit = CommitMetadata::new(
413            "abc123".to_string(),
414            "Test commit".to_string(),
415            entry_id,
416            stack_id,
417            "feature-branch".to_string(),
418            "main".to_string(),
419        );
420
421        assert_eq!(commit.hash, "abc123");
422        assert_eq!(commit.message, "Test commit");
423        assert_eq!(commit.short_hash(), "abc123");
424        assert!(!commit.is_pushed);
425        assert!(!commit.is_submitted);
426
427        commit.add_dependency("def456".to_string());
428        assert_eq!(commit.dependencies, vec!["def456"]);
429
430        commit.mark_pushed();
431        assert!(commit.is_pushed);
432
433        commit.mark_submitted("PR-123".to_string());
434        assert!(commit.is_submitted);
435        assert_eq!(commit.pull_request_id, Some("PR-123".to_string()));
436    }
437
438    #[test]
439    fn test_stack_metadata() {
440        let stack_id = Uuid::new_v4();
441        let mut stack = StackMetadata::new(
442            stack_id,
443            "test-stack".to_string(),
444            "main".to_string(),
445            Some("Test stack".to_string()),
446        );
447
448        assert_eq!(stack.name, "test-stack");
449        assert_eq!(stack.base_branch, "main");
450        assert_eq!(stack.total_commits, 0);
451        assert_eq!(stack.completion_percentage(), 0.0);
452
453        stack.add_branch("feature-1".to_string());
454        stack.add_commit("abc123".to_string());
455        stack.update_stats(2, 1, 0);
456
457        assert_eq!(stack.branches, vec!["feature-1"]);
458        assert_eq!(stack.total_commits, 2);
459        assert_eq!(stack.submitted_commits, 1);
460        assert_eq!(stack.completion_percentage(), 50.0);
461        assert!(!stack.is_complete());
462        assert!(!stack.is_fully_merged());
463
464        stack.update_stats(2, 2, 2);
465        assert!(stack.is_complete());
466        assert!(stack.is_fully_merged());
467    }
468
469    #[test]
470    fn test_repository_metadata() {
471        let mut repo = RepositoryMetadata::new("main".to_string());
472
473        let stack_id = Uuid::new_v4();
474        let stack =
475            StackMetadata::new(stack_id, "test-stack".to_string(), "main".to_string(), None);
476
477        assert!(repo.get_active_stack().is_none());
478        assert_eq!(repo.get_all_stacks().len(), 0);
479
480        repo.add_stack(stack);
481        assert_eq!(repo.get_all_stacks().len(), 1);
482        assert!(repo.get_stack(&stack_id).is_some());
483
484        repo.set_active_stack(Some(stack_id));
485        assert!(repo.get_active_stack().is_some());
486        assert_eq!(repo.get_active_stack().unwrap().stack_id, stack_id);
487
488        let found = repo.find_stack_by_name("test-stack");
489        assert!(found.is_some());
490        assert_eq!(found.unwrap().stack_id, stack_id);
491    }
492}