cascade_cli/git/
repository.rs

1use crate::errors::{CascadeError, Result};
2use chrono;
3use dialoguer::{theme::ColorfulTheme, Confirm};
4use git2::{Oid, Repository, Signature};
5use std::path::{Path, PathBuf};
6use tracing::{info, warn};
7
8/// Repository information
9#[derive(Debug, Clone)]
10pub struct RepositoryInfo {
11    pub path: PathBuf,
12    pub head_branch: Option<String>,
13    pub head_commit: Option<String>,
14    pub is_dirty: bool,
15    pub untracked_files: Vec<String>,
16}
17
18/// Backup information for force push operations
19#[derive(Debug, Clone)]
20struct ForceBackupInfo {
21    pub backup_branch_name: String,
22    pub remote_commit_id: String,
23    #[allow(dead_code)] // Used for logging/display purposes
24    pub commits_that_would_be_lost: usize,
25}
26
27/// Safety information for branch deletion operations
28#[derive(Debug, Clone)]
29struct BranchDeletionSafety {
30    pub unpushed_commits: Vec<String>,
31    pub remote_tracking_branch: Option<String>,
32    pub is_merged_to_main: bool,
33    pub main_branch_name: String,
34}
35
36/// Safety information for checkout operations
37#[derive(Debug, Clone)]
38struct CheckoutSafety {
39    #[allow(dead_code)] // Used in confirmation dialogs and future features
40    pub has_uncommitted_changes: bool,
41    pub modified_files: Vec<String>,
42    pub staged_files: Vec<String>,
43    pub untracked_files: Vec<String>,
44    #[allow(dead_code)] // Reserved for future automatic stashing implementation
45    pub stash_created: Option<String>,
46    #[allow(dead_code)] // Used for context in confirmation dialogs
47    pub current_branch: Option<String>,
48}
49
50/// Wrapper around git2::Repository with safe operations
51///
52/// For thread safety, use the async variants (e.g., fetch_async, pull_async)
53/// which automatically handle threading using tokio::spawn_blocking.
54/// The async methods create new repository instances in background threads.
55pub struct GitRepository {
56    repo: Repository,
57    path: PathBuf,
58}
59
60impl GitRepository {
61    /// Open a Git repository at the given path
62    pub fn open(path: &Path) -> Result<Self> {
63        let repo = Repository::discover(path)
64            .map_err(|e| CascadeError::config(format!("Not a git repository: {e}")))?;
65
66        let workdir = repo
67            .workdir()
68            .ok_or_else(|| CascadeError::config("Repository has no working directory"))?
69            .to_path_buf();
70
71        Ok(Self {
72            repo,
73            path: workdir,
74        })
75    }
76
77    /// Get repository information
78    pub fn get_info(&self) -> Result<RepositoryInfo> {
79        let head_branch = self.get_current_branch().ok();
80        let head_commit = self.get_head_commit_hash().ok();
81        let is_dirty = self.is_dirty()?;
82        let untracked_files = self.get_untracked_files()?;
83
84        Ok(RepositoryInfo {
85            path: self.path.clone(),
86            head_branch,
87            head_commit,
88            is_dirty,
89            untracked_files,
90        })
91    }
92
93    /// Get the current branch name
94    pub fn get_current_branch(&self) -> Result<String> {
95        let head = self
96            .repo
97            .head()
98            .map_err(|e| CascadeError::branch(format!("Could not get HEAD: {e}")))?;
99
100        if let Some(name) = head.shorthand() {
101            Ok(name.to_string())
102        } else {
103            // Detached HEAD - return commit hash
104            let commit = head
105                .peel_to_commit()
106                .map_err(|e| CascadeError::branch(format!("Could not get HEAD commit: {e}")))?;
107            Ok(format!("HEAD@{}", commit.id()))
108        }
109    }
110
111    /// Get the HEAD commit hash
112    pub fn get_head_commit_hash(&self) -> Result<String> {
113        let head = self
114            .repo
115            .head()
116            .map_err(|e| CascadeError::branch(format!("Could not get HEAD: {e}")))?;
117
118        let commit = head
119            .peel_to_commit()
120            .map_err(|e| CascadeError::branch(format!("Could not get HEAD commit: {e}")))?;
121
122        Ok(commit.id().to_string())
123    }
124
125    /// Check if the working directory is dirty (has uncommitted changes)
126    pub fn is_dirty(&self) -> Result<bool> {
127        let statuses = self.repo.statuses(None).map_err(CascadeError::Git)?;
128
129        for status in statuses.iter() {
130            let flags = status.status();
131
132            // Check for any modifications, additions, or deletions
133            if flags.intersects(
134                git2::Status::INDEX_MODIFIED
135                    | git2::Status::INDEX_NEW
136                    | git2::Status::INDEX_DELETED
137                    | git2::Status::WT_MODIFIED
138                    | git2::Status::WT_NEW
139                    | git2::Status::WT_DELETED,
140            ) {
141                return Ok(true);
142            }
143        }
144
145        Ok(false)
146    }
147
148    /// Get list of untracked files
149    pub fn get_untracked_files(&self) -> Result<Vec<String>> {
150        let statuses = self.repo.statuses(None).map_err(CascadeError::Git)?;
151
152        let mut untracked = Vec::new();
153        for status in statuses.iter() {
154            if status.status().contains(git2::Status::WT_NEW) {
155                if let Some(path) = status.path() {
156                    untracked.push(path.to_string());
157                }
158            }
159        }
160
161        Ok(untracked)
162    }
163
164    /// Create a new branch
165    pub fn create_branch(&self, name: &str, target: Option<&str>) -> Result<()> {
166        let target_commit = if let Some(target) = target {
167            // Find the specified target commit/branch
168            let target_obj = self.repo.revparse_single(target).map_err(|e| {
169                CascadeError::branch(format!("Could not find target '{target}': {e}"))
170            })?;
171            target_obj.peel_to_commit().map_err(|e| {
172                CascadeError::branch(format!("Target '{target}' is not a commit: {e}"))
173            })?
174        } else {
175            // Use current HEAD
176            let head = self
177                .repo
178                .head()
179                .map_err(|e| CascadeError::branch(format!("Could not get HEAD: {e}")))?;
180            head.peel_to_commit()
181                .map_err(|e| CascadeError::branch(format!("Could not get HEAD commit: {e}")))?
182        };
183
184        self.repo
185            .branch(name, &target_commit, false)
186            .map_err(|e| CascadeError::branch(format!("Could not create branch '{name}': {e}")))?;
187
188        tracing::info!("Created branch '{}'", name);
189        Ok(())
190    }
191
192    /// Switch to a branch with safety checks
193    pub fn checkout_branch(&self, name: &str) -> Result<()> {
194        self.checkout_branch_with_options(name, false)
195    }
196
197    /// Switch to a branch with force option to bypass safety checks
198    pub fn checkout_branch_unsafe(&self, name: &str) -> Result<()> {
199        self.checkout_branch_with_options(name, true)
200    }
201
202    /// Internal branch checkout implementation with safety options
203    fn checkout_branch_with_options(&self, name: &str, force_unsafe: bool) -> Result<()> {
204        info!("Attempting to checkout branch: {}", name);
205
206        // Enhanced safety check: Detect uncommitted work before checkout
207        if !force_unsafe {
208            let safety_result = self.check_checkout_safety(name)?;
209            if let Some(safety_info) = safety_result {
210                // Repository has uncommitted changes, get user confirmation
211                self.handle_checkout_confirmation(name, &safety_info)?;
212            }
213        }
214
215        // Find the branch
216        let branch = self
217            .repo
218            .find_branch(name, git2::BranchType::Local)
219            .map_err(|e| CascadeError::branch(format!("Could not find branch '{name}': {e}")))?;
220
221        let branch_ref = branch.get();
222        let tree = branch_ref.peel_to_tree().map_err(|e| {
223            CascadeError::branch(format!("Could not get tree for branch '{name}': {e}"))
224        })?;
225
226        // Checkout the tree
227        self.repo
228            .checkout_tree(tree.as_object(), None)
229            .map_err(|e| {
230                CascadeError::branch(format!("Could not checkout branch '{name}': {e}"))
231            })?;
232
233        // Update HEAD
234        self.repo
235            .set_head(&format!("refs/heads/{name}"))
236            .map_err(|e| CascadeError::branch(format!("Could not update HEAD to '{name}': {e}")))?;
237
238        tracing::info!("Switched to branch '{}'", name);
239        Ok(())
240    }
241
242    /// Checkout a specific commit (detached HEAD) with safety checks
243    pub fn checkout_commit(&self, commit_hash: &str) -> Result<()> {
244        self.checkout_commit_with_options(commit_hash, false)
245    }
246
247    /// Checkout a specific commit with force option to bypass safety checks
248    pub fn checkout_commit_unsafe(&self, commit_hash: &str) -> Result<()> {
249        self.checkout_commit_with_options(commit_hash, true)
250    }
251
252    /// Internal commit checkout implementation with safety options
253    fn checkout_commit_with_options(&self, commit_hash: &str, force_unsafe: bool) -> Result<()> {
254        info!("Attempting to checkout commit: {}", commit_hash);
255
256        // Enhanced safety check: Detect uncommitted work before checkout
257        if !force_unsafe {
258            let safety_result = self.check_checkout_safety(&format!("commit:{commit_hash}"))?;
259            if let Some(safety_info) = safety_result {
260                // Repository has uncommitted changes, get user confirmation
261                self.handle_checkout_confirmation(&format!("commit {commit_hash}"), &safety_info)?;
262            }
263        }
264
265        let oid = Oid::from_str(commit_hash).map_err(CascadeError::Git)?;
266
267        let commit = self.repo.find_commit(oid).map_err(|e| {
268            CascadeError::branch(format!("Could not find commit '{commit_hash}': {e}"))
269        })?;
270
271        let tree = commit.tree().map_err(|e| {
272            CascadeError::branch(format!(
273                "Could not get tree for commit '{commit_hash}': {e}"
274            ))
275        })?;
276
277        // Checkout the tree
278        self.repo
279            .checkout_tree(tree.as_object(), None)
280            .map_err(|e| {
281                CascadeError::branch(format!("Could not checkout commit '{commit_hash}': {e}"))
282            })?;
283
284        // Update HEAD to the commit (detached HEAD)
285        self.repo.set_head_detached(oid).map_err(|e| {
286            CascadeError::branch(format!(
287                "Could not update HEAD to commit '{commit_hash}': {e}"
288            ))
289        })?;
290
291        tracing::info!("Checked out commit '{}' (detached HEAD)", commit_hash);
292        Ok(())
293    }
294
295    /// Check if a branch exists
296    pub fn branch_exists(&self, name: &str) -> bool {
297        self.repo.find_branch(name, git2::BranchType::Local).is_ok()
298    }
299
300    /// Check if a branch exists locally, and if not, attempt to fetch it from remote
301    pub fn branch_exists_or_fetch(&self, name: &str) -> Result<bool> {
302        // 1. Check if branch exists locally first
303        if self.repo.find_branch(name, git2::BranchType::Local).is_ok() {
304            return Ok(true);
305        }
306
307        // 2. Try to fetch it from remote
308        println!("🔍 Branch '{name}' not found locally, trying to fetch from remote...");
309
310        use std::process::Command;
311
312        // Try: git fetch origin release/12.34:release/12.34
313        let fetch_result = Command::new("git")
314            .args(["fetch", "origin", &format!("{name}:{name}")])
315            .current_dir(&self.path)
316            .output();
317
318        match fetch_result {
319            Ok(output) => {
320                if output.status.success() {
321                    println!("✅ Successfully fetched '{name}' from origin");
322                    // 3. Check again locally after fetch
323                    return Ok(self.repo.find_branch(name, git2::BranchType::Local).is_ok());
324                } else {
325                    let stderr = String::from_utf8_lossy(&output.stderr);
326                    tracing::debug!("Failed to fetch branch '{name}': {stderr}");
327                }
328            }
329            Err(e) => {
330                tracing::debug!("Git fetch command failed: {e}");
331            }
332        }
333
334        // 4. Try alternative fetch patterns for common branch naming
335        if name.contains('/') {
336            println!("🔍 Trying alternative fetch patterns...");
337
338            // Try: git fetch origin (to get all refs, then checkout locally)
339            let fetch_all_result = Command::new("git")
340                .args(["fetch", "origin"])
341                .current_dir(&self.path)
342                .output();
343
344            if let Ok(output) = fetch_all_result {
345                if output.status.success() {
346                    // Try to create local branch from remote
347                    let checkout_result = Command::new("git")
348                        .args(["checkout", "-b", name, &format!("origin/{name}")])
349                        .current_dir(&self.path)
350                        .output();
351
352                    if let Ok(checkout_output) = checkout_result {
353                        if checkout_output.status.success() {
354                            println!(
355                                "✅ Successfully created local branch '{name}' from origin/{name}"
356                            );
357                            return Ok(true);
358                        }
359                    }
360                }
361            }
362        }
363
364        // 5. Only fail if it doesn't exist anywhere
365        Ok(false)
366    }
367
368    /// Get the commit hash for a specific branch without switching branches
369    pub fn get_branch_commit_hash(&self, branch_name: &str) -> Result<String> {
370        let branch = self
371            .repo
372            .find_branch(branch_name, git2::BranchType::Local)
373            .map_err(|e| {
374                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
375            })?;
376
377        let commit = branch.get().peel_to_commit().map_err(|e| {
378            CascadeError::branch(format!(
379                "Could not get commit for branch '{branch_name}': {e}"
380            ))
381        })?;
382
383        Ok(commit.id().to_string())
384    }
385
386    /// List all local branches
387    pub fn list_branches(&self) -> Result<Vec<String>> {
388        let branches = self
389            .repo
390            .branches(Some(git2::BranchType::Local))
391            .map_err(CascadeError::Git)?;
392
393        let mut branch_names = Vec::new();
394        for branch in branches {
395            let (branch, _) = branch.map_err(CascadeError::Git)?;
396            if let Some(name) = branch.name().map_err(CascadeError::Git)? {
397                branch_names.push(name.to_string());
398            }
399        }
400
401        Ok(branch_names)
402    }
403
404    /// Create a commit with all staged changes
405    pub fn commit(&self, message: &str) -> Result<String> {
406        let signature = self.get_signature()?;
407        let tree_id = self.get_index_tree()?;
408        let tree = self.repo.find_tree(tree_id).map_err(CascadeError::Git)?;
409
410        // Get parent commits
411        let head = self.repo.head().map_err(CascadeError::Git)?;
412        let parent_commit = head.peel_to_commit().map_err(CascadeError::Git)?;
413
414        let commit_id = self
415            .repo
416            .commit(
417                Some("HEAD"),
418                &signature,
419                &signature,
420                message,
421                &tree,
422                &[&parent_commit],
423            )
424            .map_err(CascadeError::Git)?;
425
426        tracing::info!("Created commit: {} - {}", commit_id, message);
427        Ok(commit_id.to_string())
428    }
429
430    /// Stage all changes
431    pub fn stage_all(&self) -> Result<()> {
432        let mut index = self.repo.index().map_err(CascadeError::Git)?;
433
434        index
435            .add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)
436            .map_err(CascadeError::Git)?;
437
438        index.write().map_err(CascadeError::Git)?;
439
440        tracing::debug!("Staged all changes");
441        Ok(())
442    }
443
444    /// Get repository path
445    pub fn path(&self) -> &Path {
446        &self.path
447    }
448
449    /// Check if a commit exists
450    pub fn commit_exists(&self, commit_hash: &str) -> Result<bool> {
451        match Oid::from_str(commit_hash) {
452            Ok(oid) => match self.repo.find_commit(oid) {
453                Ok(_) => Ok(true),
454                Err(_) => Ok(false),
455            },
456            Err(_) => Ok(false),
457        }
458    }
459
460    /// Get the HEAD commit object
461    pub fn get_head_commit(&self) -> Result<git2::Commit<'_>> {
462        let head = self
463            .repo
464            .head()
465            .map_err(|e| CascadeError::branch(format!("Could not get HEAD: {e}")))?;
466        head.peel_to_commit()
467            .map_err(|e| CascadeError::branch(format!("Could not get HEAD commit: {e}")))
468    }
469
470    /// Get a commit object by hash
471    pub fn get_commit(&self, commit_hash: &str) -> Result<git2::Commit<'_>> {
472        let oid = Oid::from_str(commit_hash).map_err(CascadeError::Git)?;
473
474        self.repo.find_commit(oid).map_err(CascadeError::Git)
475    }
476
477    /// Get the commit hash at the head of a branch
478    pub fn get_branch_head(&self, branch_name: &str) -> Result<String> {
479        let branch = self
480            .repo
481            .find_branch(branch_name, git2::BranchType::Local)
482            .map_err(|e| {
483                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
484            })?;
485
486        let commit = branch.get().peel_to_commit().map_err(|e| {
487            CascadeError::branch(format!(
488                "Could not get commit for branch '{branch_name}': {e}"
489            ))
490        })?;
491
492        Ok(commit.id().to_string())
493    }
494
495    /// Get a signature for commits
496    fn get_signature(&self) -> Result<Signature<'_>> {
497        // Try to get signature from Git config
498        if let Ok(config) = self.repo.config() {
499            if let (Ok(name), Ok(email)) = (
500                config.get_string("user.name"),
501                config.get_string("user.email"),
502            ) {
503                return Signature::now(&name, &email).map_err(CascadeError::Git);
504            }
505        }
506
507        // Fallback to default signature
508        Signature::now("Cascade CLI", "cascade@example.com").map_err(CascadeError::Git)
509    }
510
511    /// Get the tree ID from the current index
512    fn get_index_tree(&self) -> Result<Oid> {
513        let mut index = self.repo.index().map_err(CascadeError::Git)?;
514
515        index.write_tree().map_err(CascadeError::Git)
516    }
517
518    /// Get repository status
519    pub fn get_status(&self) -> Result<git2::Statuses<'_>> {
520        self.repo.statuses(None).map_err(CascadeError::Git)
521    }
522
523    /// Get remote URL for a given remote name
524    pub fn get_remote_url(&self, name: &str) -> Result<String> {
525        let remote = self.repo.find_remote(name).map_err(CascadeError::Git)?;
526
527        let url = remote.url().ok_or_else(|| {
528            CascadeError::Git(git2::Error::from_str("Remote URL is not valid UTF-8"))
529        })?;
530
531        Ok(url.to_string())
532    }
533
534    /// Cherry-pick a commit onto the current branch
535    pub fn cherry_pick(&self, commit_hash: &str) -> Result<String> {
536        tracing::debug!("Cherry-picking commit {}", commit_hash);
537
538        let oid = Oid::from_str(commit_hash).map_err(CascadeError::Git)?;
539        let commit = self.repo.find_commit(oid).map_err(CascadeError::Git)?;
540
541        // Get the commit's tree
542        let commit_tree = commit.tree().map_err(CascadeError::Git)?;
543
544        // Get parent tree for merge base
545        let parent_commit = if commit.parent_count() > 0 {
546            commit.parent(0).map_err(CascadeError::Git)?
547        } else {
548            // Root commit - use empty tree
549            let empty_tree_oid = self.repo.treebuilder(None)?.write()?;
550            let empty_tree = self.repo.find_tree(empty_tree_oid)?;
551            let sig = self.get_signature()?;
552            return self
553                .repo
554                .commit(
555                    Some("HEAD"),
556                    &sig,
557                    &sig,
558                    commit.message().unwrap_or("Cherry-picked commit"),
559                    &empty_tree,
560                    &[],
561                )
562                .map(|oid| oid.to_string())
563                .map_err(CascadeError::Git);
564        };
565
566        let parent_tree = parent_commit.tree().map_err(CascadeError::Git)?;
567
568        // Get current HEAD tree for 3-way merge
569        let head_commit = self.get_head_commit()?;
570        let head_tree = head_commit.tree().map_err(CascadeError::Git)?;
571
572        // Perform 3-way merge
573        let mut index = self
574            .repo
575            .merge_trees(&parent_tree, &head_tree, &commit_tree, None)
576            .map_err(CascadeError::Git)?;
577
578        // Check for conflicts
579        if index.has_conflicts() {
580            return Err(CascadeError::branch(format!(
581                "Cherry-pick of {commit_hash} has conflicts that need manual resolution"
582            )));
583        }
584
585        // Write merged tree
586        let merged_tree_oid = index.write_tree_to(&self.repo).map_err(CascadeError::Git)?;
587        let merged_tree = self
588            .repo
589            .find_tree(merged_tree_oid)
590            .map_err(CascadeError::Git)?;
591
592        // Create new commit
593        let signature = self.get_signature()?;
594        let message = format!("Cherry-pick: {}", commit.message().unwrap_or(""));
595
596        let new_commit_oid = self
597            .repo
598            .commit(
599                Some("HEAD"),
600                &signature,
601                &signature,
602                &message,
603                &merged_tree,
604                &[&head_commit],
605            )
606            .map_err(CascadeError::Git)?;
607
608        tracing::info!("Cherry-picked {} -> {}", commit_hash, new_commit_oid);
609        Ok(new_commit_oid.to_string())
610    }
611
612    /// Check for merge conflicts in the index
613    pub fn has_conflicts(&self) -> Result<bool> {
614        let index = self.repo.index().map_err(CascadeError::Git)?;
615        Ok(index.has_conflicts())
616    }
617
618    /// Get list of conflicted files
619    pub fn get_conflicted_files(&self) -> Result<Vec<String>> {
620        let index = self.repo.index().map_err(CascadeError::Git)?;
621
622        let mut conflicts = Vec::new();
623
624        // Iterate through index conflicts
625        let conflict_iter = index.conflicts().map_err(CascadeError::Git)?;
626
627        for conflict in conflict_iter {
628            let conflict = conflict.map_err(CascadeError::Git)?;
629            if let Some(our) = conflict.our {
630                if let Ok(path) = std::str::from_utf8(&our.path) {
631                    conflicts.push(path.to_string());
632                }
633            } else if let Some(their) = conflict.their {
634                if let Ok(path) = std::str::from_utf8(&their.path) {
635                    conflicts.push(path.to_string());
636                }
637            }
638        }
639
640        Ok(conflicts)
641    }
642
643    /// Fetch from remote origin
644    pub fn fetch(&self) -> Result<()> {
645        tracing::info!("Fetching from origin");
646
647        let mut remote = self
648            .repo
649            .find_remote("origin")
650            .map_err(|e| CascadeError::branch(format!("No remote 'origin' found: {e}")))?;
651
652        // Create callbacks for authentication
653        let mut callbacks = git2::RemoteCallbacks::new();
654
655        // Try to use existing authentication from git config/credential manager
656        callbacks.credentials(|_url, username_from_url, _allowed_types| {
657            if let Some(username) = username_from_url {
658                // Try SSH key first
659                git2::Cred::ssh_key_from_agent(username)
660            } else {
661                // Try default credential helper
662                git2::Cred::default()
663            }
664        });
665
666        // Fetch options with authentication
667        let mut fetch_options = git2::FetchOptions::new();
668        fetch_options.remote_callbacks(callbacks);
669
670        // Fetch with authentication
671        remote
672            .fetch::<&str>(&[], Some(&mut fetch_options), None)
673            .map_err(CascadeError::Git)?;
674
675        tracing::debug!("Fetch completed successfully");
676        Ok(())
677    }
678
679    /// Pull changes from remote (fetch + merge)
680    pub fn pull(&self, branch: &str) -> Result<()> {
681        tracing::info!("Pulling branch: {}", branch);
682
683        // First fetch
684        self.fetch()?;
685
686        // Get remote tracking branch
687        let remote_branch_name = format!("origin/{branch}");
688        let remote_oid = self
689            .repo
690            .refname_to_id(&format!("refs/remotes/{remote_branch_name}"))
691            .map_err(|e| {
692                CascadeError::branch(format!("Remote branch {remote_branch_name} not found: {e}"))
693            })?;
694
695        let remote_commit = self
696            .repo
697            .find_commit(remote_oid)
698            .map_err(CascadeError::Git)?;
699
700        // Get current HEAD
701        let head_commit = self.get_head_commit()?;
702
703        // Check if we need to merge
704        if head_commit.id() == remote_commit.id() {
705            tracing::debug!("Already up to date");
706            return Ok(());
707        }
708
709        // Perform merge
710        let head_tree = head_commit.tree().map_err(CascadeError::Git)?;
711        let remote_tree = remote_commit.tree().map_err(CascadeError::Git)?;
712
713        // Find merge base
714        let merge_base_oid = self
715            .repo
716            .merge_base(head_commit.id(), remote_commit.id())
717            .map_err(CascadeError::Git)?;
718        let merge_base_commit = self
719            .repo
720            .find_commit(merge_base_oid)
721            .map_err(CascadeError::Git)?;
722        let merge_base_tree = merge_base_commit.tree().map_err(CascadeError::Git)?;
723
724        // 3-way merge
725        let mut index = self
726            .repo
727            .merge_trees(&merge_base_tree, &head_tree, &remote_tree, None)
728            .map_err(CascadeError::Git)?;
729
730        if index.has_conflicts() {
731            return Err(CascadeError::branch(
732                "Pull has conflicts that need manual resolution".to_string(),
733            ));
734        }
735
736        // Write merged tree and create merge commit
737        let merged_tree_oid = index.write_tree_to(&self.repo).map_err(CascadeError::Git)?;
738        let merged_tree = self
739            .repo
740            .find_tree(merged_tree_oid)
741            .map_err(CascadeError::Git)?;
742
743        let signature = self.get_signature()?;
744        let message = format!("Merge branch '{branch}' from origin");
745
746        self.repo
747            .commit(
748                Some("HEAD"),
749                &signature,
750                &signature,
751                &message,
752                &merged_tree,
753                &[&head_commit, &remote_commit],
754            )
755            .map_err(CascadeError::Git)?;
756
757        tracing::info!("Pull completed successfully");
758        Ok(())
759    }
760
761    /// Push current branch to remote
762    pub fn push(&self, branch: &str) -> Result<()> {
763        tracing::info!("Pushing branch: {}", branch);
764
765        let mut remote = self
766            .repo
767            .find_remote("origin")
768            .map_err(|e| CascadeError::branch(format!("No remote 'origin' found: {e}")))?;
769
770        let refspec = format!("refs/heads/{branch}:refs/heads/{branch}");
771
772        // Create callbacks for authentication
773        let mut callbacks = git2::RemoteCallbacks::new();
774
775        // Try to use existing authentication from git config/credential manager
776        callbacks.credentials(|_url, username_from_url, _allowed_types| {
777            if let Some(username) = username_from_url {
778                // Try SSH key first
779                git2::Cred::ssh_key_from_agent(username)
780            } else {
781                // Try default credential helper
782                git2::Cred::default()
783            }
784        });
785
786        // Push options with authentication
787        let mut push_options = git2::PushOptions::new();
788        push_options.remote_callbacks(callbacks);
789
790        remote
791            .push(&[&refspec], Some(&mut push_options))
792            .map_err(CascadeError::Git)?;
793
794        tracing::info!("Push completed successfully");
795        Ok(())
796    }
797
798    /// Delete a local branch
799    pub fn delete_branch(&self, name: &str) -> Result<()> {
800        self.delete_branch_with_options(name, false)
801    }
802
803    /// Delete a local branch with force option to bypass safety checks
804    pub fn delete_branch_unsafe(&self, name: &str) -> Result<()> {
805        self.delete_branch_with_options(name, true)
806    }
807
808    /// Internal branch deletion implementation with safety options
809    fn delete_branch_with_options(&self, name: &str, force_unsafe: bool) -> Result<()> {
810        info!("Attempting to delete branch: {}", name);
811
812        // Enhanced safety check: Detect unpushed commits before deletion
813        if !force_unsafe {
814            let safety_result = self.check_branch_deletion_safety(name)?;
815            if let Some(safety_info) = safety_result {
816                // Branch has unpushed commits, get user confirmation
817                self.handle_branch_deletion_confirmation(name, &safety_info)?;
818            }
819        }
820
821        let mut branch = self
822            .repo
823            .find_branch(name, git2::BranchType::Local)
824            .map_err(|e| CascadeError::branch(format!("Could not find branch '{name}': {e}")))?;
825
826        branch
827            .delete()
828            .map_err(|e| CascadeError::branch(format!("Could not delete branch '{name}': {e}")))?;
829
830        info!("Successfully deleted branch '{}'", name);
831        Ok(())
832    }
833
834    /// Get commits between two references
835    pub fn get_commits_between(&self, from: &str, to: &str) -> Result<Vec<git2::Commit<'_>>> {
836        let from_oid = self
837            .repo
838            .refname_to_id(&format!("refs/heads/{from}"))
839            .or_else(|_| Oid::from_str(from))
840            .map_err(|e| CascadeError::branch(format!("Invalid from reference '{from}': {e}")))?;
841
842        let to_oid = self
843            .repo
844            .refname_to_id(&format!("refs/heads/{to}"))
845            .or_else(|_| Oid::from_str(to))
846            .map_err(|e| CascadeError::branch(format!("Invalid to reference '{to}': {e}")))?;
847
848        let mut revwalk = self.repo.revwalk().map_err(CascadeError::Git)?;
849
850        revwalk.push(to_oid).map_err(CascadeError::Git)?;
851        revwalk.hide(from_oid).map_err(CascadeError::Git)?;
852
853        let mut commits = Vec::new();
854        for oid in revwalk {
855            let oid = oid.map_err(CascadeError::Git)?;
856            let commit = self.repo.find_commit(oid).map_err(CascadeError::Git)?;
857            commits.push(commit);
858        }
859
860        Ok(commits)
861    }
862
863    /// Force push one branch's content to another branch name
864    /// This is used to preserve PR history while updating branch contents after rebase
865    pub fn force_push_branch(&self, target_branch: &str, source_branch: &str) -> Result<()> {
866        self.force_push_branch_with_options(target_branch, source_branch, false)
867    }
868
869    /// Force push with explicit force flag to bypass safety checks
870    pub fn force_push_branch_unsafe(&self, target_branch: &str, source_branch: &str) -> Result<()> {
871        self.force_push_branch_with_options(target_branch, source_branch, true)
872    }
873
874    /// Internal force push implementation with safety options
875    fn force_push_branch_with_options(
876        &self,
877        target_branch: &str,
878        source_branch: &str,
879        force_unsafe: bool,
880    ) -> Result<()> {
881        info!(
882            "Force pushing {} content to {} to preserve PR history",
883            source_branch, target_branch
884        );
885
886        // Enhanced safety check: Detect potential data loss and get user confirmation
887        if !force_unsafe {
888            let safety_result = self.check_force_push_safety_enhanced(target_branch)?;
889            if let Some(backup_info) = safety_result {
890                // Create backup branch before force push
891                self.create_backup_branch(target_branch, &backup_info.remote_commit_id)?;
892                info!(
893                    "✅ Created backup branch: {}",
894                    backup_info.backup_branch_name
895                );
896            }
897        }
898
899        // First, ensure we have the latest changes for the source branch
900        let source_ref = self
901            .repo
902            .find_reference(&format!("refs/heads/{source_branch}"))
903            .map_err(|e| {
904                CascadeError::config(format!("Failed to find source branch {source_branch}: {e}"))
905            })?;
906        let source_commit = source_ref.peel_to_commit().map_err(|e| {
907            CascadeError::config(format!(
908                "Failed to get commit for source branch {source_branch}: {e}"
909            ))
910        })?;
911
912        // Update the target branch to point to the source commit
913        let mut target_ref = self
914            .repo
915            .find_reference(&format!("refs/heads/{target_branch}"))
916            .map_err(|e| {
917                CascadeError::config(format!("Failed to find target branch {target_branch}: {e}"))
918            })?;
919
920        target_ref
921            .set_target(source_commit.id(), "Force push from rebase")
922            .map_err(|e| {
923                CascadeError::config(format!(
924                    "Failed to update target branch {target_branch}: {e}"
925                ))
926            })?;
927
928        // Force push to remote
929        let mut remote = self
930            .repo
931            .find_remote("origin")
932            .map_err(|e| CascadeError::config(format!("Failed to find origin remote: {e}")))?;
933
934        let refspec = format!("+refs/heads/{target_branch}:refs/heads/{target_branch}");
935
936        // Create callbacks for authentication
937        let mut callbacks = git2::RemoteCallbacks::new();
938
939        // Try to use existing authentication from git config/credential manager
940        callbacks.credentials(|_url, username_from_url, _allowed_types| {
941            if let Some(username) = username_from_url {
942                // Try SSH key first
943                git2::Cred::ssh_key_from_agent(username)
944            } else {
945                // Try default credential helper
946                git2::Cred::default()
947            }
948        });
949
950        // Push options for force push
951        let mut push_options = git2::PushOptions::new();
952        push_options.remote_callbacks(callbacks);
953
954        remote
955            .push(&[&refspec], Some(&mut push_options))
956            .map_err(|e| {
957                CascadeError::config(format!("Failed to force push {target_branch}: {e}"))
958            })?;
959
960        info!(
961            "✅ Successfully force pushed {} to preserve PR history",
962            target_branch
963        );
964        Ok(())
965    }
966
967    /// Enhanced safety check for force push operations with user confirmation
968    /// Returns backup info if data would be lost and user confirms
969    fn check_force_push_safety_enhanced(
970        &self,
971        target_branch: &str,
972    ) -> Result<Option<ForceBackupInfo>> {
973        // First fetch latest remote changes to ensure we have up-to-date information
974        match self.fetch() {
975            Ok(_) => {}
976            Err(e) => {
977                // If fetch fails, warn but don't block the operation
978                warn!("Could not fetch latest changes for safety check: {}", e);
979            }
980        }
981
982        // Check if there are commits on the remote that would be lost
983        let remote_ref = format!("refs/remotes/origin/{target_branch}");
984        let local_ref = format!("refs/heads/{target_branch}");
985
986        // Try to find both local and remote references
987        let local_commit = match self.repo.find_reference(&local_ref) {
988            Ok(reference) => reference.peel_to_commit().ok(),
989            Err(_) => None,
990        };
991
992        let remote_commit = match self.repo.find_reference(&remote_ref) {
993            Ok(reference) => reference.peel_to_commit().ok(),
994            Err(_) => None,
995        };
996
997        // If we have both commits, check for divergence
998        if let (Some(local), Some(remote)) = (local_commit, remote_commit) {
999            if local.id() != remote.id() {
1000                // Check if the remote has commits that the local doesn't have
1001                let merge_base_oid = self
1002                    .repo
1003                    .merge_base(local.id(), remote.id())
1004                    .map_err(|e| CascadeError::config(format!("Failed to find merge base: {e}")))?;
1005
1006                // If merge base != remote commit, remote has commits that would be lost
1007                if merge_base_oid != remote.id() {
1008                    let commits_to_lose = self.count_commits_between(
1009                        &merge_base_oid.to_string(),
1010                        &remote.id().to_string(),
1011                    )?;
1012
1013                    // Create backup branch name with timestamp
1014                    let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
1015                    let backup_branch_name = format!("{target_branch}_backup_{timestamp}");
1016
1017                    warn!(
1018                        "⚠️  Force push to '{}' would overwrite {} commits on remote",
1019                        target_branch, commits_to_lose
1020                    );
1021
1022                    // Check if we're in a non-interactive environment (CI/testing)
1023                    if std::env::var("CI").is_ok() || std::env::var("FORCE_PUSH_NO_CONFIRM").is_ok()
1024                    {
1025                        info!(
1026                            "Non-interactive environment detected, proceeding with backup creation"
1027                        );
1028                        return Ok(Some(ForceBackupInfo {
1029                            backup_branch_name,
1030                            remote_commit_id: remote.id().to_string(),
1031                            commits_that_would_be_lost: commits_to_lose,
1032                        }));
1033                    }
1034
1035                    // Interactive confirmation
1036                    println!("\n⚠️  FORCE PUSH WARNING ⚠️");
1037                    println!("Force push to '{target_branch}' would overwrite {commits_to_lose} commits on remote:");
1038
1039                    // Show the commits that would be lost
1040                    match self
1041                        .get_commits_between(&merge_base_oid.to_string(), &remote.id().to_string())
1042                    {
1043                        Ok(commits) => {
1044                            println!("\nCommits that would be lost:");
1045                            for (i, commit) in commits.iter().take(5).enumerate() {
1046                                let short_hash = &commit.id().to_string()[..8];
1047                                let summary = commit.summary().unwrap_or("<no message>");
1048                                println!("  {}. {} - {}", i + 1, short_hash, summary);
1049                            }
1050                            if commits.len() > 5 {
1051                                println!("  ... and {} more commits", commits.len() - 5);
1052                            }
1053                        }
1054                        Err(_) => {
1055                            println!("  (Unable to retrieve commit details)");
1056                        }
1057                    }
1058
1059                    println!("\nA backup branch '{backup_branch_name}' will be created before proceeding.");
1060
1061                    let confirmed = Confirm::with_theme(&ColorfulTheme::default())
1062                        .with_prompt("Do you want to proceed with the force push?")
1063                        .default(false)
1064                        .interact()
1065                        .map_err(|e| {
1066                            CascadeError::config(format!("Failed to get user confirmation: {e}"))
1067                        })?;
1068
1069                    if !confirmed {
1070                        return Err(CascadeError::config(
1071                            "Force push cancelled by user. Use --force to bypass this check."
1072                                .to_string(),
1073                        ));
1074                    }
1075
1076                    return Ok(Some(ForceBackupInfo {
1077                        backup_branch_name,
1078                        remote_commit_id: remote.id().to_string(),
1079                        commits_that_would_be_lost: commits_to_lose,
1080                    }));
1081                }
1082            }
1083        }
1084
1085        Ok(None)
1086    }
1087
1088    /// Create a backup branch pointing to the remote commit that would be lost
1089    fn create_backup_branch(&self, original_branch: &str, remote_commit_id: &str) -> Result<()> {
1090        let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
1091        let backup_branch_name = format!("{original_branch}_backup_{timestamp}");
1092
1093        // Parse the commit ID
1094        let commit_oid = Oid::from_str(remote_commit_id).map_err(|e| {
1095            CascadeError::config(format!("Invalid commit ID {remote_commit_id}: {e}"))
1096        })?;
1097
1098        // Find the commit
1099        let commit = self.repo.find_commit(commit_oid).map_err(|e| {
1100            CascadeError::config(format!("Failed to find commit {remote_commit_id}: {e}"))
1101        })?;
1102
1103        // Create the backup branch
1104        self.repo
1105            .branch(&backup_branch_name, &commit, false)
1106            .map_err(|e| {
1107                CascadeError::config(format!(
1108                    "Failed to create backup branch {backup_branch_name}: {e}"
1109                ))
1110            })?;
1111
1112        info!(
1113            "✅ Created backup branch '{}' pointing to {}",
1114            backup_branch_name,
1115            &remote_commit_id[..8]
1116        );
1117        Ok(())
1118    }
1119
1120    /// Check if branch deletion is safe by detecting unpushed commits
1121    /// Returns safety info if there are concerns that need user attention
1122    fn check_branch_deletion_safety(
1123        &self,
1124        branch_name: &str,
1125    ) -> Result<Option<BranchDeletionSafety>> {
1126        // First, try to fetch latest remote changes
1127        match self.fetch() {
1128            Ok(_) => {}
1129            Err(e) => {
1130                warn!(
1131                    "Could not fetch latest changes for branch deletion safety check: {}",
1132                    e
1133                );
1134            }
1135        }
1136
1137        // Find the branch
1138        let branch = self
1139            .repo
1140            .find_branch(branch_name, git2::BranchType::Local)
1141            .map_err(|e| {
1142                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
1143            })?;
1144
1145        let _branch_commit = branch.get().peel_to_commit().map_err(|e| {
1146            CascadeError::branch(format!(
1147                "Could not get commit for branch '{branch_name}': {e}"
1148            ))
1149        })?;
1150
1151        // Determine the main branch (try common names)
1152        let main_branch_name = self.detect_main_branch()?;
1153
1154        // Check if branch is merged to main
1155        let is_merged_to_main = self.is_branch_merged_to_main(branch_name, &main_branch_name)?;
1156
1157        // Find the upstream/remote tracking branch
1158        let remote_tracking_branch = self.get_remote_tracking_branch(branch_name);
1159
1160        let mut unpushed_commits = Vec::new();
1161
1162        // Check for unpushed commits compared to remote tracking branch
1163        if let Some(ref remote_branch) = remote_tracking_branch {
1164            match self.get_commits_between(remote_branch, branch_name) {
1165                Ok(commits) => {
1166                    unpushed_commits = commits.iter().map(|c| c.id().to_string()).collect();
1167                }
1168                Err(_) => {
1169                    // If we can't compare with remote, check against main branch
1170                    if !is_merged_to_main {
1171                        if let Ok(commits) =
1172                            self.get_commits_between(&main_branch_name, branch_name)
1173                        {
1174                            unpushed_commits = commits.iter().map(|c| c.id().to_string()).collect();
1175                        }
1176                    }
1177                }
1178            }
1179        } else if !is_merged_to_main {
1180            // No remote tracking branch, check against main
1181            if let Ok(commits) = self.get_commits_between(&main_branch_name, branch_name) {
1182                unpushed_commits = commits.iter().map(|c| c.id().to_string()).collect();
1183            }
1184        }
1185
1186        // If there are concerns, return safety info
1187        if !unpushed_commits.is_empty() || (!is_merged_to_main && remote_tracking_branch.is_none())
1188        {
1189            Ok(Some(BranchDeletionSafety {
1190                unpushed_commits,
1191                remote_tracking_branch,
1192                is_merged_to_main,
1193                main_branch_name,
1194            }))
1195        } else {
1196            Ok(None)
1197        }
1198    }
1199
1200    /// Handle user confirmation for branch deletion with safety concerns
1201    fn handle_branch_deletion_confirmation(
1202        &self,
1203        branch_name: &str,
1204        safety_info: &BranchDeletionSafety,
1205    ) -> Result<()> {
1206        // Check if we're in a non-interactive environment
1207        if std::env::var("CI").is_ok() || std::env::var("BRANCH_DELETE_NO_CONFIRM").is_ok() {
1208            return Err(CascadeError::branch(
1209                format!(
1210                    "Branch '{branch_name}' has {} unpushed commits and cannot be deleted in non-interactive mode. Use --force to override.",
1211                    safety_info.unpushed_commits.len()
1212                )
1213            ));
1214        }
1215
1216        // Interactive warning and confirmation
1217        println!("\n⚠️  BRANCH DELETION WARNING ⚠️");
1218        println!("Branch '{branch_name}' has potential issues:");
1219
1220        if !safety_info.unpushed_commits.is_empty() {
1221            println!(
1222                "\n🔍 Unpushed commits ({} total):",
1223                safety_info.unpushed_commits.len()
1224            );
1225
1226            // Show details of unpushed commits
1227            for (i, commit_id) in safety_info.unpushed_commits.iter().take(5).enumerate() {
1228                if let Ok(commit) = self.repo.find_commit(Oid::from_str(commit_id).unwrap()) {
1229                    let short_hash = &commit_id[..8];
1230                    let summary = commit.summary().unwrap_or("<no message>");
1231                    println!("  {}. {} - {}", i + 1, short_hash, summary);
1232                }
1233            }
1234
1235            if safety_info.unpushed_commits.len() > 5 {
1236                println!(
1237                    "  ... and {} more commits",
1238                    safety_info.unpushed_commits.len() - 5
1239                );
1240            }
1241        }
1242
1243        if !safety_info.is_merged_to_main {
1244            println!("\n📋 Branch status:");
1245            println!("  • Not merged to '{}'", safety_info.main_branch_name);
1246            if let Some(ref remote) = safety_info.remote_tracking_branch {
1247                println!("  • Remote tracking branch: {remote}");
1248            } else {
1249                println!("  • No remote tracking branch");
1250            }
1251        }
1252
1253        println!("\n💡 Safer alternatives:");
1254        if !safety_info.unpushed_commits.is_empty() {
1255            if let Some(ref _remote) = safety_info.remote_tracking_branch {
1256                println!("  • Push commits first: git push origin {branch_name}");
1257            } else {
1258                println!("  • Create and push to remote: git push -u origin {branch_name}");
1259            }
1260        }
1261        if !safety_info.is_merged_to_main {
1262            println!(
1263                "  • Merge to {} first: git checkout {} && git merge {branch_name}",
1264                safety_info.main_branch_name, safety_info.main_branch_name
1265            );
1266        }
1267
1268        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
1269            .with_prompt("Do you want to proceed with deleting this branch?")
1270            .default(false)
1271            .interact()
1272            .map_err(|e| CascadeError::branch(format!("Failed to get user confirmation: {e}")))?;
1273
1274        if !confirmed {
1275            return Err(CascadeError::branch(
1276                "Branch deletion cancelled by user. Use --force to bypass this check.".to_string(),
1277            ));
1278        }
1279
1280        Ok(())
1281    }
1282
1283    /// Detect the main branch name (main, master, develop)
1284    fn detect_main_branch(&self) -> Result<String> {
1285        let main_candidates = ["main", "master", "develop", "trunk"];
1286
1287        for candidate in &main_candidates {
1288            if self
1289                .repo
1290                .find_branch(candidate, git2::BranchType::Local)
1291                .is_ok()
1292            {
1293                return Ok(candidate.to_string());
1294            }
1295        }
1296
1297        // Fallback to HEAD's target if it's a symbolic reference
1298        if let Ok(head) = self.repo.head() {
1299            if let Some(name) = head.shorthand() {
1300                return Ok(name.to_string());
1301            }
1302        }
1303
1304        // Final fallback
1305        Ok("main".to_string())
1306    }
1307
1308    /// Check if a branch is merged to the main branch
1309    fn is_branch_merged_to_main(&self, branch_name: &str, main_branch: &str) -> Result<bool> {
1310        // Get the commits between main and the branch
1311        match self.get_commits_between(main_branch, branch_name) {
1312            Ok(commits) => Ok(commits.is_empty()),
1313            Err(_) => {
1314                // If we can't determine, assume not merged for safety
1315                Ok(false)
1316            }
1317        }
1318    }
1319
1320    /// Get the remote tracking branch for a local branch
1321    fn get_remote_tracking_branch(&self, branch_name: &str) -> Option<String> {
1322        // Try common remote tracking branch patterns
1323        let remote_candidates = [
1324            format!("origin/{branch_name}"),
1325            format!("remotes/origin/{branch_name}"),
1326        ];
1327
1328        for candidate in &remote_candidates {
1329            if self
1330                .repo
1331                .find_reference(&format!(
1332                    "refs/remotes/{}",
1333                    candidate.replace("remotes/", "")
1334                ))
1335                .is_ok()
1336            {
1337                return Some(candidate.clone());
1338            }
1339        }
1340
1341        None
1342    }
1343
1344    /// Check if checkout operation is safe
1345    fn check_checkout_safety(&self, _target: &str) -> Result<Option<CheckoutSafety>> {
1346        // Check if there are uncommitted changes
1347        let is_dirty = self.is_dirty()?;
1348        if !is_dirty {
1349            // No uncommitted changes, checkout is safe
1350            return Ok(None);
1351        }
1352
1353        // Get current branch for context
1354        let current_branch = self.get_current_branch().ok();
1355
1356        // Get detailed information about uncommitted changes
1357        let modified_files = self.get_modified_files()?;
1358        let staged_files = self.get_staged_files()?;
1359        let untracked_files = self.get_untracked_files()?;
1360
1361        let has_uncommitted_changes = !modified_files.is_empty() || !staged_files.is_empty();
1362
1363        if has_uncommitted_changes || !untracked_files.is_empty() {
1364            return Ok(Some(CheckoutSafety {
1365                has_uncommitted_changes,
1366                modified_files,
1367                staged_files,
1368                untracked_files,
1369                stash_created: None,
1370                current_branch,
1371            }));
1372        }
1373
1374        Ok(None)
1375    }
1376
1377    /// Handle user confirmation for checkout operations with uncommitted changes
1378    fn handle_checkout_confirmation(
1379        &self,
1380        target: &str,
1381        safety_info: &CheckoutSafety,
1382    ) -> Result<()> {
1383        // Check if we're in a non-interactive environment FIRST (before any output)
1384        let is_ci = std::env::var("CI").is_ok();
1385        let no_confirm = std::env::var("CHECKOUT_NO_CONFIRM").is_ok();
1386        let is_non_interactive = is_ci || no_confirm;
1387
1388        if is_non_interactive {
1389            return Err(CascadeError::branch(
1390                format!(
1391                    "Cannot checkout '{target}' with uncommitted changes in non-interactive mode. Commit your changes or use stash first."
1392                )
1393            ));
1394        }
1395
1396        // Interactive warning and confirmation
1397        println!("\n⚠️  CHECKOUT WARNING ⚠️");
1398        println!("You have uncommitted changes that could be lost:");
1399
1400        if !safety_info.modified_files.is_empty() {
1401            println!(
1402                "\n📝 Modified files ({}):",
1403                safety_info.modified_files.len()
1404            );
1405            for file in safety_info.modified_files.iter().take(10) {
1406                println!("   - {file}");
1407            }
1408            if safety_info.modified_files.len() > 10 {
1409                println!("   ... and {} more", safety_info.modified_files.len() - 10);
1410            }
1411        }
1412
1413        if !safety_info.staged_files.is_empty() {
1414            println!("\n📁 Staged files ({}):", safety_info.staged_files.len());
1415            for file in safety_info.staged_files.iter().take(10) {
1416                println!("   - {file}");
1417            }
1418            if safety_info.staged_files.len() > 10 {
1419                println!("   ... and {} more", safety_info.staged_files.len() - 10);
1420            }
1421        }
1422
1423        if !safety_info.untracked_files.is_empty() {
1424            println!(
1425                "\n❓ Untracked files ({}):",
1426                safety_info.untracked_files.len()
1427            );
1428            for file in safety_info.untracked_files.iter().take(5) {
1429                println!("   - {file}");
1430            }
1431            if safety_info.untracked_files.len() > 5 {
1432                println!("   ... and {} more", safety_info.untracked_files.len() - 5);
1433            }
1434        }
1435
1436        println!("\n🔄 Options:");
1437        println!("1. Stash changes and checkout (recommended)");
1438        println!("2. Force checkout (WILL LOSE UNCOMMITTED CHANGES)");
1439        println!("3. Cancel checkout");
1440
1441        let confirmation = Confirm::with_theme(&ColorfulTheme::default())
1442            .with_prompt("Would you like to stash your changes and proceed with checkout?")
1443            .interact()
1444            .map_err(|e| CascadeError::branch(format!("Could not get user confirmation: {e}")))?;
1445
1446        if confirmation {
1447            // Create stash before checkout
1448            let stash_message = format!(
1449                "Auto-stash before checkout to {} at {}",
1450                target,
1451                chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
1452            );
1453
1454            match self.create_stash(&stash_message) {
1455                Ok(stash_oid) => {
1456                    println!("✅ Created stash: {stash_message} ({stash_oid})");
1457                    println!("💡 You can restore with: git stash pop");
1458                }
1459                Err(e) => {
1460                    println!("❌ Failed to create stash: {e}");
1461
1462                    let force_confirm = Confirm::with_theme(&ColorfulTheme::default())
1463                        .with_prompt("Stash failed. Force checkout anyway? (WILL LOSE CHANGES)")
1464                        .interact()
1465                        .map_err(|e| {
1466                            CascadeError::branch(format!("Could not get confirmation: {e}"))
1467                        })?;
1468
1469                    if !force_confirm {
1470                        return Err(CascadeError::branch(
1471                            "Checkout cancelled by user".to_string(),
1472                        ));
1473                    }
1474                }
1475            }
1476        } else {
1477            return Err(CascadeError::branch(
1478                "Checkout cancelled by user".to_string(),
1479            ));
1480        }
1481
1482        Ok(())
1483    }
1484
1485    /// Create a stash with uncommitted changes
1486    fn create_stash(&self, message: &str) -> Result<String> {
1487        // For now, we'll use a different approach that doesn't require mutable access
1488        // This is a simplified version that recommends manual stashing
1489
1490        warn!("Automatic stashing not yet implemented - please stash manually");
1491        Err(CascadeError::branch(format!(
1492            "Please manually stash your changes first: git stash push -m \"{message}\""
1493        )))
1494    }
1495
1496    /// Get modified files in working directory
1497    fn get_modified_files(&self) -> Result<Vec<String>> {
1498        let mut opts = git2::StatusOptions::new();
1499        opts.include_untracked(false).include_ignored(false);
1500
1501        let statuses = self
1502            .repo
1503            .statuses(Some(&mut opts))
1504            .map_err(|e| CascadeError::branch(format!("Could not get repository status: {e}")))?;
1505
1506        let mut modified_files = Vec::new();
1507        for status in statuses.iter() {
1508            let flags = status.status();
1509            if flags.contains(git2::Status::WT_MODIFIED) || flags.contains(git2::Status::WT_DELETED)
1510            {
1511                if let Some(path) = status.path() {
1512                    modified_files.push(path.to_string());
1513                }
1514            }
1515        }
1516
1517        Ok(modified_files)
1518    }
1519
1520    /// Get staged files in index
1521    fn get_staged_files(&self) -> Result<Vec<String>> {
1522        let mut opts = git2::StatusOptions::new();
1523        opts.include_untracked(false).include_ignored(false);
1524
1525        let statuses = self
1526            .repo
1527            .statuses(Some(&mut opts))
1528            .map_err(|e| CascadeError::branch(format!("Could not get repository status: {e}")))?;
1529
1530        let mut staged_files = Vec::new();
1531        for status in statuses.iter() {
1532            let flags = status.status();
1533            if flags.contains(git2::Status::INDEX_MODIFIED)
1534                || flags.contains(git2::Status::INDEX_NEW)
1535                || flags.contains(git2::Status::INDEX_DELETED)
1536            {
1537                if let Some(path) = status.path() {
1538                    staged_files.push(path.to_string());
1539                }
1540            }
1541        }
1542
1543        Ok(staged_files)
1544    }
1545
1546    /// Count commits between two references
1547    fn count_commits_between(&self, from: &str, to: &str) -> Result<usize> {
1548        let commits = self.get_commits_between(from, to)?;
1549        Ok(commits.len())
1550    }
1551
1552    /// Resolve a reference (branch name, tag, or commit hash) to a commit
1553    pub fn resolve_reference(&self, reference: &str) -> Result<git2::Commit<'_>> {
1554        // Try to parse as commit hash first
1555        if let Ok(oid) = Oid::from_str(reference) {
1556            if let Ok(commit) = self.repo.find_commit(oid) {
1557                return Ok(commit);
1558            }
1559        }
1560
1561        // Try to resolve as a reference (branch, tag, etc.)
1562        let obj = self.repo.revparse_single(reference).map_err(|e| {
1563            CascadeError::branch(format!("Could not resolve reference '{reference}': {e}"))
1564        })?;
1565
1566        obj.peel_to_commit().map_err(|e| {
1567            CascadeError::branch(format!(
1568                "Reference '{reference}' does not point to a commit: {e}"
1569            ))
1570        })
1571    }
1572
1573    /// Reset HEAD to a specific reference (soft reset)
1574    pub fn reset_soft(&self, target_ref: &str) -> Result<()> {
1575        let target_commit = self.resolve_reference(target_ref)?;
1576
1577        self.repo
1578            .reset(target_commit.as_object(), git2::ResetType::Soft, None)
1579            .map_err(CascadeError::Git)?;
1580
1581        Ok(())
1582    }
1583
1584    /// Find which branch contains a specific commit
1585    pub fn find_branch_containing_commit(&self, commit_hash: &str) -> Result<String> {
1586        let oid = Oid::from_str(commit_hash).map_err(|e| {
1587            CascadeError::branch(format!("Invalid commit hash '{commit_hash}': {e}"))
1588        })?;
1589
1590        // Get all local branches
1591        let branches = self
1592            .repo
1593            .branches(Some(git2::BranchType::Local))
1594            .map_err(CascadeError::Git)?;
1595
1596        for branch_result in branches {
1597            let (branch, _) = branch_result.map_err(CascadeError::Git)?;
1598
1599            if let Some(branch_name) = branch.name().map_err(CascadeError::Git)? {
1600                // Check if this branch contains the commit
1601                if let Ok(branch_head) = branch.get().peel_to_commit() {
1602                    // Walk the commit history from this branch's HEAD
1603                    let mut revwalk = self.repo.revwalk().map_err(CascadeError::Git)?;
1604                    revwalk.push(branch_head.id()).map_err(CascadeError::Git)?;
1605
1606                    for commit_oid in revwalk {
1607                        let commit_oid = commit_oid.map_err(CascadeError::Git)?;
1608                        if commit_oid == oid {
1609                            return Ok(branch_name.to_string());
1610                        }
1611                    }
1612                }
1613            }
1614        }
1615
1616        // If not found in any branch, might be on current HEAD
1617        Err(CascadeError::branch(format!(
1618            "Commit {commit_hash} not found in any local branch"
1619        )))
1620    }
1621
1622    // Async wrappers for potentially blocking operations
1623
1624    /// Fetch from remote origin (async)
1625    pub async fn fetch_async(&self) -> Result<()> {
1626        let repo_path = self.path.clone();
1627        crate::utils::async_ops::run_git_operation(move || {
1628            let repo = GitRepository::open(&repo_path)?;
1629            repo.fetch()
1630        })
1631        .await
1632    }
1633
1634    /// Pull changes from remote (async)
1635    pub async fn pull_async(&self, branch: &str) -> Result<()> {
1636        let repo_path = self.path.clone();
1637        let branch_name = branch.to_string();
1638        crate::utils::async_ops::run_git_operation(move || {
1639            let repo = GitRepository::open(&repo_path)?;
1640            repo.pull(&branch_name)
1641        })
1642        .await
1643    }
1644
1645    /// Push branch to remote (async)
1646    pub async fn push_branch_async(&self, branch_name: &str) -> Result<()> {
1647        let repo_path = self.path.clone();
1648        let branch = branch_name.to_string();
1649        crate::utils::async_ops::run_git_operation(move || {
1650            let repo = GitRepository::open(&repo_path)?;
1651            repo.push(&branch)
1652        })
1653        .await
1654    }
1655
1656    /// Cherry-pick commit (async)
1657    pub async fn cherry_pick_commit_async(&self, commit_hash: &str) -> Result<String> {
1658        let repo_path = self.path.clone();
1659        let hash = commit_hash.to_string();
1660        crate::utils::async_ops::run_git_operation(move || {
1661            let repo = GitRepository::open(&repo_path)?;
1662            repo.cherry_pick(&hash)
1663        })
1664        .await
1665    }
1666
1667    /// Get commit hashes between two refs (async)
1668    pub async fn get_commit_hashes_between_async(
1669        &self,
1670        from: &str,
1671        to: &str,
1672    ) -> Result<Vec<String>> {
1673        let repo_path = self.path.clone();
1674        let from_str = from.to_string();
1675        let to_str = to.to_string();
1676        crate::utils::async_ops::run_git_operation(move || {
1677            let repo = GitRepository::open(&repo_path)?;
1678            let commits = repo.get_commits_between(&from_str, &to_str)?;
1679            Ok(commits.into_iter().map(|c| c.id().to_string()).collect())
1680        })
1681        .await
1682    }
1683
1684    /// Reset a branch to point to a specific commit
1685    pub fn reset_branch_to_commit(&self, branch_name: &str, commit_hash: &str) -> Result<()> {
1686        info!(
1687            "Resetting branch '{}' to commit {}",
1688            branch_name,
1689            &commit_hash[..8]
1690        );
1691
1692        // Find the target commit
1693        let target_oid = git2::Oid::from_str(commit_hash).map_err(|e| {
1694            CascadeError::branch(format!("Invalid commit hash '{commit_hash}': {e}"))
1695        })?;
1696
1697        let _target_commit = self.repo.find_commit(target_oid).map_err(|e| {
1698            CascadeError::branch(format!("Could not find commit '{commit_hash}': {e}"))
1699        })?;
1700
1701        // Find the branch
1702        let _branch = self
1703            .repo
1704            .find_branch(branch_name, git2::BranchType::Local)
1705            .map_err(|e| {
1706                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
1707            })?;
1708
1709        // Update the branch reference to point to the target commit
1710        let branch_ref_name = format!("refs/heads/{branch_name}");
1711        self.repo
1712            .reference(
1713                &branch_ref_name,
1714                target_oid,
1715                true,
1716                &format!("Reset {branch_name} to {commit_hash}"),
1717            )
1718            .map_err(|e| {
1719                CascadeError::branch(format!(
1720                    "Could not reset branch '{branch_name}' to commit '{commit_hash}': {e}"
1721                ))
1722            })?;
1723
1724        tracing::info!(
1725            "Successfully reset branch '{}' to commit {}",
1726            branch_name,
1727            &commit_hash[..8]
1728        );
1729        Ok(())
1730    }
1731}
1732
1733#[cfg(test)]
1734mod tests {
1735    use super::*;
1736    use std::process::Command;
1737    use tempfile::TempDir;
1738
1739    fn create_test_repo() -> (TempDir, PathBuf) {
1740        let temp_dir = TempDir::new().unwrap();
1741        let repo_path = temp_dir.path().to_path_buf();
1742
1743        // Initialize git repository
1744        Command::new("git")
1745            .args(["init"])
1746            .current_dir(&repo_path)
1747            .output()
1748            .unwrap();
1749        Command::new("git")
1750            .args(["config", "user.name", "Test"])
1751            .current_dir(&repo_path)
1752            .output()
1753            .unwrap();
1754        Command::new("git")
1755            .args(["config", "user.email", "test@test.com"])
1756            .current_dir(&repo_path)
1757            .output()
1758            .unwrap();
1759
1760        // Create initial commit
1761        std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
1762        Command::new("git")
1763            .args(["add", "."])
1764            .current_dir(&repo_path)
1765            .output()
1766            .unwrap();
1767        Command::new("git")
1768            .args(["commit", "-m", "Initial commit"])
1769            .current_dir(&repo_path)
1770            .output()
1771            .unwrap();
1772
1773        (temp_dir, repo_path)
1774    }
1775
1776    fn create_commit(repo_path: &PathBuf, message: &str, filename: &str) {
1777        let file_path = repo_path.join(filename);
1778        std::fs::write(&file_path, format!("Content for {filename}\n")).unwrap();
1779
1780        Command::new("git")
1781            .args(["add", filename])
1782            .current_dir(repo_path)
1783            .output()
1784            .unwrap();
1785        Command::new("git")
1786            .args(["commit", "-m", message])
1787            .current_dir(repo_path)
1788            .output()
1789            .unwrap();
1790    }
1791
1792    #[test]
1793    fn test_repository_info() {
1794        let (_temp_dir, repo_path) = create_test_repo();
1795        let repo = GitRepository::open(&repo_path).unwrap();
1796
1797        let info = repo.get_info().unwrap();
1798        assert!(!info.is_dirty); // Should be clean after commit
1799        assert!(
1800            info.head_branch == Some("master".to_string())
1801                || info.head_branch == Some("main".to_string()),
1802            "Expected default branch to be 'master' or 'main', got {:?}",
1803            info.head_branch
1804        );
1805        assert!(info.head_commit.is_some()); // Just check it exists
1806        assert!(info.untracked_files.is_empty()); // Should be empty after commit
1807    }
1808
1809    #[test]
1810    fn test_force_push_branch_basic() {
1811        let (_temp_dir, repo_path) = create_test_repo();
1812        let repo = GitRepository::open(&repo_path).unwrap();
1813
1814        // Get the actual default branch name
1815        let default_branch = repo.get_current_branch().unwrap();
1816
1817        // Create source branch with commits
1818        create_commit(&repo_path, "Feature commit 1", "feature1.rs");
1819        Command::new("git")
1820            .args(["checkout", "-b", "source-branch"])
1821            .current_dir(&repo_path)
1822            .output()
1823            .unwrap();
1824        create_commit(&repo_path, "Feature commit 2", "feature2.rs");
1825
1826        // Create target branch
1827        Command::new("git")
1828            .args(["checkout", &default_branch])
1829            .current_dir(&repo_path)
1830            .output()
1831            .unwrap();
1832        Command::new("git")
1833            .args(["checkout", "-b", "target-branch"])
1834            .current_dir(&repo_path)
1835            .output()
1836            .unwrap();
1837        create_commit(&repo_path, "Target commit", "target.rs");
1838
1839        // Test force push from source to target
1840        let result = repo.force_push_branch("target-branch", "source-branch");
1841
1842        // Should succeed in test environment (even though it doesn't actually push to remote)
1843        // The important thing is that the function doesn't panic and handles the git2 operations
1844        assert!(result.is_ok() || result.is_err()); // Either is acceptable for unit test
1845    }
1846
1847    #[test]
1848    fn test_force_push_branch_nonexistent_branches() {
1849        let (_temp_dir, repo_path) = create_test_repo();
1850        let repo = GitRepository::open(&repo_path).unwrap();
1851
1852        // Get the actual default branch name
1853        let default_branch = repo.get_current_branch().unwrap();
1854
1855        // Test force push with nonexistent source branch
1856        let result = repo.force_push_branch("target", "nonexistent-source");
1857        assert!(result.is_err());
1858
1859        // Test force push with nonexistent target branch
1860        let result = repo.force_push_branch("nonexistent-target", &default_branch);
1861        assert!(result.is_err());
1862    }
1863
1864    #[test]
1865    fn test_force_push_workflow_simulation() {
1866        let (_temp_dir, repo_path) = create_test_repo();
1867        let repo = GitRepository::open(&repo_path).unwrap();
1868
1869        // Simulate the smart force push workflow:
1870        // 1. Original branch exists with PR
1871        Command::new("git")
1872            .args(["checkout", "-b", "feature-auth"])
1873            .current_dir(&repo_path)
1874            .output()
1875            .unwrap();
1876        create_commit(&repo_path, "Add authentication", "auth.rs");
1877
1878        // 2. Rebase creates versioned branch
1879        Command::new("git")
1880            .args(["checkout", "-b", "feature-auth-v2"])
1881            .current_dir(&repo_path)
1882            .output()
1883            .unwrap();
1884        create_commit(&repo_path, "Fix auth validation", "auth.rs");
1885
1886        // 3. Smart force push: update original branch from versioned branch
1887        let result = repo.force_push_branch("feature-auth", "feature-auth-v2");
1888
1889        // Verify the operation is handled properly (success or expected error)
1890        match result {
1891            Ok(_) => {
1892                // Force push succeeded - verify branch state if possible
1893                Command::new("git")
1894                    .args(["checkout", "feature-auth"])
1895                    .current_dir(&repo_path)
1896                    .output()
1897                    .unwrap();
1898                let log_output = Command::new("git")
1899                    .args(["log", "--oneline", "-2"])
1900                    .current_dir(&repo_path)
1901                    .output()
1902                    .unwrap();
1903                let log_str = String::from_utf8_lossy(&log_output.stdout);
1904                assert!(
1905                    log_str.contains("Fix auth validation")
1906                        || log_str.contains("Add authentication")
1907                );
1908            }
1909            Err(_) => {
1910                // Expected in test environment without remote - that's fine
1911                // The important thing is we tested the code path without panicking
1912            }
1913        }
1914    }
1915
1916    #[test]
1917    fn test_branch_operations() {
1918        let (_temp_dir, repo_path) = create_test_repo();
1919        let repo = GitRepository::open(&repo_path).unwrap();
1920
1921        // Test get current branch - accept either main or master
1922        let current = repo.get_current_branch().unwrap();
1923        assert!(
1924            current == "master" || current == "main",
1925            "Expected default branch to be 'master' or 'main', got '{current}'"
1926        );
1927
1928        // Test create branch
1929        Command::new("git")
1930            .args(["checkout", "-b", "test-branch"])
1931            .current_dir(&repo_path)
1932            .output()
1933            .unwrap();
1934        let current = repo.get_current_branch().unwrap();
1935        assert_eq!(current, "test-branch");
1936    }
1937
1938    #[test]
1939    fn test_commit_operations() {
1940        let (_temp_dir, repo_path) = create_test_repo();
1941        let repo = GitRepository::open(&repo_path).unwrap();
1942
1943        // Test get head commit
1944        let head = repo.get_head_commit().unwrap();
1945        assert_eq!(head.message().unwrap().trim(), "Initial commit");
1946
1947        // Test get commit by hash
1948        let hash = head.id().to_string();
1949        let same_commit = repo.get_commit(&hash).unwrap();
1950        assert_eq!(head.id(), same_commit.id());
1951    }
1952
1953    #[test]
1954    fn test_checkout_safety_clean_repo() {
1955        let (_temp_dir, repo_path) = create_test_repo();
1956        let repo = GitRepository::open(&repo_path).unwrap();
1957
1958        // Create a test branch
1959        create_commit(&repo_path, "Second commit", "test.txt");
1960        Command::new("git")
1961            .args(["checkout", "-b", "test-branch"])
1962            .current_dir(&repo_path)
1963            .output()
1964            .unwrap();
1965
1966        // Test checkout safety with clean repo
1967        let safety_result = repo.check_checkout_safety("main");
1968        assert!(safety_result.is_ok());
1969        assert!(safety_result.unwrap().is_none()); // Clean repo should return None
1970    }
1971
1972    #[test]
1973    fn test_checkout_safety_with_modified_files() {
1974        let (_temp_dir, repo_path) = create_test_repo();
1975        let repo = GitRepository::open(&repo_path).unwrap();
1976
1977        // Create a test branch
1978        Command::new("git")
1979            .args(["checkout", "-b", "test-branch"])
1980            .current_dir(&repo_path)
1981            .output()
1982            .unwrap();
1983
1984        // Modify a file to create uncommitted changes
1985        std::fs::write(repo_path.join("README.md"), "Modified content").unwrap();
1986
1987        // Test checkout safety with modified files
1988        let safety_result = repo.check_checkout_safety("main");
1989        assert!(safety_result.is_ok());
1990        let safety_info = safety_result.unwrap();
1991        assert!(safety_info.is_some());
1992
1993        let info = safety_info.unwrap();
1994        assert!(!info.modified_files.is_empty());
1995        assert!(info.modified_files.contains(&"README.md".to_string()));
1996    }
1997
1998    #[test]
1999    fn test_unsafe_checkout_methods() {
2000        let (_temp_dir, repo_path) = create_test_repo();
2001        let repo = GitRepository::open(&repo_path).unwrap();
2002
2003        // Create a test branch
2004        create_commit(&repo_path, "Second commit", "test.txt");
2005        Command::new("git")
2006            .args(["checkout", "-b", "test-branch"])
2007            .current_dir(&repo_path)
2008            .output()
2009            .unwrap();
2010
2011        // Modify a file to create uncommitted changes
2012        std::fs::write(repo_path.join("README.md"), "Modified content").unwrap();
2013
2014        // Test unsafe checkout methods bypass safety checks
2015        let _result = repo.checkout_branch_unsafe("master");
2016        // Note: This might still fail due to git2 restrictions, but shouldn't hit our safety code
2017        // The important thing is that it doesn't trigger our safety confirmation
2018
2019        // Test unsafe commit checkout
2020        let head_commit = repo.get_head_commit().unwrap();
2021        let commit_hash = head_commit.id().to_string();
2022        let _result = repo.checkout_commit_unsafe(&commit_hash);
2023        // Similar to above - testing that safety is bypassed
2024    }
2025
2026    #[test]
2027    fn test_get_modified_files() {
2028        let (_temp_dir, repo_path) = create_test_repo();
2029        let repo = GitRepository::open(&repo_path).unwrap();
2030
2031        // Initially should have no modified files
2032        let modified = repo.get_modified_files().unwrap();
2033        assert!(modified.is_empty());
2034
2035        // Modify a file
2036        std::fs::write(repo_path.join("README.md"), "Modified content").unwrap();
2037
2038        // Should now detect the modified file
2039        let modified = repo.get_modified_files().unwrap();
2040        assert_eq!(modified.len(), 1);
2041        assert!(modified.contains(&"README.md".to_string()));
2042    }
2043
2044    #[test]
2045    fn test_get_staged_files() {
2046        let (_temp_dir, repo_path) = create_test_repo();
2047        let repo = GitRepository::open(&repo_path).unwrap();
2048
2049        // Initially should have no staged files
2050        let staged = repo.get_staged_files().unwrap();
2051        assert!(staged.is_empty());
2052
2053        // Create and stage a new file
2054        std::fs::write(repo_path.join("staged.txt"), "Staged content").unwrap();
2055        Command::new("git")
2056            .args(["add", "staged.txt"])
2057            .current_dir(&repo_path)
2058            .output()
2059            .unwrap();
2060
2061        // Should now detect the staged file
2062        let staged = repo.get_staged_files().unwrap();
2063        assert_eq!(staged.len(), 1);
2064        assert!(staged.contains(&"staged.txt".to_string()));
2065    }
2066
2067    #[test]
2068    fn test_create_stash_fallback() {
2069        let (_temp_dir, repo_path) = create_test_repo();
2070        let repo = GitRepository::open(&repo_path).unwrap();
2071
2072        // Test that stash creation returns helpful error message
2073        let result = repo.create_stash("test stash");
2074        assert!(result.is_err());
2075        let error_msg = result.unwrap_err().to_string();
2076        assert!(error_msg.contains("git stash push"));
2077    }
2078
2079    #[test]
2080    fn test_delete_branch_unsafe() {
2081        let (_temp_dir, repo_path) = create_test_repo();
2082        let repo = GitRepository::open(&repo_path).unwrap();
2083
2084        // Create a test branch
2085        create_commit(&repo_path, "Second commit", "test.txt");
2086        Command::new("git")
2087            .args(["checkout", "-b", "test-branch"])
2088            .current_dir(&repo_path)
2089            .output()
2090            .unwrap();
2091
2092        // Add another commit to the test branch to make it different from master
2093        create_commit(&repo_path, "Branch-specific commit", "branch.txt");
2094
2095        // Go back to master
2096        Command::new("git")
2097            .args(["checkout", "master"])
2098            .current_dir(&repo_path)
2099            .output()
2100            .unwrap();
2101
2102        // Test unsafe delete bypasses safety checks
2103        // Note: This may still fail if the branch has unpushed commits, but it should bypass our safety confirmation
2104        let result = repo.delete_branch_unsafe("test-branch");
2105        // Even if it fails, the key is that it didn't prompt for user confirmation
2106        // So we just check that it attempted the operation without interactive prompts
2107        let _ = result; // Don't assert success since delete may fail for git reasons
2108    }
2109
2110    #[test]
2111    fn test_force_push_unsafe() {
2112        let (_temp_dir, repo_path) = create_test_repo();
2113        let repo = GitRepository::open(&repo_path).unwrap();
2114
2115        // Create a test branch
2116        create_commit(&repo_path, "Second commit", "test.txt");
2117        Command::new("git")
2118            .args(["checkout", "-b", "test-branch"])
2119            .current_dir(&repo_path)
2120            .output()
2121            .unwrap();
2122
2123        // Test unsafe force push bypasses safety checks
2124        // Note: This will likely fail due to no remote, but it tests the safety bypass
2125        let _result = repo.force_push_branch_unsafe("test-branch", "test-branch");
2126        // The key is that it doesn't trigger safety confirmation dialogs
2127    }
2128}