1use super::metadata::RepositoryMetadata;
2use super::{CommitMetadata, Stack, StackEntry, StackMetadata, StackStatus};
3use crate::config::get_repo_config_dir;
4use crate::errors::{CascadeError, Result};
5use crate::git::GitRepository;
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9use tracing::info;
10use uuid::Uuid;
11
12#[derive(Debug)]
14pub enum BranchModification {
15 Missing {
17 branch: String,
18 entry_id: Uuid,
19 expected_commit: String,
20 },
21 ExtraCommits {
23 branch: String,
24 entry_id: Uuid,
25 expected_commit: String,
26 actual_commit: String,
27 extra_commit_count: usize,
28 extra_commit_messages: Vec<String>,
29 },
30}
31
32pub struct StackManager {
34 repo: GitRepository,
36 repo_path: PathBuf,
38 config_dir: PathBuf,
40 stacks_file: PathBuf,
42 metadata_file: PathBuf,
44 stacks: HashMap<Uuid, Stack>,
46 metadata: RepositoryMetadata,
48}
49
50impl StackManager {
51 pub fn new(repo_path: &Path) -> Result<Self> {
53 let repo = GitRepository::open(repo_path)?;
54 let config_dir = get_repo_config_dir(repo_path)?;
55 let stacks_file = config_dir.join("stacks.json");
56 let metadata_file = config_dir.join("metadata.json");
57
58 let default_base = match repo.get_current_branch() {
60 Ok(branch) => branch,
61 Err(_) => {
62 if repo.branch_exists("main") {
64 "main".to_string()
65 } else if repo.branch_exists("master") {
66 "master".to_string()
67 } else {
68 "main".to_string()
70 }
71 }
72 };
73
74 let mut manager = Self {
75 repo,
76 repo_path: repo_path.to_path_buf(),
77 config_dir,
78 stacks_file,
79 metadata_file,
80 stacks: HashMap::new(),
81 metadata: RepositoryMetadata::new(default_base),
82 };
83
84 manager.load_from_disk()?;
86
87 Ok(manager)
88 }
89
90 pub fn create_stack(
92 &mut self,
93 name: String,
94 base_branch: Option<String>,
95 description: Option<String>,
96 ) -> Result<Uuid> {
97 if self.metadata.find_stack_by_name(&name).is_some() {
99 return Err(CascadeError::config(format!(
100 "Stack '{name}' already exists"
101 )));
102 }
103
104 let base_branch = base_branch.unwrap_or_else(|| self.metadata.default_base_branch.clone());
106
107 if !self.repo.branch_exists_or_fetch(&base_branch)? {
109 return Err(CascadeError::branch(format!(
110 "Base branch '{base_branch}' does not exist locally or remotely"
111 )));
112 }
113
114 let stack = Stack::new(name.clone(), base_branch.clone(), description.clone());
116 let stack_id = stack.id;
117
118 let stack_metadata = StackMetadata::new(stack_id, name, base_branch, description);
120
121 self.stacks.insert(stack_id, stack);
123 self.metadata.add_stack(stack_metadata);
124
125 if self.metadata.stacks.len() == 1 {
127 self.set_active_stack(Some(stack_id))?;
128 } else {
129 self.save_to_disk()?;
131 }
132
133 Ok(stack_id)
134 }
135
136 pub fn get_stack(&self, stack_id: &Uuid) -> Option<&Stack> {
138 self.stacks.get(stack_id)
139 }
140
141 pub fn get_stack_mut(&mut self, stack_id: &Uuid) -> Option<&mut Stack> {
143 self.stacks.get_mut(stack_id)
144 }
145
146 pub fn get_stack_by_name(&self, name: &str) -> Option<&Stack> {
148 if let Some(metadata) = self.metadata.find_stack_by_name(name) {
149 self.stacks.get(&metadata.stack_id)
150 } else {
151 None
152 }
153 }
154
155 pub fn get_active_stack(&self) -> Option<&Stack> {
157 self.metadata
158 .active_stack_id
159 .and_then(|id| self.stacks.get(&id))
160 }
161
162 pub fn get_active_stack_mut(&mut self) -> Option<&mut Stack> {
164 if let Some(id) = self.metadata.active_stack_id {
165 self.stacks.get_mut(&id)
166 } else {
167 None
168 }
169 }
170
171 pub fn set_active_stack(&mut self, stack_id: Option<Uuid>) -> Result<()> {
173 if let Some(id) = stack_id {
175 if !self.stacks.contains_key(&id) {
176 return Err(CascadeError::config(format!(
177 "Stack with ID {id} not found"
178 )));
179 }
180 }
181
182 for stack in self.stacks.values_mut() {
184 stack.set_active(Some(stack.id) == stack_id);
185 }
186
187 if let Some(id) = stack_id {
189 let current_branch = self.repo.get_current_branch().ok();
190 if let Some(stack_meta) = self.metadata.get_stack_mut(&id) {
191 stack_meta.set_current_branch(current_branch);
192 }
193 }
194
195 self.metadata.set_active_stack(stack_id);
196 self.save_to_disk()?;
197
198 Ok(())
199 }
200
201 pub fn set_active_stack_by_name(&mut self, name: &str) -> Result<()> {
203 if let Some(metadata) = self.metadata.find_stack_by_name(name) {
204 self.set_active_stack(Some(metadata.stack_id))
205 } else {
206 Err(CascadeError::config(format!("Stack '{name}' not found")))
207 }
208 }
209
210 pub fn delete_stack(&mut self, stack_id: &Uuid) -> Result<Stack> {
212 let stack = self
213 .stacks
214 .remove(stack_id)
215 .ok_or_else(|| CascadeError::config(format!("Stack with ID {stack_id} not found")))?;
216
217 self.metadata.remove_stack(stack_id);
219
220 let stack_commits: Vec<String> = self
222 .metadata
223 .commits
224 .values()
225 .filter(|commit| &commit.stack_id == stack_id)
226 .map(|commit| commit.hash.clone())
227 .collect();
228
229 for commit_hash in stack_commits {
230 self.metadata.remove_commit(&commit_hash);
231 }
232
233 if self.metadata.active_stack_id == Some(*stack_id) {
235 let new_active = self.metadata.stacks.keys().next().copied();
236 self.set_active_stack(new_active)?;
237 }
238
239 self.save_to_disk()?;
240
241 Ok(stack)
242 }
243
244 pub fn push_to_stack(
246 &mut self,
247 branch: String,
248 commit_hash: String,
249 message: String,
250 source_branch: String,
251 ) -> Result<Uuid> {
252 let stack_id = self
253 .metadata
254 .active_stack_id
255 .ok_or_else(|| CascadeError::config("No active stack"))?;
256
257 let stack = self
258 .stacks
259 .get_mut(&stack_id)
260 .ok_or_else(|| CascadeError::config("Active stack not found"))?;
261
262 if !stack.entries.is_empty() {
264 if let Err(integrity_error) = stack.validate_git_integrity(&self.repo) {
265 return Err(CascadeError::validation(format!(
266 "Cannot push to corrupted stack '{}':\n{}\n\n\
267 š” Fix the stack integrity issues first using 'ca stack validate {}' for details.",
268 stack.name, integrity_error, stack.name
269 )));
270 }
271 }
272
273 if !self.repo.commit_exists(&commit_hash)? {
275 return Err(CascadeError::branch(format!(
276 "Commit {commit_hash} does not exist"
277 )));
278 }
279
280 if let Some(duplicate_entry) = stack.entries.iter().find(|entry| entry.message == message) {
282 return Err(CascadeError::validation(format!(
283 "Duplicate commit message in stack: \"{message}\"\n\n\
284 This message already exists in entry {} (commit: {})\n\n\
285 š” Consider using a more specific message:\n\
286 ⢠Add context: \"{message} - add validation\"\n\
287 ⢠Be more specific: \"Fix user authentication timeout bug\"\n\
288 ⢠Or amend the previous commit: git commit --amend",
289 duplicate_entry.id,
290 &duplicate_entry.commit_hash[..8]
291 )));
292 }
293
294 if stack.entries.is_empty() {
299 let current_branch = self.repo.get_current_branch()?;
300
301 if current_branch != stack.base_branch && current_branch != "HEAD" {
302 let base_exists = self.repo.branch_exists(&stack.base_branch);
304 let current_is_feature = current_branch.starts_with("feature/")
305 || current_branch.starts_with("fix/")
306 || current_branch.starts_with("chore/")
307 || current_branch.contains("feature")
308 || current_branch.contains("fix");
309
310 if base_exists && current_is_feature {
311 tracing::info!(
312 "šÆ First commit detected: updating stack '{}' base branch from '{}' to '{}'",
313 stack.name, stack.base_branch, current_branch
314 );
315
316 println!("šÆ Smart Base Branch Update:");
317 println!(
318 " Stack '{}' was created with base '{}'",
319 stack.name, stack.base_branch
320 );
321 println!(" You're now working on feature branch '{current_branch}'");
322 println!(" Updating stack base branch to match your workflow");
323
324 stack.base_branch = current_branch.clone();
326
327 if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
329 stack_meta.base_branch = current_branch.clone();
330 stack_meta.set_current_branch(Some(current_branch.clone()));
331 }
332
333 println!(
334 " ā
Stack '{}' base branch updated to '{current_branch}'",
335 stack.name
336 );
337 }
338 }
339 }
340
341 if self.repo.branch_exists(&branch) {
344 tracing::info!("Branch '{}' already exists, skipping creation", branch);
345 } else {
346 self.repo
348 .create_branch(&branch, Some(&commit_hash))
349 .map_err(|e| {
350 CascadeError::branch(format!(
351 "Failed to create branch '{}' from commit {}: {}",
352 branch,
353 &commit_hash[..8],
354 e
355 ))
356 })?;
357
358 tracing::info!(
359 "ā
Created Git branch '{}' from commit {}",
360 branch,
361 &commit_hash[..8]
362 );
363 }
364
365 let entry_id = stack.push_entry(branch.clone(), commit_hash.clone(), message.clone());
367
368 let commit_metadata = CommitMetadata::new(
370 commit_hash.clone(),
371 message,
372 entry_id,
373 stack_id,
374 branch.clone(),
375 source_branch,
376 );
377
378 self.metadata.add_commit(commit_metadata);
380 if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
381 stack_meta.add_branch(branch);
382 stack_meta.add_commit(commit_hash);
383 }
384
385 self.save_to_disk()?;
386
387 Ok(entry_id)
388 }
389
390 pub fn pop_from_stack(&mut self) -> Result<StackEntry> {
392 let stack_id = self
393 .metadata
394 .active_stack_id
395 .ok_or_else(|| CascadeError::config("No active stack"))?;
396
397 let stack = self
398 .stacks
399 .get_mut(&stack_id)
400 .ok_or_else(|| CascadeError::config("Active stack not found"))?;
401
402 let entry = stack
403 .pop_entry()
404 .ok_or_else(|| CascadeError::config("Stack is empty"))?;
405
406 self.metadata.remove_commit(&entry.commit_hash);
408
409 if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
411 stack_meta.remove_commit(&entry.commit_hash);
412 }
414
415 self.save_to_disk()?;
416
417 Ok(entry)
418 }
419
420 pub fn submit_entry(
422 &mut self,
423 stack_id: &Uuid,
424 entry_id: &Uuid,
425 pull_request_id: String,
426 ) -> Result<()> {
427 let stack = self
428 .stacks
429 .get_mut(stack_id)
430 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
431
432 let entry_commit_hash = {
433 let entry = stack
434 .get_entry(entry_id)
435 .ok_or_else(|| CascadeError::config(format!("Entry {entry_id} not found")))?;
436 entry.commit_hash.clone()
437 };
438
439 if !stack.mark_entry_submitted(entry_id, pull_request_id.clone()) {
441 return Err(CascadeError::config(format!(
442 "Failed to mark entry {entry_id} as submitted"
443 )));
444 }
445
446 if let Some(commit_meta) = self.metadata.commits.get_mut(&entry_commit_hash) {
448 commit_meta.mark_submitted(pull_request_id);
449 }
450
451 if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
453 let submitted_count = stack.entries.iter().filter(|e| e.is_submitted).count();
454 stack_meta.update_stats(
455 stack.entries.len(),
456 submitted_count,
457 stack_meta.merged_commits,
458 );
459 }
460
461 self.save_to_disk()?;
462
463 Ok(())
464 }
465
466 pub fn repair_all_stacks(&mut self) -> Result<()> {
468 for stack in self.stacks.values_mut() {
469 stack.repair_data_consistency();
470 }
471 self.save_to_disk()?;
472 Ok(())
473 }
474
475 pub fn get_all_stacks(&self) -> Vec<&Stack> {
477 self.stacks.values().collect()
478 }
479
480 pub fn get_stack_metadata(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
482 self.metadata.get_stack(stack_id)
483 }
484
485 pub fn get_repository_metadata(&self) -> &RepositoryMetadata {
487 &self.metadata
488 }
489
490 pub fn git_repo(&self) -> &GitRepository {
492 &self.repo
493 }
494
495 pub fn repo_path(&self) -> &Path {
497 &self.repo_path
498 }
499
500 pub fn is_in_edit_mode(&self) -> bool {
504 self.metadata
505 .edit_mode
506 .as_ref()
507 .map(|edit_state| edit_state.is_active)
508 .unwrap_or(false)
509 }
510
511 pub fn get_edit_mode_info(&self) -> Option<&super::metadata::EditModeState> {
513 self.metadata.edit_mode.as_ref()
514 }
515
516 pub fn enter_edit_mode(&mut self, stack_id: Uuid, entry_id: Uuid) -> Result<()> {
518 let commit_hash = {
520 let stack = self
521 .get_stack(&stack_id)
522 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
523
524 let entry = stack.get_entry(&entry_id).ok_or_else(|| {
525 CascadeError::config(format!("Entry {entry_id} not found in stack"))
526 })?;
527
528 entry.commit_hash.clone()
529 };
530
531 if self.is_in_edit_mode() {
533 self.exit_edit_mode()?;
534 }
535
536 let edit_state = super::metadata::EditModeState::new(stack_id, entry_id, commit_hash);
538
539 self.metadata.edit_mode = Some(edit_state);
540 self.save_to_disk()?;
541
542 info!(
543 "Entered edit mode for entry {} in stack {}",
544 entry_id, stack_id
545 );
546 Ok(())
547 }
548
549 pub fn exit_edit_mode(&mut self) -> Result<()> {
551 if !self.is_in_edit_mode() {
552 return Err(CascadeError::config("Not currently in edit mode"));
553 }
554
555 self.metadata.edit_mode = None;
557 self.save_to_disk()?;
558
559 info!("Exited edit mode");
560 Ok(())
561 }
562
563 pub fn sync_stack(&mut self, stack_id: &Uuid) -> Result<()> {
565 let stack = self
566 .stacks
567 .get_mut(stack_id)
568 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
569
570 if let Err(integrity_error) = stack.validate_git_integrity(&self.repo) {
572 stack.update_status(StackStatus::Corrupted);
573 return Err(CascadeError::branch(format!(
574 "Stack '{}' Git integrity check failed:\n{}",
575 stack.name, integrity_error
576 )));
577 }
578
579 let mut missing_commits = Vec::new();
581 for entry in &stack.entries {
582 if !self.repo.commit_exists(&entry.commit_hash)? {
583 missing_commits.push(entry.commit_hash.clone());
584 }
585 }
586
587 if !missing_commits.is_empty() {
588 stack.update_status(StackStatus::Corrupted);
589 return Err(CascadeError::branch(format!(
590 "Stack {} has missing commits: {}",
591 stack.name,
592 missing_commits.join(", ")
593 )));
594 }
595
596 if !self.repo.branch_exists_or_fetch(&stack.base_branch)? {
598 return Err(CascadeError::branch(format!(
599 "Base branch '{}' does not exist locally or remotely. Check the branch name or switch to a different base.",
600 stack.base_branch
601 )));
602 }
603
604 let _base_hash = self.repo.get_branch_head(&stack.base_branch)?;
605
606 let mut corrupted_entry = None;
608 for entry in &stack.entries {
609 if !self.repo.commit_exists(&entry.commit_hash)? {
610 corrupted_entry = Some((entry.commit_hash.clone(), entry.branch.clone()));
611 break;
612 }
613 }
614
615 if let Some((commit_hash, branch)) = corrupted_entry {
616 stack.update_status(StackStatus::Corrupted);
617 return Err(CascadeError::branch(format!(
618 "Commit {commit_hash} from stack entry '{branch}' no longer exists"
619 )));
620 }
621
622 let needs_sync = if let Some(first_entry) = stack.entries.first() {
624 match self
626 .repo
627 .get_commits_between(&stack.base_branch, &first_entry.commit_hash)
628 {
629 Ok(commits) => !commits.is_empty(), Err(_) => true, }
632 } else {
633 false };
635
636 if needs_sync {
638 stack.update_status(StackStatus::NeedsSync);
639 info!(
640 "Stack '{}' needs sync - new commits on base branch",
641 stack.name
642 );
643 } else {
644 stack.update_status(StackStatus::Clean);
645 info!("Stack '{}' is clean", stack.name);
646 }
647
648 if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
650 stack_meta.set_up_to_date(true);
651 }
652
653 self.save_to_disk()?;
654
655 Ok(())
656 }
657
658 pub fn list_stacks(&self) -> Vec<(Uuid, &str, &StackStatus, usize, Option<&str>)> {
660 self.stacks
661 .values()
662 .map(|stack| {
663 (
664 stack.id,
665 stack.name.as_str(),
666 &stack.status,
667 stack.entries.len(),
668 if stack.is_active {
669 Some("active")
670 } else {
671 None
672 },
673 )
674 })
675 .collect()
676 }
677
678 pub fn get_all_stacks_objects(&self) -> Result<Vec<Stack>> {
680 let mut stacks: Vec<Stack> = self.stacks.values().cloned().collect();
681 stacks.sort_by(|a, b| a.name.cmp(&b.name));
682 Ok(stacks)
683 }
684
685 pub fn validate_all(&self) -> Result<()> {
687 for stack in self.stacks.values() {
688 stack.validate().map_err(|e| {
690 CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
691 })?;
692
693 stack.validate_git_integrity(&self.repo).map_err(|e| {
695 CascadeError::config(format!(
696 "Stack '{}' Git integrity validation failed: {}",
697 stack.name, e
698 ))
699 })?;
700 }
701 Ok(())
702 }
703
704 pub fn validate_stack(&self, stack_id: &Uuid) -> Result<()> {
706 let stack = self
707 .stacks
708 .get(stack_id)
709 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
710
711 stack.validate().map_err(|e| {
713 CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
714 })?;
715
716 stack.validate_git_integrity(&self.repo).map_err(|e| {
718 CascadeError::config(format!(
719 "Stack '{}' Git integrity validation failed: {}",
720 stack.name, e
721 ))
722 })?;
723
724 Ok(())
725 }
726
727 fn save_to_disk(&self) -> Result<()> {
729 if !self.config_dir.exists() {
731 fs::create_dir_all(&self.config_dir).map_err(|e| {
732 CascadeError::config(format!("Failed to create config directory: {e}"))
733 })?;
734 }
735
736 crate::utils::atomic_file::write_json(&self.stacks_file, &self.stacks)?;
738
739 crate::utils::atomic_file::write_json(&self.metadata_file, &self.metadata)?;
741
742 Ok(())
743 }
744
745 fn load_from_disk(&mut self) -> Result<()> {
747 if self.stacks_file.exists() {
749 let stacks_content = fs::read_to_string(&self.stacks_file)
750 .map_err(|e| CascadeError::config(format!("Failed to read stacks file: {e}")))?;
751
752 self.stacks = serde_json::from_str(&stacks_content)
753 .map_err(|e| CascadeError::config(format!("Failed to parse stacks file: {e}")))?;
754 }
755
756 if self.metadata_file.exists() {
758 let metadata_content = fs::read_to_string(&self.metadata_file)
759 .map_err(|e| CascadeError::config(format!("Failed to read metadata file: {e}")))?;
760
761 self.metadata = serde_json::from_str(&metadata_content)
762 .map_err(|e| CascadeError::config(format!("Failed to parse metadata file: {e}")))?;
763 }
764
765 Ok(())
766 }
767
768 pub fn check_for_branch_change(&mut self) -> Result<bool> {
771 let (stack_id, stack_name, stored_branch) = {
773 if let Some(active_stack) = self.get_active_stack() {
774 let stack_id = active_stack.id;
775 let stack_name = active_stack.name.clone();
776 let stored_branch = if let Some(stack_meta) = self.metadata.get_stack(&stack_id) {
777 stack_meta.current_branch.clone()
778 } else {
779 None
780 };
781 (Some(stack_id), stack_name, stored_branch)
782 } else {
783 (None, String::new(), None)
784 }
785 };
786
787 let Some(stack_id) = stack_id else {
789 return Ok(true);
790 };
791
792 let current_branch = self.repo.get_current_branch().ok();
793
794 if stored_branch.as_ref() != current_branch.as_ref() {
796 println!("ā ļø Branch change detected!");
797 println!(
798 " Stack '{}' was active on: {}",
799 stack_name,
800 stored_branch.as_deref().unwrap_or("unknown")
801 );
802 println!(
803 " Current branch: {}",
804 current_branch.as_deref().unwrap_or("unknown")
805 );
806 println!();
807 println!("What would you like to do?");
808 println!(" 1. Keep stack '{stack_name}' active (continue with stack workflow)");
809 println!(" 2. Deactivate stack (use normal Git workflow)");
810 println!(" 3. Switch to a different stack");
811 println!(" 4. Cancel and stay on current workflow");
812 print!(" Choice (1-4): ");
813
814 use std::io::{self, Write};
815 io::stdout()
816 .flush()
817 .map_err(|e| CascadeError::config(format!("Failed to write to stdout: {e}")))?;
818
819 let mut input = String::new();
820 io::stdin()
821 .read_line(&mut input)
822 .map_err(|e| CascadeError::config(format!("Failed to read user input: {e}")))?;
823
824 match input.trim() {
825 "1" => {
826 if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
828 stack_meta.set_current_branch(current_branch);
829 }
830 self.save_to_disk()?;
831 println!("ā
Continuing with stack '{stack_name}' on current branch");
832 return Ok(true);
833 }
834 "2" => {
835 self.set_active_stack(None)?;
837 println!("ā
Deactivated stack '{stack_name}' - using normal Git workflow");
838 return Ok(false);
839 }
840 "3" => {
841 let stacks = self.get_all_stacks();
843 if stacks.len() <= 1 {
844 println!("ā ļø No other stacks available. Deactivating current stack.");
845 self.set_active_stack(None)?;
846 return Ok(false);
847 }
848
849 println!("\nAvailable stacks:");
850 for (i, stack) in stacks.iter().enumerate() {
851 if stack.id != stack_id {
852 println!(" {}. {}", i + 1, stack.name);
853 }
854 }
855 print!(" Enter stack name: ");
856 io::stdout().flush().map_err(|e| {
857 CascadeError::config(format!("Failed to write to stdout: {e}"))
858 })?;
859
860 let mut stack_name_input = String::new();
861 io::stdin().read_line(&mut stack_name_input).map_err(|e| {
862 CascadeError::config(format!("Failed to read user input: {e}"))
863 })?;
864 let stack_name_input = stack_name_input.trim();
865
866 if let Err(e) = self.set_active_stack_by_name(stack_name_input) {
867 println!("ā ļø {e}");
868 println!(" Deactivating stack instead.");
869 self.set_active_stack(None)?;
870 return Ok(false);
871 } else {
872 println!("ā
Switched to stack '{stack_name_input}'");
873 return Ok(true);
874 }
875 }
876 _ => {
877 println!("Cancelled - no changes made");
878 return Ok(false);
879 }
880 }
881 }
882
883 Ok(true)
885 }
886
887 pub fn handle_branch_modifications(
890 &mut self,
891 stack_id: &Uuid,
892 auto_mode: Option<String>,
893 ) -> Result<()> {
894 let stack = self
895 .stacks
896 .get_mut(stack_id)
897 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
898
899 info!("Checking Git integrity for stack '{}'", stack.name);
900
901 let mut modifications = Vec::new();
903 for entry in &stack.entries {
904 if !self.repo.branch_exists(&entry.branch) {
905 modifications.push(BranchModification::Missing {
906 branch: entry.branch.clone(),
907 entry_id: entry.id,
908 expected_commit: entry.commit_hash.clone(),
909 });
910 } else if let Ok(branch_head) = self.repo.get_branch_head(&entry.branch) {
911 if branch_head != entry.commit_hash {
912 let extra_commits = self
914 .repo
915 .get_commits_between(&entry.commit_hash, &branch_head)?;
916 let mut extra_messages = Vec::new();
917 for commit in &extra_commits {
918 if let Some(message) = commit.message() {
919 let first_line =
920 message.lines().next().unwrap_or("(no message)").to_string();
921 extra_messages.push(format!(
922 "{}: {}",
923 &commit.id().to_string()[..8],
924 first_line
925 ));
926 }
927 }
928
929 modifications.push(BranchModification::ExtraCommits {
930 branch: entry.branch.clone(),
931 entry_id: entry.id,
932 expected_commit: entry.commit_hash.clone(),
933 actual_commit: branch_head,
934 extra_commit_count: extra_commits.len(),
935 extra_commit_messages: extra_messages,
936 });
937 }
938 }
939 }
940
941 if modifications.is_empty() {
942 println!("ā
Stack '{}' has no Git integrity issues", stack.name);
943 return Ok(());
944 }
945
946 println!(
948 "š Detected branch modifications in stack '{}':",
949 stack.name
950 );
951 for (i, modification) in modifications.iter().enumerate() {
952 match modification {
953 BranchModification::Missing { branch, .. } => {
954 println!(" {}. Branch '{}' is missing", i + 1, branch);
955 }
956 BranchModification::ExtraCommits {
957 branch,
958 expected_commit,
959 actual_commit,
960 extra_commit_count,
961 extra_commit_messages,
962 ..
963 } => {
964 println!(
965 " {}. Branch '{}' has {} extra commit(s)",
966 i + 1,
967 branch,
968 extra_commit_count
969 );
970 println!(
971 " Expected: {} | Actual: {}",
972 &expected_commit[..8],
973 &actual_commit[..8]
974 );
975
976 for (j, message) in extra_commit_messages.iter().enumerate() {
978 if j < 3 {
979 println!(" + {message}");
980 } else if j == 3 {
981 println!(" + ... and {} more", extra_commit_count - 3);
982 break;
983 }
984 }
985 }
986 }
987 }
988 println!();
989
990 if let Some(mode) = auto_mode {
992 return self.apply_auto_fix(stack_id, &modifications, &mode);
993 }
994
995 for modification in modifications {
997 self.handle_single_modification(stack_id, &modification)?;
998 }
999
1000 self.save_to_disk()?;
1001 println!("š All branch modifications handled successfully!");
1002 Ok(())
1003 }
1004
1005 fn handle_single_modification(
1007 &mut self,
1008 stack_id: &Uuid,
1009 modification: &BranchModification,
1010 ) -> Result<()> {
1011 match modification {
1012 BranchModification::Missing {
1013 branch,
1014 expected_commit,
1015 ..
1016 } => {
1017 println!("š§ Missing branch '{branch}'");
1018 println!(
1019 " This will create the branch at commit {}",
1020 &expected_commit[..8]
1021 );
1022
1023 self.repo.create_branch(branch, Some(expected_commit))?;
1024 println!(" ā
Created branch '{branch}'");
1025 }
1026
1027 BranchModification::ExtraCommits {
1028 branch,
1029 entry_id,
1030 expected_commit,
1031 extra_commit_count,
1032 ..
1033 } => {
1034 println!(
1035 "š¤ Branch '{branch}' has {extra_commit_count} extra commit(s). What would you like to do?"
1036 );
1037 println!(" 1. š Incorporate - Update stack entry to include extra commits");
1038 println!(" 2. ā Split - Create new stack entry for extra commits");
1039 println!(" 3. šļø Reset - Remove extra commits (DESTRUCTIVE)");
1040 println!(" 4. āļø Skip - Leave as-is for now");
1041 print!(" Choice (1-4): ");
1042
1043 use std::io::{self, Write};
1044 io::stdout().flush().unwrap();
1045
1046 let mut input = String::new();
1047 io::stdin().read_line(&mut input).unwrap();
1048
1049 match input.trim() {
1050 "1" | "incorporate" | "inc" => {
1051 self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1052 }
1053 "2" | "split" | "new" => {
1054 self.split_extra_commits(stack_id, *entry_id, branch)?;
1055 }
1056 "3" | "reset" | "remove" => {
1057 self.reset_branch_destructive(branch, expected_commit)?;
1058 }
1059 "4" | "skip" | "ignore" => {
1060 println!(" āļø Skipping '{branch}' (integrity issue remains)");
1061 }
1062 _ => {
1063 println!(" ā Invalid choice. Skipping '{branch}'");
1064 }
1065 }
1066 }
1067 }
1068
1069 Ok(())
1070 }
1071
1072 fn apply_auto_fix(
1074 &mut self,
1075 stack_id: &Uuid,
1076 modifications: &[BranchModification],
1077 mode: &str,
1078 ) -> Result<()> {
1079 println!("š¤ Applying automatic fix mode: {mode}");
1080
1081 for modification in modifications {
1082 match (modification, mode) {
1083 (
1084 BranchModification::Missing {
1085 branch,
1086 expected_commit,
1087 ..
1088 },
1089 _,
1090 ) => {
1091 self.repo.create_branch(branch, Some(expected_commit))?;
1092 println!(" ā
Created missing branch '{branch}'");
1093 }
1094
1095 (
1096 BranchModification::ExtraCommits {
1097 branch, entry_id, ..
1098 },
1099 "incorporate",
1100 ) => {
1101 self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1102 }
1103
1104 (
1105 BranchModification::ExtraCommits {
1106 branch, entry_id, ..
1107 },
1108 "split",
1109 ) => {
1110 self.split_extra_commits(stack_id, *entry_id, branch)?;
1111 }
1112
1113 (
1114 BranchModification::ExtraCommits {
1115 branch,
1116 expected_commit,
1117 ..
1118 },
1119 "reset",
1120 ) => {
1121 self.reset_branch_destructive(branch, expected_commit)?;
1122 }
1123
1124 _ => {
1125 return Err(CascadeError::config(format!(
1126 "Unknown auto-fix mode '{mode}'. Use: incorporate, split, reset"
1127 )));
1128 }
1129 }
1130 }
1131
1132 self.save_to_disk()?;
1133 println!("š Auto-fix completed for mode: {mode}");
1134 Ok(())
1135 }
1136
1137 fn incorporate_extra_commits(
1139 &mut self,
1140 stack_id: &Uuid,
1141 entry_id: Uuid,
1142 branch: &str,
1143 ) -> Result<()> {
1144 let stack = self.stacks.get_mut(stack_id).unwrap();
1145
1146 if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == entry_id) {
1147 let new_head = self.repo.get_branch_head(branch)?;
1148 let old_commit = entry.commit_hash[..8].to_string(); let extra_commits = self
1152 .repo
1153 .get_commits_between(&entry.commit_hash, &new_head)?;
1154
1155 entry.commit_hash = new_head.clone();
1157
1158 let mut extra_messages = Vec::new();
1160 for commit in &extra_commits {
1161 if let Some(message) = commit.message() {
1162 let first_line = message.lines().next().unwrap_or("").to_string();
1163 extra_messages.push(first_line);
1164 }
1165 }
1166
1167 if !extra_messages.is_empty() {
1168 entry.message = format!(
1169 "{}\n\nIncorporated commits:\n⢠{}",
1170 entry.message,
1171 extra_messages.join("\n⢠")
1172 );
1173 }
1174
1175 println!(
1176 " ā
Incorporated {} commit(s) into entry '{}'",
1177 extra_commits.len(),
1178 entry.short_hash()
1179 );
1180 println!(" Updated: {} -> {}", old_commit, &new_head[..8]);
1181 }
1182
1183 Ok(())
1184 }
1185
1186 fn split_extra_commits(&mut self, stack_id: &Uuid, entry_id: Uuid, branch: &str) -> Result<()> {
1188 let stack = self.stacks.get_mut(stack_id).unwrap();
1189 let new_head = self.repo.get_branch_head(branch)?;
1190
1191 let entry_position = stack
1193 .entries
1194 .iter()
1195 .position(|e| e.id == entry_id)
1196 .ok_or_else(|| CascadeError::config("Entry not found in stack"))?;
1197
1198 let base_name = branch.trim_end_matches(|c: char| c.is_ascii_digit() || c == '-');
1200 let new_branch = format!("{base_name}-continued");
1201
1202 self.repo.create_branch(&new_branch, Some(&new_head))?;
1204
1205 let original_entry = &stack.entries[entry_position];
1207 let original_commit_hash = original_entry.commit_hash.clone(); let extra_commits = self
1209 .repo
1210 .get_commits_between(&original_commit_hash, &new_head)?;
1211
1212 let mut extra_messages = Vec::new();
1214 for commit in &extra_commits {
1215 if let Some(message) = commit.message() {
1216 let first_line = message.lines().next().unwrap_or("").to_string();
1217 extra_messages.push(first_line);
1218 }
1219 }
1220
1221 let new_message = if extra_messages.len() == 1 {
1222 extra_messages[0].clone()
1223 } else {
1224 format!("Combined changes:\n⢠{}", extra_messages.join("\n⢠"))
1225 };
1226
1227 let now = chrono::Utc::now();
1229 let new_entry = crate::stack::StackEntry {
1230 id: uuid::Uuid::new_v4(),
1231 branch: new_branch.clone(),
1232 commit_hash: new_head,
1233 message: new_message,
1234 parent_id: Some(entry_id), children: Vec::new(),
1236 created_at: now,
1237 updated_at: now,
1238 is_submitted: false,
1239 pull_request_id: None,
1240 is_synced: false,
1241 };
1242
1243 stack.entries.insert(entry_position + 1, new_entry);
1245
1246 self.repo
1248 .reset_branch_to_commit(branch, &original_commit_hash)?;
1249
1250 println!(
1251 " ā
Split {} commit(s) into new entry '{}'",
1252 extra_commits.len(),
1253 new_branch
1254 );
1255 println!(" Original branch '{branch}' reset to expected commit");
1256
1257 Ok(())
1258 }
1259
1260 fn reset_branch_destructive(&self, branch: &str, expected_commit: &str) -> Result<()> {
1262 self.repo.reset_branch_to_commit(branch, expected_commit)?;
1263 println!(
1264 " ā ļø Reset branch '{}' to {} (extra commits lost)",
1265 branch,
1266 &expected_commit[..8]
1267 );
1268 Ok(())
1269 }
1270}
1271
1272#[cfg(test)]
1273mod tests {
1274 use super::*;
1275 use std::process::Command;
1276 use tempfile::TempDir;
1277
1278 fn create_test_repo() -> (TempDir, PathBuf) {
1279 let temp_dir = TempDir::new().unwrap();
1280 let repo_path = temp_dir.path().to_path_buf();
1281
1282 Command::new("git")
1284 .args(["init"])
1285 .current_dir(&repo_path)
1286 .output()
1287 .unwrap();
1288
1289 Command::new("git")
1291 .args(["config", "user.name", "Test User"])
1292 .current_dir(&repo_path)
1293 .output()
1294 .unwrap();
1295
1296 Command::new("git")
1297 .args(["config", "user.email", "test@example.com"])
1298 .current_dir(&repo_path)
1299 .output()
1300 .unwrap();
1301
1302 std::fs::write(repo_path.join("README.md"), "# Test Repo").unwrap();
1304 Command::new("git")
1305 .args(["add", "."])
1306 .current_dir(&repo_path)
1307 .output()
1308 .unwrap();
1309
1310 Command::new("git")
1311 .args(["commit", "-m", "Initial commit"])
1312 .current_dir(&repo_path)
1313 .output()
1314 .unwrap();
1315
1316 crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
1318 .unwrap();
1319
1320 (temp_dir, repo_path)
1321 }
1322
1323 #[test]
1324 fn test_create_stack_manager() {
1325 let (_temp_dir, repo_path) = create_test_repo();
1326 let manager = StackManager::new(&repo_path).unwrap();
1327
1328 assert_eq!(manager.stacks.len(), 0);
1329 assert!(manager.get_active_stack().is_none());
1330 }
1331
1332 #[test]
1333 fn test_create_and_manage_stack() {
1334 let (_temp_dir, repo_path) = create_test_repo();
1335 let mut manager = StackManager::new(&repo_path).unwrap();
1336
1337 let stack_id = manager
1339 .create_stack(
1340 "test-stack".to_string(),
1341 None, Some("Test stack description".to_string()),
1343 )
1344 .unwrap();
1345
1346 assert_eq!(manager.stacks.len(), 1);
1348 let stack = manager.get_stack(&stack_id).unwrap();
1349 assert_eq!(stack.name, "test-stack");
1350 assert!(!stack.base_branch.is_empty());
1352 assert!(stack.is_active);
1353
1354 let active = manager.get_active_stack().unwrap();
1356 assert_eq!(active.id, stack_id);
1357
1358 let found = manager.get_stack_by_name("test-stack").unwrap();
1360 assert_eq!(found.id, stack_id);
1361 }
1362
1363 #[test]
1364 fn test_stack_persistence() {
1365 let (_temp_dir, repo_path) = create_test_repo();
1366
1367 let stack_id = {
1368 let mut manager = StackManager::new(&repo_path).unwrap();
1369 manager
1370 .create_stack("persistent-stack".to_string(), None, None)
1371 .unwrap()
1372 };
1373
1374 let manager = StackManager::new(&repo_path).unwrap();
1376 assert_eq!(manager.stacks.len(), 1);
1377 let stack = manager.get_stack(&stack_id).unwrap();
1378 assert_eq!(stack.name, "persistent-stack");
1379 }
1380
1381 #[test]
1382 fn test_multiple_stacks() {
1383 let (_temp_dir, repo_path) = create_test_repo();
1384 let mut manager = StackManager::new(&repo_path).unwrap();
1385
1386 let stack1_id = manager
1387 .create_stack("stack-1".to_string(), None, None)
1388 .unwrap();
1389 let stack2_id = manager
1390 .create_stack("stack-2".to_string(), None, None)
1391 .unwrap();
1392
1393 assert_eq!(manager.stacks.len(), 2);
1394
1395 assert!(manager.get_stack(&stack1_id).unwrap().is_active);
1397 assert!(!manager.get_stack(&stack2_id).unwrap().is_active);
1398
1399 manager.set_active_stack(Some(stack2_id)).unwrap();
1401 assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1402 assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1403 }
1404
1405 #[test]
1406 fn test_delete_stack() {
1407 let (_temp_dir, repo_path) = create_test_repo();
1408 let mut manager = StackManager::new(&repo_path).unwrap();
1409
1410 let stack_id = manager
1411 .create_stack("to-delete".to_string(), None, None)
1412 .unwrap();
1413 assert_eq!(manager.stacks.len(), 1);
1414
1415 let deleted = manager.delete_stack(&stack_id).unwrap();
1416 assert_eq!(deleted.name, "to-delete");
1417 assert_eq!(manager.stacks.len(), 0);
1418 assert!(manager.get_active_stack().is_none());
1419 }
1420
1421 #[test]
1422 fn test_validation() {
1423 let (_temp_dir, repo_path) = create_test_repo();
1424 let mut manager = StackManager::new(&repo_path).unwrap();
1425
1426 manager
1427 .create_stack("valid-stack".to_string(), None, None)
1428 .unwrap();
1429
1430 assert!(manager.validate_all().is_ok());
1432 }
1433
1434 #[test]
1435 fn test_duplicate_commit_message_detection() {
1436 let (_temp_dir, repo_path) = create_test_repo();
1437 let mut manager = StackManager::new(&repo_path).unwrap();
1438
1439 manager
1441 .create_stack("test-stack".to_string(), None, None)
1442 .unwrap();
1443
1444 std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1446 Command::new("git")
1447 .args(["add", "file1.txt"])
1448 .current_dir(&repo_path)
1449 .output()
1450 .unwrap();
1451
1452 Command::new("git")
1453 .args(["commit", "-m", "Add authentication feature"])
1454 .current_dir(&repo_path)
1455 .output()
1456 .unwrap();
1457
1458 let commit1_hash = Command::new("git")
1459 .args(["rev-parse", "HEAD"])
1460 .current_dir(&repo_path)
1461 .output()
1462 .unwrap();
1463 let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1464 .trim()
1465 .to_string();
1466
1467 let entry1_id = manager
1469 .push_to_stack(
1470 "feature/auth".to_string(),
1471 commit1_hash,
1472 "Add authentication feature".to_string(),
1473 "main".to_string(),
1474 )
1475 .unwrap();
1476
1477 assert!(manager
1479 .get_active_stack()
1480 .unwrap()
1481 .get_entry(&entry1_id)
1482 .is_some());
1483
1484 std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1486 Command::new("git")
1487 .args(["add", "file2.txt"])
1488 .current_dir(&repo_path)
1489 .output()
1490 .unwrap();
1491
1492 Command::new("git")
1493 .args(["commit", "-m", "Different commit message"])
1494 .current_dir(&repo_path)
1495 .output()
1496 .unwrap();
1497
1498 let commit2_hash = Command::new("git")
1499 .args(["rev-parse", "HEAD"])
1500 .current_dir(&repo_path)
1501 .output()
1502 .unwrap();
1503 let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1504 .trim()
1505 .to_string();
1506
1507 let result = manager.push_to_stack(
1509 "feature/auth2".to_string(),
1510 commit2_hash.clone(),
1511 "Add authentication feature".to_string(), "main".to_string(),
1513 );
1514
1515 assert!(result.is_err());
1517 let error = result.unwrap_err();
1518 assert!(matches!(error, CascadeError::Validation(_)));
1519
1520 let error_msg = error.to_string();
1522 assert!(error_msg.contains("Duplicate commit message"));
1523 assert!(error_msg.contains("Add authentication feature"));
1524 assert!(error_msg.contains("š” Consider using a more specific message"));
1525
1526 let entry2_id = manager
1528 .push_to_stack(
1529 "feature/auth2".to_string(),
1530 commit2_hash,
1531 "Add authentication validation".to_string(), "main".to_string(),
1533 )
1534 .unwrap();
1535
1536 let stack = manager.get_active_stack().unwrap();
1538 assert_eq!(stack.entries.len(), 2);
1539 assert!(stack.get_entry(&entry1_id).is_some());
1540 assert!(stack.get_entry(&entry2_id).is_some());
1541 }
1542
1543 #[test]
1544 fn test_duplicate_message_with_different_case() {
1545 let (_temp_dir, repo_path) = create_test_repo();
1546 let mut manager = StackManager::new(&repo_path).unwrap();
1547
1548 manager
1549 .create_stack("test-stack".to_string(), None, None)
1550 .unwrap();
1551
1552 std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1554 Command::new("git")
1555 .args(["add", "file1.txt"])
1556 .current_dir(&repo_path)
1557 .output()
1558 .unwrap();
1559
1560 Command::new("git")
1561 .args(["commit", "-m", "fix bug"])
1562 .current_dir(&repo_path)
1563 .output()
1564 .unwrap();
1565
1566 let commit1_hash = Command::new("git")
1567 .args(["rev-parse", "HEAD"])
1568 .current_dir(&repo_path)
1569 .output()
1570 .unwrap();
1571 let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1572 .trim()
1573 .to_string();
1574
1575 manager
1576 .push_to_stack(
1577 "feature/fix1".to_string(),
1578 commit1_hash,
1579 "fix bug".to_string(),
1580 "main".to_string(),
1581 )
1582 .unwrap();
1583
1584 std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1586 Command::new("git")
1587 .args(["add", "file2.txt"])
1588 .current_dir(&repo_path)
1589 .output()
1590 .unwrap();
1591
1592 Command::new("git")
1593 .args(["commit", "-m", "Fix Bug"])
1594 .current_dir(&repo_path)
1595 .output()
1596 .unwrap();
1597
1598 let commit2_hash = Command::new("git")
1599 .args(["rev-parse", "HEAD"])
1600 .current_dir(&repo_path)
1601 .output()
1602 .unwrap();
1603 let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1604 .trim()
1605 .to_string();
1606
1607 let result = manager.push_to_stack(
1609 "feature/fix2".to_string(),
1610 commit2_hash,
1611 "Fix Bug".to_string(), "main".to_string(),
1613 );
1614
1615 assert!(result.is_ok());
1617 }
1618
1619 #[test]
1620 fn test_duplicate_message_across_different_stacks() {
1621 let (_temp_dir, repo_path) = create_test_repo();
1622 let mut manager = StackManager::new(&repo_path).unwrap();
1623
1624 let stack1_id = manager
1626 .create_stack("stack1".to_string(), None, None)
1627 .unwrap();
1628
1629 std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1630 Command::new("git")
1631 .args(["add", "file1.txt"])
1632 .current_dir(&repo_path)
1633 .output()
1634 .unwrap();
1635
1636 Command::new("git")
1637 .args(["commit", "-m", "shared message"])
1638 .current_dir(&repo_path)
1639 .output()
1640 .unwrap();
1641
1642 let commit1_hash = Command::new("git")
1643 .args(["rev-parse", "HEAD"])
1644 .current_dir(&repo_path)
1645 .output()
1646 .unwrap();
1647 let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1648 .trim()
1649 .to_string();
1650
1651 manager
1652 .push_to_stack(
1653 "feature/shared1".to_string(),
1654 commit1_hash,
1655 "shared message".to_string(),
1656 "main".to_string(),
1657 )
1658 .unwrap();
1659
1660 let stack2_id = manager
1662 .create_stack("stack2".to_string(), None, None)
1663 .unwrap();
1664
1665 manager.set_active_stack(Some(stack2_id)).unwrap();
1667
1668 std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1670 Command::new("git")
1671 .args(["add", "file2.txt"])
1672 .current_dir(&repo_path)
1673 .output()
1674 .unwrap();
1675
1676 Command::new("git")
1677 .args(["commit", "-m", "shared message"])
1678 .current_dir(&repo_path)
1679 .output()
1680 .unwrap();
1681
1682 let commit2_hash = Command::new("git")
1683 .args(["rev-parse", "HEAD"])
1684 .current_dir(&repo_path)
1685 .output()
1686 .unwrap();
1687 let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1688 .trim()
1689 .to_string();
1690
1691 let result = manager.push_to_stack(
1693 "feature/shared2".to_string(),
1694 commit2_hash,
1695 "shared message".to_string(), "main".to_string(),
1697 );
1698
1699 assert!(result.is_ok());
1701
1702 let stack1 = manager.get_stack(&stack1_id).unwrap();
1704 let stack2 = manager.get_stack(&stack2_id).unwrap();
1705
1706 assert_eq!(stack1.entries.len(), 1);
1707 assert_eq!(stack2.entries.len(), 1);
1708 assert_eq!(stack1.entries[0].message, "shared message");
1709 assert_eq!(stack2.entries[0].message, "shared message");
1710 }
1711
1712 #[test]
1713 fn test_duplicate_after_pop() {
1714 let (_temp_dir, repo_path) = create_test_repo();
1715 let mut manager = StackManager::new(&repo_path).unwrap();
1716
1717 manager
1718 .create_stack("test-stack".to_string(), None, None)
1719 .unwrap();
1720
1721 std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1723 Command::new("git")
1724 .args(["add", "file1.txt"])
1725 .current_dir(&repo_path)
1726 .output()
1727 .unwrap();
1728
1729 Command::new("git")
1730 .args(["commit", "-m", "temporary message"])
1731 .current_dir(&repo_path)
1732 .output()
1733 .unwrap();
1734
1735 let commit1_hash = Command::new("git")
1736 .args(["rev-parse", "HEAD"])
1737 .current_dir(&repo_path)
1738 .output()
1739 .unwrap();
1740 let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1741 .trim()
1742 .to_string();
1743
1744 manager
1745 .push_to_stack(
1746 "feature/temp".to_string(),
1747 commit1_hash,
1748 "temporary message".to_string(),
1749 "main".to_string(),
1750 )
1751 .unwrap();
1752
1753 let popped = manager.pop_from_stack().unwrap();
1755 assert_eq!(popped.message, "temporary message");
1756
1757 std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1759 Command::new("git")
1760 .args(["add", "file2.txt"])
1761 .current_dir(&repo_path)
1762 .output()
1763 .unwrap();
1764
1765 Command::new("git")
1766 .args(["commit", "-m", "temporary message"])
1767 .current_dir(&repo_path)
1768 .output()
1769 .unwrap();
1770
1771 let commit2_hash = Command::new("git")
1772 .args(["rev-parse", "HEAD"])
1773 .current_dir(&repo_path)
1774 .output()
1775 .unwrap();
1776 let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1777 .trim()
1778 .to_string();
1779
1780 let result = manager.push_to_stack(
1782 "feature/temp2".to_string(),
1783 commit2_hash,
1784 "temporary message".to_string(),
1785 "main".to_string(),
1786 );
1787
1788 assert!(result.is_ok());
1789 }
1790}