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