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