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 self.repo.branch_exists(&branch) {
297 tracing::info!("Branch '{}' already exists, skipping creation", branch);
298 } else {
299 self.repo
301 .create_branch(&branch, Some(&commit_hash))
302 .map_err(|e| {
303 CascadeError::branch(format!(
304 "Failed to create branch '{}' from commit {}: {}",
305 branch,
306 &commit_hash[..8],
307 e
308 ))
309 })?;
310
311 tracing::info!(
312 "ā
Created Git branch '{}' from commit {}",
313 branch,
314 &commit_hash[..8]
315 );
316 }
317
318 let entry_id = stack.push_entry(branch.clone(), commit_hash.clone(), message.clone());
320
321 let commit_metadata = CommitMetadata::new(
323 commit_hash.clone(),
324 message,
325 entry_id,
326 stack_id,
327 branch.clone(),
328 source_branch,
329 );
330
331 self.metadata.add_commit(commit_metadata);
333 if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
334 stack_meta.add_branch(branch);
335 stack_meta.add_commit(commit_hash);
336 }
337
338 self.save_to_disk()?;
339
340 Ok(entry_id)
341 }
342
343 pub fn pop_from_stack(&mut self) -> Result<StackEntry> {
345 let stack_id = self
346 .metadata
347 .active_stack_id
348 .ok_or_else(|| CascadeError::config("No active stack"))?;
349
350 let stack = self
351 .stacks
352 .get_mut(&stack_id)
353 .ok_or_else(|| CascadeError::config("Active stack not found"))?;
354
355 let entry = stack
356 .pop_entry()
357 .ok_or_else(|| CascadeError::config("Stack is empty"))?;
358
359 self.metadata.remove_commit(&entry.commit_hash);
361
362 if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
364 stack_meta.remove_commit(&entry.commit_hash);
365 }
367
368 self.save_to_disk()?;
369
370 Ok(entry)
371 }
372
373 pub fn submit_entry(
375 &mut self,
376 stack_id: &Uuid,
377 entry_id: &Uuid,
378 pull_request_id: String,
379 ) -> Result<()> {
380 let stack = self
381 .stacks
382 .get_mut(stack_id)
383 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
384
385 let entry_commit_hash = {
386 let entry = stack
387 .get_entry(entry_id)
388 .ok_or_else(|| CascadeError::config(format!("Entry {entry_id} not found")))?;
389 entry.commit_hash.clone()
390 };
391
392 if !stack.mark_entry_submitted(entry_id, pull_request_id.clone()) {
394 return Err(CascadeError::config(format!(
395 "Failed to mark entry {entry_id} as submitted"
396 )));
397 }
398
399 if let Some(commit_meta) = self.metadata.commits.get_mut(&entry_commit_hash) {
401 commit_meta.mark_submitted(pull_request_id);
402 }
403
404 if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
406 let submitted_count = stack.entries.iter().filter(|e| e.is_submitted).count();
407 stack_meta.update_stats(
408 stack.entries.len(),
409 submitted_count,
410 stack_meta.merged_commits,
411 );
412 }
413
414 self.save_to_disk()?;
415
416 Ok(())
417 }
418
419 pub fn repair_all_stacks(&mut self) -> Result<()> {
421 for stack in self.stacks.values_mut() {
422 stack.repair_data_consistency();
423 }
424 self.save_to_disk()?;
425 Ok(())
426 }
427
428 pub fn get_all_stacks(&self) -> Vec<&Stack> {
430 self.stacks.values().collect()
431 }
432
433 pub fn get_stack_metadata(&self, stack_id: &Uuid) -> Option<&StackMetadata> {
435 self.metadata.get_stack(stack_id)
436 }
437
438 pub fn get_repository_metadata(&self) -> &RepositoryMetadata {
440 &self.metadata
441 }
442
443 pub fn git_repo(&self) -> &GitRepository {
445 &self.repo
446 }
447
448 pub fn repo_path(&self) -> &Path {
450 &self.repo_path
451 }
452
453 pub fn is_in_edit_mode(&self) -> bool {
457 self.metadata
458 .edit_mode
459 .as_ref()
460 .map(|edit_state| edit_state.is_active)
461 .unwrap_or(false)
462 }
463
464 pub fn get_edit_mode_info(&self) -> Option<&super::metadata::EditModeState> {
466 self.metadata.edit_mode.as_ref()
467 }
468
469 pub fn enter_edit_mode(&mut self, stack_id: Uuid, entry_id: Uuid) -> Result<()> {
471 let commit_hash = {
473 let stack = self
474 .get_stack(&stack_id)
475 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
476
477 let entry = stack.get_entry(&entry_id).ok_or_else(|| {
478 CascadeError::config(format!("Entry {entry_id} not found in stack"))
479 })?;
480
481 entry.commit_hash.clone()
482 };
483
484 if self.is_in_edit_mode() {
486 self.exit_edit_mode()?;
487 }
488
489 let edit_state = super::metadata::EditModeState::new(stack_id, entry_id, commit_hash);
491
492 self.metadata.edit_mode = Some(edit_state);
493 self.save_to_disk()?;
494
495 info!(
496 "Entered edit mode for entry {} in stack {}",
497 entry_id, stack_id
498 );
499 Ok(())
500 }
501
502 pub fn exit_edit_mode(&mut self) -> Result<()> {
504 if !self.is_in_edit_mode() {
505 return Err(CascadeError::config("Not currently in edit mode"));
506 }
507
508 self.metadata.edit_mode = None;
510 self.save_to_disk()?;
511
512 info!("Exited edit mode");
513 Ok(())
514 }
515
516 pub fn sync_stack(&mut self, stack_id: &Uuid) -> Result<()> {
518 let stack = self
519 .stacks
520 .get_mut(stack_id)
521 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
522
523 if let Err(integrity_error) = stack.validate_git_integrity(&self.repo) {
525 stack.update_status(StackStatus::Corrupted);
526 return Err(CascadeError::branch(format!(
527 "Stack '{}' Git integrity check failed:\n{}",
528 stack.name, integrity_error
529 )));
530 }
531
532 let mut missing_commits = Vec::new();
534 for entry in &stack.entries {
535 if !self.repo.commit_exists(&entry.commit_hash)? {
536 missing_commits.push(entry.commit_hash.clone());
537 }
538 }
539
540 if !missing_commits.is_empty() {
541 stack.update_status(StackStatus::Corrupted);
542 return Err(CascadeError::branch(format!(
543 "Stack {} has missing commits: {}",
544 stack.name,
545 missing_commits.join(", ")
546 )));
547 }
548
549 if !self.repo.branch_exists_or_fetch(&stack.base_branch)? {
551 return Err(CascadeError::branch(format!(
552 "Base branch '{}' does not exist locally or remotely. Check the branch name or switch to a different base.",
553 stack.base_branch
554 )));
555 }
556
557 let _base_hash = self.repo.get_branch_head(&stack.base_branch)?;
558
559 let mut corrupted_entry = None;
561 for entry in &stack.entries {
562 if !self.repo.commit_exists(&entry.commit_hash)? {
563 corrupted_entry = Some((entry.commit_hash.clone(), entry.branch.clone()));
564 break;
565 }
566 }
567
568 if let Some((commit_hash, branch)) = corrupted_entry {
569 stack.update_status(StackStatus::Corrupted);
570 return Err(CascadeError::branch(format!(
571 "Commit {commit_hash} from stack entry '{branch}' no longer exists"
572 )));
573 }
574
575 let needs_sync = if let Some(first_entry) = stack.entries.first() {
577 match self
579 .repo
580 .get_commits_between(&stack.base_branch, &first_entry.commit_hash)
581 {
582 Ok(commits) => !commits.is_empty(), Err(_) => true, }
585 } else {
586 false };
588
589 if needs_sync {
591 stack.update_status(StackStatus::NeedsSync);
592 info!(
593 "Stack '{}' needs sync - new commits on base branch",
594 stack.name
595 );
596 } else {
597 stack.update_status(StackStatus::Clean);
598 info!("Stack '{}' is clean", stack.name);
599 }
600
601 if let Some(stack_meta) = self.metadata.get_stack_mut(stack_id) {
603 stack_meta.set_up_to_date(true);
604 }
605
606 self.save_to_disk()?;
607
608 Ok(())
609 }
610
611 pub fn list_stacks(&self) -> Vec<(Uuid, &str, &StackStatus, usize, Option<&str>)> {
613 self.stacks
614 .values()
615 .map(|stack| {
616 (
617 stack.id,
618 stack.name.as_str(),
619 &stack.status,
620 stack.entries.len(),
621 if stack.is_active {
622 Some("active")
623 } else {
624 None
625 },
626 )
627 })
628 .collect()
629 }
630
631 pub fn get_all_stacks_objects(&self) -> Result<Vec<Stack>> {
633 let mut stacks: Vec<Stack> = self.stacks.values().cloned().collect();
634 stacks.sort_by(|a, b| a.name.cmp(&b.name));
635 Ok(stacks)
636 }
637
638 pub fn validate_all(&self) -> Result<()> {
640 for stack in self.stacks.values() {
641 stack.validate().map_err(|e| {
643 CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
644 })?;
645
646 stack.validate_git_integrity(&self.repo).map_err(|e| {
648 CascadeError::config(format!(
649 "Stack '{}' Git integrity validation failed: {}",
650 stack.name, e
651 ))
652 })?;
653 }
654 Ok(())
655 }
656
657 pub fn validate_stack(&self, stack_id: &Uuid) -> Result<()> {
659 let stack = self
660 .stacks
661 .get(stack_id)
662 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
663
664 stack.validate().map_err(|e| {
666 CascadeError::config(format!("Stack '{}' validation failed: {}", stack.name, e))
667 })?;
668
669 stack.validate_git_integrity(&self.repo).map_err(|e| {
671 CascadeError::config(format!(
672 "Stack '{}' Git integrity validation failed: {}",
673 stack.name, e
674 ))
675 })?;
676
677 Ok(())
678 }
679
680 fn save_to_disk(&self) -> Result<()> {
682 if !self.config_dir.exists() {
684 fs::create_dir_all(&self.config_dir).map_err(|e| {
685 CascadeError::config(format!("Failed to create config directory: {e}"))
686 })?;
687 }
688
689 crate::utils::atomic_file::write_json(&self.stacks_file, &self.stacks)?;
691
692 crate::utils::atomic_file::write_json(&self.metadata_file, &self.metadata)?;
694
695 Ok(())
696 }
697
698 fn load_from_disk(&mut self) -> Result<()> {
700 if self.stacks_file.exists() {
702 let stacks_content = fs::read_to_string(&self.stacks_file)
703 .map_err(|e| CascadeError::config(format!("Failed to read stacks file: {e}")))?;
704
705 self.stacks = serde_json::from_str(&stacks_content)
706 .map_err(|e| CascadeError::config(format!("Failed to parse stacks file: {e}")))?;
707 }
708
709 if self.metadata_file.exists() {
711 let metadata_content = fs::read_to_string(&self.metadata_file)
712 .map_err(|e| CascadeError::config(format!("Failed to read metadata file: {e}")))?;
713
714 self.metadata = serde_json::from_str(&metadata_content)
715 .map_err(|e| CascadeError::config(format!("Failed to parse metadata file: {e}")))?;
716 }
717
718 Ok(())
719 }
720
721 pub fn check_for_branch_change(&mut self) -> Result<bool> {
724 let (stack_id, stack_name, stored_branch) = {
726 if let Some(active_stack) = self.get_active_stack() {
727 let stack_id = active_stack.id;
728 let stack_name = active_stack.name.clone();
729 let stored_branch = if let Some(stack_meta) = self.metadata.get_stack(&stack_id) {
730 stack_meta.current_branch.clone()
731 } else {
732 None
733 };
734 (Some(stack_id), stack_name, stored_branch)
735 } else {
736 (None, String::new(), None)
737 }
738 };
739
740 let Some(stack_id) = stack_id else {
742 return Ok(true);
743 };
744
745 let current_branch = self.repo.get_current_branch().ok();
746
747 if stored_branch.as_ref() != current_branch.as_ref() {
749 println!("ā ļø Branch change detected!");
750 println!(
751 " Stack '{}' was active on: {}",
752 stack_name,
753 stored_branch.as_deref().unwrap_or("unknown")
754 );
755 println!(
756 " Current branch: {}",
757 current_branch.as_deref().unwrap_or("unknown")
758 );
759 println!();
760 println!("What would you like to do?");
761 println!(" 1. Keep stack '{stack_name}' active (continue with stack workflow)");
762 println!(" 2. Deactivate stack (use normal Git workflow)");
763 println!(" 3. Switch to a different stack");
764 println!(" 4. Cancel and stay on current workflow");
765 print!(" Choice (1-4): ");
766
767 use std::io::{self, Write};
768 io::stdout()
769 .flush()
770 .map_err(|e| CascadeError::config(format!("Failed to write to stdout: {e}")))?;
771
772 let mut input = String::new();
773 io::stdin()
774 .read_line(&mut input)
775 .map_err(|e| CascadeError::config(format!("Failed to read user input: {e}")))?;
776
777 match input.trim() {
778 "1" => {
779 if let Some(stack_meta) = self.metadata.get_stack_mut(&stack_id) {
781 stack_meta.set_current_branch(current_branch);
782 }
783 self.save_to_disk()?;
784 println!("ā
Continuing with stack '{stack_name}' on current branch");
785 return Ok(true);
786 }
787 "2" => {
788 self.set_active_stack(None)?;
790 println!("ā
Deactivated stack '{stack_name}' - using normal Git workflow");
791 return Ok(false);
792 }
793 "3" => {
794 let stacks = self.get_all_stacks();
796 if stacks.len() <= 1 {
797 println!("ā ļø No other stacks available. Deactivating current stack.");
798 self.set_active_stack(None)?;
799 return Ok(false);
800 }
801
802 println!("\nAvailable stacks:");
803 for (i, stack) in stacks.iter().enumerate() {
804 if stack.id != stack_id {
805 println!(" {}. {}", i + 1, stack.name);
806 }
807 }
808 print!(" Enter stack name: ");
809 io::stdout().flush().map_err(|e| {
810 CascadeError::config(format!("Failed to write to stdout: {e}"))
811 })?;
812
813 let mut stack_name_input = String::new();
814 io::stdin().read_line(&mut stack_name_input).map_err(|e| {
815 CascadeError::config(format!("Failed to read user input: {e}"))
816 })?;
817 let stack_name_input = stack_name_input.trim();
818
819 if let Err(e) = self.set_active_stack_by_name(stack_name_input) {
820 println!("ā ļø {e}");
821 println!(" Deactivating stack instead.");
822 self.set_active_stack(None)?;
823 return Ok(false);
824 } else {
825 println!("ā
Switched to stack '{stack_name_input}'");
826 return Ok(true);
827 }
828 }
829 _ => {
830 println!("Cancelled - no changes made");
831 return Ok(false);
832 }
833 }
834 }
835
836 Ok(true)
838 }
839
840 pub fn handle_branch_modifications(
843 &mut self,
844 stack_id: &Uuid,
845 auto_mode: Option<String>,
846 ) -> Result<()> {
847 let stack = self
848 .stacks
849 .get_mut(stack_id)
850 .ok_or_else(|| CascadeError::config(format!("Stack {stack_id} not found")))?;
851
852 info!("Checking Git integrity for stack '{}'", stack.name);
853
854 let mut modifications = Vec::new();
856 for entry in &stack.entries {
857 if !self.repo.branch_exists(&entry.branch) {
858 modifications.push(BranchModification::Missing {
859 branch: entry.branch.clone(),
860 entry_id: entry.id,
861 expected_commit: entry.commit_hash.clone(),
862 });
863 } else if let Ok(branch_head) = self.repo.get_branch_head(&entry.branch) {
864 if branch_head != entry.commit_hash {
865 let extra_commits = self
867 .repo
868 .get_commits_between(&entry.commit_hash, &branch_head)?;
869 let mut extra_messages = Vec::new();
870 for commit in &extra_commits {
871 if let Some(message) = commit.message() {
872 let first_line =
873 message.lines().next().unwrap_or("(no message)").to_string();
874 extra_messages.push(format!(
875 "{}: {}",
876 &commit.id().to_string()[..8],
877 first_line
878 ));
879 }
880 }
881
882 modifications.push(BranchModification::ExtraCommits {
883 branch: entry.branch.clone(),
884 entry_id: entry.id,
885 expected_commit: entry.commit_hash.clone(),
886 actual_commit: branch_head,
887 extra_commit_count: extra_commits.len(),
888 extra_commit_messages: extra_messages,
889 });
890 }
891 }
892 }
893
894 if modifications.is_empty() {
895 println!("ā
Stack '{}' has no Git integrity issues", stack.name);
896 return Ok(());
897 }
898
899 println!(
901 "š Detected branch modifications in stack '{}':",
902 stack.name
903 );
904 for (i, modification) in modifications.iter().enumerate() {
905 match modification {
906 BranchModification::Missing { branch, .. } => {
907 println!(" {}. Branch '{}' is missing", i + 1, branch);
908 }
909 BranchModification::ExtraCommits {
910 branch,
911 expected_commit,
912 actual_commit,
913 extra_commit_count,
914 extra_commit_messages,
915 ..
916 } => {
917 println!(
918 " {}. Branch '{}' has {} extra commit(s)",
919 i + 1,
920 branch,
921 extra_commit_count
922 );
923 println!(
924 " Expected: {} | Actual: {}",
925 &expected_commit[..8],
926 &actual_commit[..8]
927 );
928
929 for (j, message) in extra_commit_messages.iter().enumerate() {
931 if j < 3 {
932 println!(" + {message}");
933 } else if j == 3 {
934 println!(" + ... and {} more", extra_commit_count - 3);
935 break;
936 }
937 }
938 }
939 }
940 }
941 println!();
942
943 if let Some(mode) = auto_mode {
945 return self.apply_auto_fix(stack_id, &modifications, &mode);
946 }
947
948 for modification in modifications {
950 self.handle_single_modification(stack_id, &modification)?;
951 }
952
953 self.save_to_disk()?;
954 println!("š All branch modifications handled successfully!");
955 Ok(())
956 }
957
958 fn handle_single_modification(
960 &mut self,
961 stack_id: &Uuid,
962 modification: &BranchModification,
963 ) -> Result<()> {
964 match modification {
965 BranchModification::Missing {
966 branch,
967 expected_commit,
968 ..
969 } => {
970 println!("š§ Missing branch '{branch}'");
971 println!(
972 " This will create the branch at commit {}",
973 &expected_commit[..8]
974 );
975
976 self.repo.create_branch(branch, Some(expected_commit))?;
977 println!(" ā
Created branch '{branch}'");
978 }
979
980 BranchModification::ExtraCommits {
981 branch,
982 entry_id,
983 expected_commit,
984 extra_commit_count,
985 ..
986 } => {
987 println!(
988 "š¤ Branch '{branch}' has {extra_commit_count} extra commit(s). What would you like to do?"
989 );
990 println!(" 1. š Incorporate - Update stack entry to include extra commits");
991 println!(" 2. ā Split - Create new stack entry for extra commits");
992 println!(" 3. šļø Reset - Remove extra commits (DESTRUCTIVE)");
993 println!(" 4. āļø Skip - Leave as-is for now");
994 print!(" Choice (1-4): ");
995
996 use std::io::{self, Write};
997 io::stdout().flush().unwrap();
998
999 let mut input = String::new();
1000 io::stdin().read_line(&mut input).unwrap();
1001
1002 match input.trim() {
1003 "1" | "incorporate" | "inc" => {
1004 self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1005 }
1006 "2" | "split" | "new" => {
1007 self.split_extra_commits(stack_id, *entry_id, branch)?;
1008 }
1009 "3" | "reset" | "remove" => {
1010 self.reset_branch_destructive(branch, expected_commit)?;
1011 }
1012 "4" | "skip" | "ignore" => {
1013 println!(" āļø Skipping '{branch}' (integrity issue remains)");
1014 }
1015 _ => {
1016 println!(" ā Invalid choice. Skipping '{branch}'");
1017 }
1018 }
1019 }
1020 }
1021
1022 Ok(())
1023 }
1024
1025 fn apply_auto_fix(
1027 &mut self,
1028 stack_id: &Uuid,
1029 modifications: &[BranchModification],
1030 mode: &str,
1031 ) -> Result<()> {
1032 println!("š¤ Applying automatic fix mode: {mode}");
1033
1034 for modification in modifications {
1035 match (modification, mode) {
1036 (
1037 BranchModification::Missing {
1038 branch,
1039 expected_commit,
1040 ..
1041 },
1042 _,
1043 ) => {
1044 self.repo.create_branch(branch, Some(expected_commit))?;
1045 println!(" ā
Created missing branch '{branch}'");
1046 }
1047
1048 (
1049 BranchModification::ExtraCommits {
1050 branch, entry_id, ..
1051 },
1052 "incorporate",
1053 ) => {
1054 self.incorporate_extra_commits(stack_id, *entry_id, branch)?;
1055 }
1056
1057 (
1058 BranchModification::ExtraCommits {
1059 branch, entry_id, ..
1060 },
1061 "split",
1062 ) => {
1063 self.split_extra_commits(stack_id, *entry_id, branch)?;
1064 }
1065
1066 (
1067 BranchModification::ExtraCommits {
1068 branch,
1069 expected_commit,
1070 ..
1071 },
1072 "reset",
1073 ) => {
1074 self.reset_branch_destructive(branch, expected_commit)?;
1075 }
1076
1077 _ => {
1078 return Err(CascadeError::config(format!(
1079 "Unknown auto-fix mode '{mode}'. Use: incorporate, split, reset"
1080 )));
1081 }
1082 }
1083 }
1084
1085 self.save_to_disk()?;
1086 println!("š Auto-fix completed for mode: {mode}");
1087 Ok(())
1088 }
1089
1090 fn incorporate_extra_commits(
1092 &mut self,
1093 stack_id: &Uuid,
1094 entry_id: Uuid,
1095 branch: &str,
1096 ) -> Result<()> {
1097 let stack = self.stacks.get_mut(stack_id).unwrap();
1098
1099 if let Some(entry) = stack.entries.iter_mut().find(|e| e.id == entry_id) {
1100 let new_head = self.repo.get_branch_head(branch)?;
1101 let old_commit = entry.commit_hash[..8].to_string(); let extra_commits = self
1105 .repo
1106 .get_commits_between(&entry.commit_hash, &new_head)?;
1107
1108 entry.commit_hash = new_head.clone();
1110
1111 let mut extra_messages = Vec::new();
1113 for commit in &extra_commits {
1114 if let Some(message) = commit.message() {
1115 let first_line = message.lines().next().unwrap_or("").to_string();
1116 extra_messages.push(first_line);
1117 }
1118 }
1119
1120 if !extra_messages.is_empty() {
1121 entry.message = format!(
1122 "{}\n\nIncorporated commits:\n⢠{}",
1123 entry.message,
1124 extra_messages.join("\n⢠")
1125 );
1126 }
1127
1128 println!(
1129 " ā
Incorporated {} commit(s) into entry '{}'",
1130 extra_commits.len(),
1131 entry.short_hash()
1132 );
1133 println!(" Updated: {} -> {}", old_commit, &new_head[..8]);
1134 }
1135
1136 Ok(())
1137 }
1138
1139 fn split_extra_commits(&mut self, stack_id: &Uuid, entry_id: Uuid, branch: &str) -> Result<()> {
1141 let stack = self.stacks.get_mut(stack_id).unwrap();
1142 let new_head = self.repo.get_branch_head(branch)?;
1143
1144 let entry_position = stack
1146 .entries
1147 .iter()
1148 .position(|e| e.id == entry_id)
1149 .ok_or_else(|| CascadeError::config("Entry not found in stack"))?;
1150
1151 let base_name = branch.trim_end_matches(|c: char| c.is_ascii_digit() || c == '-');
1153 let new_branch = format!("{base_name}-continued");
1154
1155 self.repo.create_branch(&new_branch, Some(&new_head))?;
1157
1158 let original_entry = &stack.entries[entry_position];
1160 let original_commit_hash = original_entry.commit_hash.clone(); let extra_commits = self
1162 .repo
1163 .get_commits_between(&original_commit_hash, &new_head)?;
1164
1165 let mut extra_messages = Vec::new();
1167 for commit in &extra_commits {
1168 if let Some(message) = commit.message() {
1169 let first_line = message.lines().next().unwrap_or("").to_string();
1170 extra_messages.push(first_line);
1171 }
1172 }
1173
1174 let new_message = if extra_messages.len() == 1 {
1175 extra_messages[0].clone()
1176 } else {
1177 format!("Combined changes:\n⢠{}", extra_messages.join("\n⢠"))
1178 };
1179
1180 let now = chrono::Utc::now();
1182 let new_entry = crate::stack::StackEntry {
1183 id: uuid::Uuid::new_v4(),
1184 branch: new_branch.clone(),
1185 commit_hash: new_head,
1186 message: new_message,
1187 parent_id: Some(entry_id), children: Vec::new(),
1189 created_at: now,
1190 updated_at: now,
1191 is_submitted: false,
1192 pull_request_id: None,
1193 is_synced: false,
1194 };
1195
1196 stack.entries.insert(entry_position + 1, new_entry);
1198
1199 self.repo
1201 .reset_branch_to_commit(branch, &original_commit_hash)?;
1202
1203 println!(
1204 " ā
Split {} commit(s) into new entry '{}'",
1205 extra_commits.len(),
1206 new_branch
1207 );
1208 println!(" Original branch '{branch}' reset to expected commit");
1209
1210 Ok(())
1211 }
1212
1213 fn reset_branch_destructive(&self, branch: &str, expected_commit: &str) -> Result<()> {
1215 self.repo.reset_branch_to_commit(branch, expected_commit)?;
1216 println!(
1217 " ā ļø Reset branch '{}' to {} (extra commits lost)",
1218 branch,
1219 &expected_commit[..8]
1220 );
1221 Ok(())
1222 }
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227 use super::*;
1228 use std::process::Command;
1229 use tempfile::TempDir;
1230
1231 fn create_test_repo() -> (TempDir, PathBuf) {
1232 let temp_dir = TempDir::new().unwrap();
1233 let repo_path = temp_dir.path().to_path_buf();
1234
1235 Command::new("git")
1237 .args(["init"])
1238 .current_dir(&repo_path)
1239 .output()
1240 .unwrap();
1241
1242 Command::new("git")
1244 .args(["config", "user.name", "Test User"])
1245 .current_dir(&repo_path)
1246 .output()
1247 .unwrap();
1248
1249 Command::new("git")
1250 .args(["config", "user.email", "test@example.com"])
1251 .current_dir(&repo_path)
1252 .output()
1253 .unwrap();
1254
1255 std::fs::write(repo_path.join("README.md"), "# Test Repo").unwrap();
1257 Command::new("git")
1258 .args(["add", "."])
1259 .current_dir(&repo_path)
1260 .output()
1261 .unwrap();
1262
1263 Command::new("git")
1264 .args(["commit", "-m", "Initial commit"])
1265 .current_dir(&repo_path)
1266 .output()
1267 .unwrap();
1268
1269 crate::config::initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string()))
1271 .unwrap();
1272
1273 (temp_dir, repo_path)
1274 }
1275
1276 #[test]
1277 fn test_create_stack_manager() {
1278 let (_temp_dir, repo_path) = create_test_repo();
1279 let manager = StackManager::new(&repo_path).unwrap();
1280
1281 assert_eq!(manager.stacks.len(), 0);
1282 assert!(manager.get_active_stack().is_none());
1283 }
1284
1285 #[test]
1286 fn test_create_and_manage_stack() {
1287 let (_temp_dir, repo_path) = create_test_repo();
1288 let mut manager = StackManager::new(&repo_path).unwrap();
1289
1290 let stack_id = manager
1292 .create_stack(
1293 "test-stack".to_string(),
1294 None, Some("Test stack description".to_string()),
1296 )
1297 .unwrap();
1298
1299 assert_eq!(manager.stacks.len(), 1);
1301 let stack = manager.get_stack(&stack_id).unwrap();
1302 assert_eq!(stack.name, "test-stack");
1303 assert!(!stack.base_branch.is_empty());
1305 assert!(stack.is_active);
1306
1307 let active = manager.get_active_stack().unwrap();
1309 assert_eq!(active.id, stack_id);
1310
1311 let found = manager.get_stack_by_name("test-stack").unwrap();
1313 assert_eq!(found.id, stack_id);
1314 }
1315
1316 #[test]
1317 fn test_stack_persistence() {
1318 let (_temp_dir, repo_path) = create_test_repo();
1319
1320 let stack_id = {
1321 let mut manager = StackManager::new(&repo_path).unwrap();
1322 manager
1323 .create_stack("persistent-stack".to_string(), None, None)
1324 .unwrap()
1325 };
1326
1327 let manager = StackManager::new(&repo_path).unwrap();
1329 assert_eq!(manager.stacks.len(), 1);
1330 let stack = manager.get_stack(&stack_id).unwrap();
1331 assert_eq!(stack.name, "persistent-stack");
1332 }
1333
1334 #[test]
1335 fn test_multiple_stacks() {
1336 let (_temp_dir, repo_path) = create_test_repo();
1337 let mut manager = StackManager::new(&repo_path).unwrap();
1338
1339 let stack1_id = manager
1340 .create_stack("stack-1".to_string(), None, None)
1341 .unwrap();
1342 let stack2_id = manager
1343 .create_stack("stack-2".to_string(), None, None)
1344 .unwrap();
1345
1346 assert_eq!(manager.stacks.len(), 2);
1347
1348 assert!(manager.get_stack(&stack1_id).unwrap().is_active);
1350 assert!(!manager.get_stack(&stack2_id).unwrap().is_active);
1351
1352 manager.set_active_stack(Some(stack2_id)).unwrap();
1354 assert!(!manager.get_stack(&stack1_id).unwrap().is_active);
1355 assert!(manager.get_stack(&stack2_id).unwrap().is_active);
1356 }
1357
1358 #[test]
1359 fn test_delete_stack() {
1360 let (_temp_dir, repo_path) = create_test_repo();
1361 let mut manager = StackManager::new(&repo_path).unwrap();
1362
1363 let stack_id = manager
1364 .create_stack("to-delete".to_string(), None, None)
1365 .unwrap();
1366 assert_eq!(manager.stacks.len(), 1);
1367
1368 let deleted = manager.delete_stack(&stack_id).unwrap();
1369 assert_eq!(deleted.name, "to-delete");
1370 assert_eq!(manager.stacks.len(), 0);
1371 assert!(manager.get_active_stack().is_none());
1372 }
1373
1374 #[test]
1375 fn test_validation() {
1376 let (_temp_dir, repo_path) = create_test_repo();
1377 let mut manager = StackManager::new(&repo_path).unwrap();
1378
1379 manager
1380 .create_stack("valid-stack".to_string(), None, None)
1381 .unwrap();
1382
1383 assert!(manager.validate_all().is_ok());
1385 }
1386
1387 #[test]
1388 fn test_duplicate_commit_message_detection() {
1389 let (_temp_dir, repo_path) = create_test_repo();
1390 let mut manager = StackManager::new(&repo_path).unwrap();
1391
1392 manager
1394 .create_stack("test-stack".to_string(), None, None)
1395 .unwrap();
1396
1397 std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1399 Command::new("git")
1400 .args(["add", "file1.txt"])
1401 .current_dir(&repo_path)
1402 .output()
1403 .unwrap();
1404
1405 Command::new("git")
1406 .args(["commit", "-m", "Add authentication feature"])
1407 .current_dir(&repo_path)
1408 .output()
1409 .unwrap();
1410
1411 let commit1_hash = Command::new("git")
1412 .args(["rev-parse", "HEAD"])
1413 .current_dir(&repo_path)
1414 .output()
1415 .unwrap();
1416 let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1417 .trim()
1418 .to_string();
1419
1420 let entry1_id = manager
1422 .push_to_stack(
1423 "feature/auth".to_string(),
1424 commit1_hash,
1425 "Add authentication feature".to_string(),
1426 "main".to_string(),
1427 )
1428 .unwrap();
1429
1430 assert!(manager
1432 .get_active_stack()
1433 .unwrap()
1434 .get_entry(&entry1_id)
1435 .is_some());
1436
1437 std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1439 Command::new("git")
1440 .args(["add", "file2.txt"])
1441 .current_dir(&repo_path)
1442 .output()
1443 .unwrap();
1444
1445 Command::new("git")
1446 .args(["commit", "-m", "Different commit message"])
1447 .current_dir(&repo_path)
1448 .output()
1449 .unwrap();
1450
1451 let commit2_hash = Command::new("git")
1452 .args(["rev-parse", "HEAD"])
1453 .current_dir(&repo_path)
1454 .output()
1455 .unwrap();
1456 let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1457 .trim()
1458 .to_string();
1459
1460 let result = manager.push_to_stack(
1462 "feature/auth2".to_string(),
1463 commit2_hash.clone(),
1464 "Add authentication feature".to_string(), "main".to_string(),
1466 );
1467
1468 assert!(result.is_err());
1470 let error = result.unwrap_err();
1471 assert!(matches!(error, CascadeError::Validation(_)));
1472
1473 let error_msg = error.to_string();
1475 assert!(error_msg.contains("Duplicate commit message"));
1476 assert!(error_msg.contains("Add authentication feature"));
1477 assert!(error_msg.contains("š” Consider using a more specific message"));
1478
1479 let entry2_id = manager
1481 .push_to_stack(
1482 "feature/auth2".to_string(),
1483 commit2_hash,
1484 "Add authentication validation".to_string(), "main".to_string(),
1486 )
1487 .unwrap();
1488
1489 let stack = manager.get_active_stack().unwrap();
1491 assert_eq!(stack.entries.len(), 2);
1492 assert!(stack.get_entry(&entry1_id).is_some());
1493 assert!(stack.get_entry(&entry2_id).is_some());
1494 }
1495
1496 #[test]
1497 fn test_duplicate_message_with_different_case() {
1498 let (_temp_dir, repo_path) = create_test_repo();
1499 let mut manager = StackManager::new(&repo_path).unwrap();
1500
1501 manager
1502 .create_stack("test-stack".to_string(), None, None)
1503 .unwrap();
1504
1505 std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1507 Command::new("git")
1508 .args(["add", "file1.txt"])
1509 .current_dir(&repo_path)
1510 .output()
1511 .unwrap();
1512
1513 Command::new("git")
1514 .args(["commit", "-m", "fix bug"])
1515 .current_dir(&repo_path)
1516 .output()
1517 .unwrap();
1518
1519 let commit1_hash = Command::new("git")
1520 .args(["rev-parse", "HEAD"])
1521 .current_dir(&repo_path)
1522 .output()
1523 .unwrap();
1524 let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1525 .trim()
1526 .to_string();
1527
1528 manager
1529 .push_to_stack(
1530 "feature/fix1".to_string(),
1531 commit1_hash,
1532 "fix bug".to_string(),
1533 "main".to_string(),
1534 )
1535 .unwrap();
1536
1537 std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1539 Command::new("git")
1540 .args(["add", "file2.txt"])
1541 .current_dir(&repo_path)
1542 .output()
1543 .unwrap();
1544
1545 Command::new("git")
1546 .args(["commit", "-m", "Fix Bug"])
1547 .current_dir(&repo_path)
1548 .output()
1549 .unwrap();
1550
1551 let commit2_hash = Command::new("git")
1552 .args(["rev-parse", "HEAD"])
1553 .current_dir(&repo_path)
1554 .output()
1555 .unwrap();
1556 let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1557 .trim()
1558 .to_string();
1559
1560 let result = manager.push_to_stack(
1562 "feature/fix2".to_string(),
1563 commit2_hash,
1564 "Fix Bug".to_string(), "main".to_string(),
1566 );
1567
1568 assert!(result.is_ok());
1570 }
1571
1572 #[test]
1573 fn test_duplicate_message_across_different_stacks() {
1574 let (_temp_dir, repo_path) = create_test_repo();
1575 let mut manager = StackManager::new(&repo_path).unwrap();
1576
1577 let stack1_id = manager
1579 .create_stack("stack1".to_string(), None, None)
1580 .unwrap();
1581
1582 std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1583 Command::new("git")
1584 .args(["add", "file1.txt"])
1585 .current_dir(&repo_path)
1586 .output()
1587 .unwrap();
1588
1589 Command::new("git")
1590 .args(["commit", "-m", "shared message"])
1591 .current_dir(&repo_path)
1592 .output()
1593 .unwrap();
1594
1595 let commit1_hash = Command::new("git")
1596 .args(["rev-parse", "HEAD"])
1597 .current_dir(&repo_path)
1598 .output()
1599 .unwrap();
1600 let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1601 .trim()
1602 .to_string();
1603
1604 manager
1605 .push_to_stack(
1606 "feature/shared1".to_string(),
1607 commit1_hash,
1608 "shared message".to_string(),
1609 "main".to_string(),
1610 )
1611 .unwrap();
1612
1613 let stack2_id = manager
1615 .create_stack("stack2".to_string(), None, None)
1616 .unwrap();
1617
1618 manager.set_active_stack(Some(stack2_id)).unwrap();
1620
1621 std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1623 Command::new("git")
1624 .args(["add", "file2.txt"])
1625 .current_dir(&repo_path)
1626 .output()
1627 .unwrap();
1628
1629 Command::new("git")
1630 .args(["commit", "-m", "shared message"])
1631 .current_dir(&repo_path)
1632 .output()
1633 .unwrap();
1634
1635 let commit2_hash = Command::new("git")
1636 .args(["rev-parse", "HEAD"])
1637 .current_dir(&repo_path)
1638 .output()
1639 .unwrap();
1640 let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1641 .trim()
1642 .to_string();
1643
1644 let result = manager.push_to_stack(
1646 "feature/shared2".to_string(),
1647 commit2_hash,
1648 "shared message".to_string(), "main".to_string(),
1650 );
1651
1652 assert!(result.is_ok());
1654
1655 let stack1 = manager.get_stack(&stack1_id).unwrap();
1657 let stack2 = manager.get_stack(&stack2_id).unwrap();
1658
1659 assert_eq!(stack1.entries.len(), 1);
1660 assert_eq!(stack2.entries.len(), 1);
1661 assert_eq!(stack1.entries[0].message, "shared message");
1662 assert_eq!(stack2.entries[0].message, "shared message");
1663 }
1664
1665 #[test]
1666 fn test_duplicate_after_pop() {
1667 let (_temp_dir, repo_path) = create_test_repo();
1668 let mut manager = StackManager::new(&repo_path).unwrap();
1669
1670 manager
1671 .create_stack("test-stack".to_string(), None, None)
1672 .unwrap();
1673
1674 std::fs::write(repo_path.join("file1.txt"), "content1").unwrap();
1676 Command::new("git")
1677 .args(["add", "file1.txt"])
1678 .current_dir(&repo_path)
1679 .output()
1680 .unwrap();
1681
1682 Command::new("git")
1683 .args(["commit", "-m", "temporary message"])
1684 .current_dir(&repo_path)
1685 .output()
1686 .unwrap();
1687
1688 let commit1_hash = Command::new("git")
1689 .args(["rev-parse", "HEAD"])
1690 .current_dir(&repo_path)
1691 .output()
1692 .unwrap();
1693 let commit1_hash = String::from_utf8_lossy(&commit1_hash.stdout)
1694 .trim()
1695 .to_string();
1696
1697 manager
1698 .push_to_stack(
1699 "feature/temp".to_string(),
1700 commit1_hash,
1701 "temporary message".to_string(),
1702 "main".to_string(),
1703 )
1704 .unwrap();
1705
1706 let popped = manager.pop_from_stack().unwrap();
1708 assert_eq!(popped.message, "temporary message");
1709
1710 std::fs::write(repo_path.join("file2.txt"), "content2").unwrap();
1712 Command::new("git")
1713 .args(["add", "file2.txt"])
1714 .current_dir(&repo_path)
1715 .output()
1716 .unwrap();
1717
1718 Command::new("git")
1719 .args(["commit", "-m", "temporary message"])
1720 .current_dir(&repo_path)
1721 .output()
1722 .unwrap();
1723
1724 let commit2_hash = Command::new("git")
1725 .args(["rev-parse", "HEAD"])
1726 .current_dir(&repo_path)
1727 .output()
1728 .unwrap();
1729 let commit2_hash = String::from_utf8_lossy(&commit2_hash.stdout)
1730 .trim()
1731 .to_string();
1732
1733 let result = manager.push_to_stack(
1735 "feature/temp2".to_string(),
1736 commit2_hash,
1737 "temporary message".to_string(),
1738 "main".to_string(),
1739 );
1740
1741 assert!(result.is_ok());
1742 }
1743}