cascade_cli/git/
repository.rs

1use crate::cli::output::Output;
2use crate::errors::{CascadeError, Result};
3use chrono;
4use dialoguer::{theme::ColorfulTheme, Confirm, Select};
5use git2::{Oid, Repository, Signature};
6use std::path::{Path, PathBuf};
7use tracing::{info, warn};
8
9/// Repository information
10#[derive(Debug, Clone)]
11pub struct RepositoryInfo {
12    pub path: PathBuf,
13    pub head_branch: Option<String>,
14    pub head_commit: Option<String>,
15    pub is_dirty: bool,
16    pub untracked_files: Vec<String>,
17}
18
19/// Backup information for force push operations
20#[derive(Debug, Clone)]
21struct ForceBackupInfo {
22    pub backup_branch_name: String,
23    pub remote_commit_id: String,
24    #[allow(dead_code)] // Used for logging/display purposes
25    pub commits_that_would_be_lost: usize,
26}
27
28/// Safety information for branch deletion operations
29#[derive(Debug, Clone)]
30struct BranchDeletionSafety {
31    pub unpushed_commits: Vec<String>,
32    pub remote_tracking_branch: Option<String>,
33    pub is_merged_to_main: bool,
34    pub main_branch_name: String,
35}
36
37/// Safety information for checkout operations
38#[derive(Debug, Clone)]
39struct CheckoutSafety {
40    #[allow(dead_code)] // Used in confirmation dialogs and future features
41    pub has_uncommitted_changes: bool,
42    pub modified_files: Vec<String>,
43    pub staged_files: Vec<String>,
44    pub untracked_files: Vec<String>,
45    #[allow(dead_code)] // Reserved for future automatic stashing implementation
46    pub stash_created: Option<String>,
47    #[allow(dead_code)] // Used for context in confirmation dialogs
48    pub current_branch: Option<String>,
49}
50
51/// SSL configuration for git operations
52#[derive(Debug, Clone)]
53pub struct GitSslConfig {
54    pub accept_invalid_certs: bool,
55    pub ca_bundle_path: Option<String>,
56}
57
58/// Wrapper around git2::Repository with safe operations
59///
60/// For thread safety, use the async variants (e.g., fetch_async, pull_async)
61/// which automatically handle threading using tokio::spawn_blocking.
62/// The async methods create new repository instances in background threads.
63pub struct GitRepository {
64    repo: Repository,
65    path: PathBuf,
66    ssl_config: Option<GitSslConfig>,
67    bitbucket_credentials: Option<BitbucketCredentials>,
68}
69
70#[derive(Debug, Clone)]
71struct BitbucketCredentials {
72    username: Option<String>,
73    token: Option<String>,
74}
75
76impl GitRepository {
77    /// Open a Git repository at the given path
78    /// Automatically loads SSL configuration from cascade config if available
79    pub fn open(path: &Path) -> Result<Self> {
80        let repo = Repository::discover(path)
81            .map_err(|e| CascadeError::config(format!("Not a git repository: {e}")))?;
82
83        let workdir = repo
84            .workdir()
85            .ok_or_else(|| CascadeError::config("Repository has no working directory"))?
86            .to_path_buf();
87
88        // Try to load SSL configuration from cascade config
89        let ssl_config = Self::load_ssl_config_from_cascade(&workdir);
90        let bitbucket_credentials = Self::load_bitbucket_credentials_from_cascade(&workdir);
91
92        Ok(Self {
93            repo,
94            path: workdir,
95            ssl_config,
96            bitbucket_credentials,
97        })
98    }
99
100    /// Load SSL configuration from cascade config file if it exists
101    fn load_ssl_config_from_cascade(repo_path: &Path) -> Option<GitSslConfig> {
102        // Try to load cascade configuration
103        let config_dir = crate::config::get_repo_config_dir(repo_path).ok()?;
104        let config_path = config_dir.join("config.json");
105        let settings = crate::config::Settings::load_from_file(&config_path).ok()?;
106
107        // Convert BitbucketConfig to GitSslConfig if SSL settings exist
108        if settings.bitbucket.accept_invalid_certs.is_some()
109            || settings.bitbucket.ca_bundle_path.is_some()
110        {
111            Some(GitSslConfig {
112                accept_invalid_certs: settings.bitbucket.accept_invalid_certs.unwrap_or(false),
113                ca_bundle_path: settings.bitbucket.ca_bundle_path,
114            })
115        } else {
116            None
117        }
118    }
119
120    /// Load Bitbucket credentials from cascade config file if it exists
121    fn load_bitbucket_credentials_from_cascade(repo_path: &Path) -> Option<BitbucketCredentials> {
122        // Try to load cascade configuration
123        let config_dir = crate::config::get_repo_config_dir(repo_path).ok()?;
124        let config_path = config_dir.join("config.json");
125        let settings = crate::config::Settings::load_from_file(&config_path).ok()?;
126
127        // Return credentials if any are configured
128        if settings.bitbucket.username.is_some() || settings.bitbucket.token.is_some() {
129            Some(BitbucketCredentials {
130                username: settings.bitbucket.username.clone(),
131                token: settings.bitbucket.token.clone(),
132            })
133        } else {
134            None
135        }
136    }
137
138    /// Get repository information
139    pub fn get_info(&self) -> Result<RepositoryInfo> {
140        let head_branch = self.get_current_branch().ok();
141        let head_commit = self.get_head_commit_hash().ok();
142        let is_dirty = self.is_dirty()?;
143        let untracked_files = self.get_untracked_files()?;
144
145        Ok(RepositoryInfo {
146            path: self.path.clone(),
147            head_branch,
148            head_commit,
149            is_dirty,
150            untracked_files,
151        })
152    }
153
154    /// Get the current branch name
155    pub fn get_current_branch(&self) -> Result<String> {
156        let head = self
157            .repo
158            .head()
159            .map_err(|e| CascadeError::branch(format!("Could not get HEAD: {e}")))?;
160
161        if let Some(name) = head.shorthand() {
162            Ok(name.to_string())
163        } else {
164            // Detached HEAD - return commit hash
165            let commit = head
166                .peel_to_commit()
167                .map_err(|e| CascadeError::branch(format!("Could not get HEAD commit: {e}")))?;
168            Ok(format!("HEAD@{}", commit.id()))
169        }
170    }
171
172    /// Get the HEAD commit hash
173    pub fn get_head_commit_hash(&self) -> Result<String> {
174        let head = self
175            .repo
176            .head()
177            .map_err(|e| CascadeError::branch(format!("Could not get HEAD: {e}")))?;
178
179        let commit = head
180            .peel_to_commit()
181            .map_err(|e| CascadeError::branch(format!("Could not get HEAD commit: {e}")))?;
182
183        Ok(commit.id().to_string())
184    }
185
186    /// Check if the working directory is dirty (has uncommitted changes)
187    pub fn is_dirty(&self) -> Result<bool> {
188        let statuses = self.repo.statuses(None).map_err(CascadeError::Git)?;
189
190        for status in statuses.iter() {
191            let flags = status.status();
192
193            // Check for any modifications, additions, or deletions
194            if flags.intersects(
195                git2::Status::INDEX_MODIFIED
196                    | git2::Status::INDEX_NEW
197                    | git2::Status::INDEX_DELETED
198                    | git2::Status::WT_MODIFIED
199                    | git2::Status::WT_NEW
200                    | git2::Status::WT_DELETED,
201            ) {
202                return Ok(true);
203            }
204        }
205
206        Ok(false)
207    }
208
209    /// Get list of untracked files
210    pub fn get_untracked_files(&self) -> Result<Vec<String>> {
211        let statuses = self.repo.statuses(None).map_err(CascadeError::Git)?;
212
213        let mut untracked = Vec::new();
214        for status in statuses.iter() {
215            if status.status().contains(git2::Status::WT_NEW) {
216                if let Some(path) = status.path() {
217                    untracked.push(path.to_string());
218                }
219            }
220        }
221
222        Ok(untracked)
223    }
224
225    /// Create a new branch
226    pub fn create_branch(&self, name: &str, target: Option<&str>) -> Result<()> {
227        let target_commit = if let Some(target) = target {
228            // Find the specified target commit/branch
229            let target_obj = self.repo.revparse_single(target).map_err(|e| {
230                CascadeError::branch(format!("Could not find target '{target}': {e}"))
231            })?;
232            target_obj.peel_to_commit().map_err(|e| {
233                CascadeError::branch(format!("Target '{target}' is not a commit: {e}"))
234            })?
235        } else {
236            // Use current HEAD
237            let head = self
238                .repo
239                .head()
240                .map_err(|e| CascadeError::branch(format!("Could not get HEAD: {e}")))?;
241            head.peel_to_commit()
242                .map_err(|e| CascadeError::branch(format!("Could not get HEAD commit: {e}")))?
243        };
244
245        self.repo
246            .branch(name, &target_commit, false)
247            .map_err(|e| CascadeError::branch(format!("Could not create branch '{name}': {e}")))?;
248
249        // Branch creation logging is handled by the caller for clean output
250        Ok(())
251    }
252
253    /// Switch to a branch with safety checks
254    pub fn checkout_branch(&self, name: &str) -> Result<()> {
255        self.checkout_branch_with_options(name, false)
256    }
257
258    /// Switch to a branch with force option to bypass safety checks
259    pub fn checkout_branch_unsafe(&self, name: &str) -> Result<()> {
260        self.checkout_branch_with_options(name, true)
261    }
262
263    /// Internal branch checkout implementation with safety options
264    fn checkout_branch_with_options(&self, name: &str, force_unsafe: bool) -> Result<()> {
265        info!("Attempting to checkout branch: {}", name);
266
267        // Enhanced safety check: Detect uncommitted work before checkout
268        if !force_unsafe {
269            let safety_result = self.check_checkout_safety(name)?;
270            if let Some(safety_info) = safety_result {
271                // Repository has uncommitted changes, get user confirmation
272                self.handle_checkout_confirmation(name, &safety_info)?;
273            }
274        }
275
276        // Find the branch
277        let branch = self
278            .repo
279            .find_branch(name, git2::BranchType::Local)
280            .map_err(|e| CascadeError::branch(format!("Could not find branch '{name}': {e}")))?;
281
282        let branch_ref = branch.get();
283        let tree = branch_ref.peel_to_tree().map_err(|e| {
284            CascadeError::branch(format!("Could not get tree for branch '{name}': {e}"))
285        })?;
286
287        // Checkout the tree
288        self.repo
289            .checkout_tree(tree.as_object(), None)
290            .map_err(|e| {
291                CascadeError::branch(format!("Could not checkout branch '{name}': {e}"))
292            })?;
293
294        // Update HEAD
295        self.repo
296            .set_head(&format!("refs/heads/{name}"))
297            .map_err(|e| CascadeError::branch(format!("Could not update HEAD to '{name}': {e}")))?;
298
299        Output::success(format!("Switched to branch '{name}'"));
300        Ok(())
301    }
302
303    /// Checkout a specific commit (detached HEAD) with safety checks
304    pub fn checkout_commit(&self, commit_hash: &str) -> Result<()> {
305        self.checkout_commit_with_options(commit_hash, false)
306    }
307
308    /// Checkout a specific commit with force option to bypass safety checks
309    pub fn checkout_commit_unsafe(&self, commit_hash: &str) -> Result<()> {
310        self.checkout_commit_with_options(commit_hash, true)
311    }
312
313    /// Internal commit checkout implementation with safety options
314    fn checkout_commit_with_options(&self, commit_hash: &str, force_unsafe: bool) -> Result<()> {
315        info!("Attempting to checkout commit: {}", commit_hash);
316
317        // Enhanced safety check: Detect uncommitted work before checkout
318        if !force_unsafe {
319            let safety_result = self.check_checkout_safety(&format!("commit:{commit_hash}"))?;
320            if let Some(safety_info) = safety_result {
321                // Repository has uncommitted changes, get user confirmation
322                self.handle_checkout_confirmation(&format!("commit {commit_hash}"), &safety_info)?;
323            }
324        }
325
326        let oid = Oid::from_str(commit_hash).map_err(CascadeError::Git)?;
327
328        let commit = self.repo.find_commit(oid).map_err(|e| {
329            CascadeError::branch(format!("Could not find commit '{commit_hash}': {e}"))
330        })?;
331
332        let tree = commit.tree().map_err(|e| {
333            CascadeError::branch(format!(
334                "Could not get tree for commit '{commit_hash}': {e}"
335            ))
336        })?;
337
338        // Checkout the tree
339        self.repo
340            .checkout_tree(tree.as_object(), None)
341            .map_err(|e| {
342                CascadeError::branch(format!("Could not checkout commit '{commit_hash}': {e}"))
343            })?;
344
345        // Update HEAD to the commit (detached HEAD)
346        self.repo.set_head_detached(oid).map_err(|e| {
347            CascadeError::branch(format!(
348                "Could not update HEAD to commit '{commit_hash}': {e}"
349            ))
350        })?;
351
352        Output::success(format!(
353            "Checked out commit '{commit_hash}' (detached HEAD)"
354        ));
355        Ok(())
356    }
357
358    /// Check if a branch exists
359    pub fn branch_exists(&self, name: &str) -> bool {
360        self.repo.find_branch(name, git2::BranchType::Local).is_ok()
361    }
362
363    /// Check if a branch exists locally, and if not, attempt to fetch it from remote
364    pub fn branch_exists_or_fetch(&self, name: &str) -> Result<bool> {
365        // 1. Check if branch exists locally first
366        if self.repo.find_branch(name, git2::BranchType::Local).is_ok() {
367            return Ok(true);
368        }
369
370        // 2. Try to fetch it from remote
371        println!("🔍 Branch '{name}' not found locally, trying to fetch from remote...");
372
373        use std::process::Command;
374
375        // Try: git fetch origin release/12.34:release/12.34
376        let fetch_result = Command::new("git")
377            .args(["fetch", "origin", &format!("{name}:{name}")])
378            .current_dir(&self.path)
379            .output();
380
381        match fetch_result {
382            Ok(output) => {
383                if output.status.success() {
384                    println!("✅ Successfully fetched '{name}' from origin");
385                    // 3. Check again locally after fetch
386                    return Ok(self.repo.find_branch(name, git2::BranchType::Local).is_ok());
387                } else {
388                    let stderr = String::from_utf8_lossy(&output.stderr);
389                    tracing::debug!("Failed to fetch branch '{name}': {stderr}");
390                }
391            }
392            Err(e) => {
393                tracing::debug!("Git fetch command failed: {e}");
394            }
395        }
396
397        // 4. Try alternative fetch patterns for common branch naming
398        if name.contains('/') {
399            println!("🔍 Trying alternative fetch patterns...");
400
401            // Try: git fetch origin (to get all refs, then checkout locally)
402            let fetch_all_result = Command::new("git")
403                .args(["fetch", "origin"])
404                .current_dir(&self.path)
405                .output();
406
407            if let Ok(output) = fetch_all_result {
408                if output.status.success() {
409                    // Try to create local branch from remote
410                    let checkout_result = Command::new("git")
411                        .args(["checkout", "-b", name, &format!("origin/{name}")])
412                        .current_dir(&self.path)
413                        .output();
414
415                    if let Ok(checkout_output) = checkout_result {
416                        if checkout_output.status.success() {
417                            println!(
418                                "✅ Successfully created local branch '{name}' from origin/{name}"
419                            );
420                            return Ok(true);
421                        }
422                    }
423                }
424            }
425        }
426
427        // 5. Only fail if it doesn't exist anywhere
428        Ok(false)
429    }
430
431    /// Get the commit hash for a specific branch without switching branches
432    pub fn get_branch_commit_hash(&self, branch_name: &str) -> Result<String> {
433        let branch = self
434            .repo
435            .find_branch(branch_name, git2::BranchType::Local)
436            .map_err(|e| {
437                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
438            })?;
439
440        let commit = branch.get().peel_to_commit().map_err(|e| {
441            CascadeError::branch(format!(
442                "Could not get commit for branch '{branch_name}': {e}"
443            ))
444        })?;
445
446        Ok(commit.id().to_string())
447    }
448
449    /// List all local branches
450    pub fn list_branches(&self) -> Result<Vec<String>> {
451        let branches = self
452            .repo
453            .branches(Some(git2::BranchType::Local))
454            .map_err(CascadeError::Git)?;
455
456        let mut branch_names = Vec::new();
457        for branch in branches {
458            let (branch, _) = branch.map_err(CascadeError::Git)?;
459            if let Some(name) = branch.name().map_err(CascadeError::Git)? {
460                branch_names.push(name.to_string());
461            }
462        }
463
464        Ok(branch_names)
465    }
466
467    /// Create a commit with all staged changes
468    pub fn commit(&self, message: &str) -> Result<String> {
469        // Validate git user configuration before attempting commit operations
470        self.validate_git_user_config()?;
471
472        let signature = self.get_signature()?;
473        let tree_id = self.get_index_tree()?;
474        let tree = self.repo.find_tree(tree_id).map_err(CascadeError::Git)?;
475
476        // Get parent commits
477        let head = self.repo.head().map_err(CascadeError::Git)?;
478        let parent_commit = head.peel_to_commit().map_err(CascadeError::Git)?;
479
480        let commit_id = self
481            .repo
482            .commit(
483                Some("HEAD"),
484                &signature,
485                &signature,
486                message,
487                &tree,
488                &[&parent_commit],
489            )
490            .map_err(CascadeError::Git)?;
491
492        Output::success(format!("Created commit: {commit_id} - {message}"));
493        Ok(commit_id.to_string())
494    }
495
496    /// Commit any staged changes with a default message
497    pub fn commit_staged_changes(&self, default_message: &str) -> Result<Option<String>> {
498        // Check if there are staged changes
499        let staged_files = self.get_staged_files()?;
500        if staged_files.is_empty() {
501            tracing::debug!("No staged changes to commit");
502            return Ok(None);
503        }
504
505        tracing::info!("Committing {} staged files", staged_files.len());
506        let commit_hash = self.commit(default_message)?;
507        Ok(Some(commit_hash))
508    }
509
510    /// Stage all changes
511    pub fn stage_all(&self) -> Result<()> {
512        let mut index = self.repo.index().map_err(CascadeError::Git)?;
513
514        index
515            .add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)
516            .map_err(CascadeError::Git)?;
517
518        index.write().map_err(CascadeError::Git)?;
519
520        tracing::debug!("Staged all changes");
521        Ok(())
522    }
523
524    /// Stage only specific files (safer than stage_all during rebase)
525    pub fn stage_files(&self, file_paths: &[&str]) -> Result<()> {
526        if file_paths.is_empty() {
527            tracing::debug!("No files to stage");
528            return Ok(());
529        }
530
531        let mut index = self.repo.index().map_err(CascadeError::Git)?;
532
533        for file_path in file_paths {
534            index
535                .add_path(std::path::Path::new(file_path))
536                .map_err(CascadeError::Git)?;
537        }
538
539        index.write().map_err(CascadeError::Git)?;
540
541        tracing::debug!(
542            "Staged {} specific files: {:?}",
543            file_paths.len(),
544            file_paths
545        );
546        Ok(())
547    }
548
549    /// Stage only files that had conflicts (safer for rebase operations)
550    pub fn stage_conflict_resolved_files(&self) -> Result<()> {
551        let conflicted_files = self.get_conflicted_files()?;
552        if conflicted_files.is_empty() {
553            tracing::debug!("No conflicted files to stage");
554            return Ok(());
555        }
556
557        let file_paths: Vec<&str> = conflicted_files.iter().map(|s| s.as_str()).collect();
558        self.stage_files(&file_paths)?;
559
560        tracing::debug!("Staged {} conflict-resolved files", conflicted_files.len());
561        Ok(())
562    }
563
564    /// Get repository path
565    pub fn path(&self) -> &Path {
566        &self.path
567    }
568
569    /// Check if a commit exists
570    pub fn commit_exists(&self, commit_hash: &str) -> Result<bool> {
571        match Oid::from_str(commit_hash) {
572            Ok(oid) => match self.repo.find_commit(oid) {
573                Ok(_) => Ok(true),
574                Err(_) => Ok(false),
575            },
576            Err(_) => Ok(false),
577        }
578    }
579
580    /// Get the HEAD commit object
581    pub fn get_head_commit(&self) -> Result<git2::Commit<'_>> {
582        let head = self
583            .repo
584            .head()
585            .map_err(|e| CascadeError::branch(format!("Could not get HEAD: {e}")))?;
586        head.peel_to_commit()
587            .map_err(|e| CascadeError::branch(format!("Could not get HEAD commit: {e}")))
588    }
589
590    /// Get a commit object by hash
591    pub fn get_commit(&self, commit_hash: &str) -> Result<git2::Commit<'_>> {
592        let oid = Oid::from_str(commit_hash).map_err(CascadeError::Git)?;
593
594        self.repo.find_commit(oid).map_err(CascadeError::Git)
595    }
596
597    /// Get the commit hash at the head of a branch
598    pub fn get_branch_head(&self, branch_name: &str) -> Result<String> {
599        let branch = self
600            .repo
601            .find_branch(branch_name, git2::BranchType::Local)
602            .map_err(|e| {
603                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
604            })?;
605
606        let commit = branch.get().peel_to_commit().map_err(|e| {
607            CascadeError::branch(format!(
608                "Could not get commit for branch '{branch_name}': {e}"
609            ))
610        })?;
611
612        Ok(commit.id().to_string())
613    }
614
615    /// Validate git user configuration is properly set
616    pub fn validate_git_user_config(&self) -> Result<()> {
617        if let Ok(config) = self.repo.config() {
618            let name_result = config.get_string("user.name");
619            let email_result = config.get_string("user.email");
620
621            if let (Ok(name), Ok(email)) = (name_result, email_result) {
622                if !name.trim().is_empty() && !email.trim().is_empty() {
623                    tracing::debug!("Git user config validated: {} <{}>", name, email);
624                    return Ok(());
625                }
626            }
627        }
628
629        // Check if this is a CI environment where validation can be skipped
630        let is_ci = std::env::var("CI").is_ok();
631
632        if is_ci {
633            tracing::debug!("CI environment - skipping git user config validation");
634            return Ok(());
635        }
636
637        Output::warning("Git user configuration missing or incomplete");
638        Output::info("This can cause cherry-pick and commit operations to fail");
639        Output::info("Please configure git user information:");
640        Output::bullet("git config user.name \"Your Name\"".to_string());
641        Output::bullet("git config user.email \"your.email@example.com\"".to_string());
642        Output::info("Or set globally with the --global flag");
643
644        // Don't fail - let operations continue with fallback signature
645        // This preserves backward compatibility while providing guidance
646        Ok(())
647    }
648
649    /// Get a signature for commits with comprehensive fallback and validation
650    fn get_signature(&self) -> Result<Signature<'_>> {
651        // Try to get signature from Git config first
652        if let Ok(config) = self.repo.config() {
653            // Try global/system config first
654            let name_result = config.get_string("user.name");
655            let email_result = config.get_string("user.email");
656
657            if let (Ok(name), Ok(email)) = (name_result, email_result) {
658                if !name.trim().is_empty() && !email.trim().is_empty() {
659                    tracing::debug!("Using git config: {} <{}>", name, email);
660                    return Signature::now(&name, &email).map_err(CascadeError::Git);
661                }
662            } else {
663                tracing::debug!("Git user config incomplete or missing");
664            }
665        }
666
667        // Check if this is a CI environment where fallback is acceptable
668        let is_ci = std::env::var("CI").is_ok();
669
670        if is_ci {
671            tracing::debug!("CI environment detected, using fallback signature");
672            return Signature::now("Cascade CLI", "cascade@example.com").map_err(CascadeError::Git);
673        }
674
675        // Interactive environment - provide helpful guidance
676        tracing::warn!("Git user configuration missing - this can cause commit operations to fail");
677
678        // Try fallback signature, but warn about the issue
679        match Signature::now("Cascade CLI", "cascade@example.com") {
680            Ok(sig) => {
681                Output::warning("Git user not configured - using fallback signature");
682                Output::info("For better git history, run:");
683                Output::bullet("git config user.name \"Your Name\"".to_string());
684                Output::bullet("git config user.email \"your.email@example.com\"".to_string());
685                Output::info("Or set it globally with --global flag");
686                Ok(sig)
687            }
688            Err(e) => {
689                Err(CascadeError::branch(format!(
690                    "Cannot create git signature: {e}. Please configure git user with:\n  git config user.name \"Your Name\"\n  git config user.email \"your.email@example.com\""
691                )))
692            }
693        }
694    }
695
696    /// Configure remote callbacks with SSL settings
697    /// Priority: Cascade SSL config > Git config > Default
698    fn configure_remote_callbacks(&self) -> Result<git2::RemoteCallbacks<'_>> {
699        let mut callbacks = git2::RemoteCallbacks::new();
700
701        // Configure authentication with comprehensive credential support
702        let bitbucket_credentials = self.bitbucket_credentials.clone();
703        callbacks.credentials(move |url, username_from_url, allowed_types| {
704            tracing::debug!(
705                "Authentication requested for URL: {}, username: {:?}, allowed_types: {:?}",
706                url,
707                username_from_url,
708                allowed_types
709            );
710
711            // For SSH URLs with username
712            if allowed_types.contains(git2::CredentialType::SSH_KEY) {
713                if let Some(username) = username_from_url {
714                    tracing::debug!("Trying SSH key authentication for user: {}", username);
715                    return git2::Cred::ssh_key_from_agent(username);
716                }
717            }
718
719            // For HTTPS URLs, try multiple authentication methods in sequence
720            if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
721                if url.contains("bitbucket") {
722                    if let Some(creds) = &bitbucket_credentials {
723                        // Method 1: Username + Token (common for Bitbucket)
724                        if let (Some(username), Some(token)) = (&creds.username, &creds.token) {
725                            tracing::debug!("Trying Bitbucket username + token authentication");
726                            return git2::Cred::userpass_plaintext(username, token);
727                        }
728
729                        // Method 2: Token as username, empty password (alternate Bitbucket format)
730                        if let Some(token) = &creds.token {
731                            tracing::debug!("Trying Bitbucket token-as-username authentication");
732                            return git2::Cred::userpass_plaintext(token, "");
733                        }
734
735                        // Method 3: Just username (will prompt for password or use credential helper)
736                        if let Some(username) = &creds.username {
737                            tracing::debug!("Trying Bitbucket username authentication (will use credential helper)");
738                            return git2::Cred::username(username);
739                        }
740                    }
741                }
742
743                // Method 4: Default credential helper for all HTTPS URLs
744                tracing::debug!("Trying default credential helper for HTTPS authentication");
745                return git2::Cred::default();
746            }
747
748            // Fallback to default for any other cases
749            tracing::debug!("Using default credential fallback");
750            git2::Cred::default()
751        });
752
753        // Configure SSL certificate checking with system certificates by default
754        // This matches what tools like Graphite, Sapling, and Phabricator do
755        // Priority: 1. Use system certificates (default), 2. Manual overrides only if needed
756
757        let mut ssl_configured = false;
758
759        // Check for manual SSL overrides first (only when user explicitly needs them)
760        if let Some(ssl_config) = &self.ssl_config {
761            if ssl_config.accept_invalid_certs {
762                Output::warning(
763                    "SSL certificate verification DISABLED via Cascade config - this is insecure!",
764                );
765                callbacks.certificate_check(|_cert, _host| {
766                    tracing::debug!("⚠️  Accepting invalid certificate for host: {}", _host);
767                    Ok(git2::CertificateCheckStatus::CertificateOk)
768                });
769                ssl_configured = true;
770            } else if let Some(ca_path) = &ssl_config.ca_bundle_path {
771                Output::info(format!(
772                    "Using custom CA bundle from Cascade config: {ca_path}"
773                ));
774                callbacks.certificate_check(|_cert, host| {
775                    tracing::debug!("Using custom CA bundle for host: {}", host);
776                    Ok(git2::CertificateCheckStatus::CertificateOk)
777                });
778                ssl_configured = true;
779            }
780        }
781
782        // Check git config for manual overrides
783        if !ssl_configured {
784            if let Ok(config) = self.repo.config() {
785                let ssl_verify = config.get_bool("http.sslVerify").unwrap_or(true);
786
787                if !ssl_verify {
788                    Output::warning(
789                        "SSL certificate verification DISABLED via git config - this is insecure!",
790                    );
791                    callbacks.certificate_check(|_cert, host| {
792                        tracing::debug!("⚠️  Bypassing SSL verification for host: {}", host);
793                        Ok(git2::CertificateCheckStatus::CertificateOk)
794                    });
795                    ssl_configured = true;
796                } else if let Ok(ca_path) = config.get_string("http.sslCAInfo") {
797                    Output::info(format!("Using custom CA bundle from git config: {ca_path}"));
798                    callbacks.certificate_check(|_cert, host| {
799                        tracing::debug!("Using git config CA bundle for host: {}", host);
800                        Ok(git2::CertificateCheckStatus::CertificateOk)
801                    });
802                    ssl_configured = true;
803                }
804            }
805        }
806
807        // DEFAULT BEHAVIOR: Use system certificates (like git CLI and other modern tools)
808        // This should work out-of-the-box in corporate environments
809        if !ssl_configured {
810            tracing::debug!(
811                "Using system certificate store for SSL verification (default behavior)"
812            );
813
814            // For macOS with SecureTransport backend, try default certificate validation first
815            if cfg!(target_os = "macos") {
816                tracing::debug!("macOS detected - using default certificate validation");
817                // Don't set any certificate callback - let git2 use its default behavior
818                // This often works better with SecureTransport backend on macOS
819            } else {
820                // Use CertificatePassthrough for other platforms
821                callbacks.certificate_check(|_cert, host| {
822                    tracing::debug!("System certificate validation for host: {}", host);
823                    Ok(git2::CertificateCheckStatus::CertificatePassthrough)
824                });
825            }
826        }
827
828        Ok(callbacks)
829    }
830
831    /// Get the tree ID from the current index
832    fn get_index_tree(&self) -> Result<Oid> {
833        let mut index = self.repo.index().map_err(CascadeError::Git)?;
834
835        index.write_tree().map_err(CascadeError::Git)
836    }
837
838    /// Get repository status
839    pub fn get_status(&self) -> Result<git2::Statuses<'_>> {
840        self.repo.statuses(None).map_err(CascadeError::Git)
841    }
842
843    /// Get remote URL for a given remote name
844    pub fn get_remote_url(&self, name: &str) -> Result<String> {
845        let remote = self.repo.find_remote(name).map_err(CascadeError::Git)?;
846        Ok(remote.url().unwrap_or("unknown").to_string())
847    }
848
849    /// Cherry-pick a specific commit to the current branch
850    pub fn cherry_pick(&self, commit_hash: &str) -> Result<String> {
851        tracing::debug!("Cherry-picking commit {}", commit_hash);
852
853        // Validate git user configuration before attempting commit operations
854        self.validate_git_user_config()?;
855
856        let oid = Oid::from_str(commit_hash).map_err(CascadeError::Git)?;
857        let commit = self.repo.find_commit(oid).map_err(CascadeError::Git)?;
858
859        // Get the commit's tree
860        let commit_tree = commit.tree().map_err(CascadeError::Git)?;
861
862        // Get parent tree for merge base
863        let parent_commit = if commit.parent_count() > 0 {
864            commit.parent(0).map_err(CascadeError::Git)?
865        } else {
866            // Root commit - use empty tree
867            let empty_tree_oid = self.repo.treebuilder(None)?.write()?;
868            let empty_tree = self.repo.find_tree(empty_tree_oid)?;
869            let sig = self.get_signature()?;
870            return self
871                .repo
872                .commit(
873                    Some("HEAD"),
874                    &sig,
875                    &sig,
876                    commit.message().unwrap_or("Cherry-picked commit"),
877                    &empty_tree,
878                    &[],
879                )
880                .map(|oid| oid.to_string())
881                .map_err(CascadeError::Git);
882        };
883
884        let parent_tree = parent_commit.tree().map_err(CascadeError::Git)?;
885
886        // Get current HEAD tree for 3-way merge
887        let head_commit = self.get_head_commit()?;
888        let head_tree = head_commit.tree().map_err(CascadeError::Git)?;
889
890        // Perform 3-way merge
891        let mut index = self
892            .repo
893            .merge_trees(&parent_tree, &head_tree, &commit_tree, None)
894            .map_err(CascadeError::Git)?;
895
896        // Check for conflicts
897        if index.has_conflicts() {
898            return Err(CascadeError::branch(format!(
899                "Cherry-pick of {commit_hash} has conflicts that need manual resolution"
900            )));
901        }
902
903        // Write merged tree
904        let merged_tree_oid = index.write_tree_to(&self.repo).map_err(CascadeError::Git)?;
905        let merged_tree = self
906            .repo
907            .find_tree(merged_tree_oid)
908            .map_err(CascadeError::Git)?;
909
910        // Create new commit
911        let signature = self.get_signature()?;
912        let message = format!("Cherry-pick: {}", commit.message().unwrap_or(""));
913
914        let new_commit_oid = self
915            .repo
916            .commit(
917                Some("HEAD"),
918                &signature,
919                &signature,
920                &message,
921                &merged_tree,
922                &[&head_commit],
923            )
924            .map_err(CascadeError::Git)?;
925
926        tracing::info!("Cherry-picked {} -> {}", commit_hash, new_commit_oid);
927        Ok(new_commit_oid.to_string())
928    }
929
930    /// Check for merge conflicts in the index
931    pub fn has_conflicts(&self) -> Result<bool> {
932        let index = self.repo.index().map_err(CascadeError::Git)?;
933        Ok(index.has_conflicts())
934    }
935
936    /// Get list of conflicted files
937    pub fn get_conflicted_files(&self) -> Result<Vec<String>> {
938        let index = self.repo.index().map_err(CascadeError::Git)?;
939
940        let mut conflicts = Vec::new();
941
942        // Iterate through index conflicts
943        let conflict_iter = index.conflicts().map_err(CascadeError::Git)?;
944
945        for conflict in conflict_iter {
946            let conflict = conflict.map_err(CascadeError::Git)?;
947            if let Some(our) = conflict.our {
948                if let Ok(path) = std::str::from_utf8(&our.path) {
949                    conflicts.push(path.to_string());
950                }
951            } else if let Some(their) = conflict.their {
952                if let Ok(path) = std::str::from_utf8(&their.path) {
953                    conflicts.push(path.to_string());
954                }
955            }
956        }
957
958        Ok(conflicts)
959    }
960
961    /// Fetch from remote origin
962    pub fn fetch(&self) -> Result<()> {
963        tracing::info!("Fetching from origin");
964
965        let mut remote = self
966            .repo
967            .find_remote("origin")
968            .map_err(|e| CascadeError::branch(format!("No remote 'origin' found: {e}")))?;
969
970        // Configure callbacks with SSL settings from git config
971        let callbacks = self.configure_remote_callbacks()?;
972
973        // Fetch options with authentication and SSL config
974        let mut fetch_options = git2::FetchOptions::new();
975        fetch_options.remote_callbacks(callbacks);
976
977        // Fetch with authentication
978        match remote.fetch::<&str>(&[], Some(&mut fetch_options), None) {
979            Ok(_) => {
980                tracing::debug!("Fetch completed successfully");
981                Ok(())
982            }
983            Err(e) => {
984                // Check if this is a TLS/SSL error that might be resolved by falling back to git CLI
985                let error_string = e.to_string();
986                if error_string.contains("TLS stream") || error_string.contains("SSL") {
987                    tracing::warn!(
988                        "git2 TLS error detected: {}, falling back to git CLI for fetch operation",
989                        e
990                    );
991                    return self.fetch_with_git_cli();
992                }
993                Err(CascadeError::Git(e))
994            }
995        }
996    }
997
998    /// Pull changes from remote (fetch + merge)
999    pub fn pull(&self, branch: &str) -> Result<()> {
1000        tracing::info!("Pulling branch: {}", branch);
1001
1002        // First fetch - this now includes TLS fallback
1003        match self.fetch() {
1004            Ok(_) => {}
1005            Err(e) => {
1006                // If fetch failed even with CLI fallback, try full git pull as last resort
1007                let error_string = e.to_string();
1008                if error_string.contains("TLS stream") || error_string.contains("SSL") {
1009                    tracing::warn!(
1010                        "git2 error detected: {}, falling back to git CLI for pull operation",
1011                        e
1012                    );
1013                    return self.pull_with_git_cli(branch);
1014                }
1015                return Err(e);
1016            }
1017        }
1018
1019        // Get remote tracking branch
1020        let remote_branch_name = format!("origin/{branch}");
1021        let remote_oid = self
1022            .repo
1023            .refname_to_id(&format!("refs/remotes/{remote_branch_name}"))
1024            .map_err(|e| {
1025                CascadeError::branch(format!("Remote branch {remote_branch_name} not found: {e}"))
1026            })?;
1027
1028        let remote_commit = self
1029            .repo
1030            .find_commit(remote_oid)
1031            .map_err(CascadeError::Git)?;
1032
1033        // Get current HEAD
1034        let head_commit = self.get_head_commit()?;
1035
1036        // Check if we need to merge
1037        if head_commit.id() == remote_commit.id() {
1038            tracing::debug!("Already up to date");
1039            return Ok(());
1040        }
1041
1042        // Perform merge
1043        let head_tree = head_commit.tree().map_err(CascadeError::Git)?;
1044        let remote_tree = remote_commit.tree().map_err(CascadeError::Git)?;
1045
1046        // Find merge base
1047        let merge_base_oid = self
1048            .repo
1049            .merge_base(head_commit.id(), remote_commit.id())
1050            .map_err(CascadeError::Git)?;
1051        let merge_base_commit = self
1052            .repo
1053            .find_commit(merge_base_oid)
1054            .map_err(CascadeError::Git)?;
1055        let merge_base_tree = merge_base_commit.tree().map_err(CascadeError::Git)?;
1056
1057        // 3-way merge
1058        let mut index = self
1059            .repo
1060            .merge_trees(&merge_base_tree, &head_tree, &remote_tree, None)
1061            .map_err(CascadeError::Git)?;
1062
1063        if index.has_conflicts() {
1064            return Err(CascadeError::branch(
1065                "Pull has conflicts that need manual resolution".to_string(),
1066            ));
1067        }
1068
1069        // Write merged tree and create merge commit
1070        let merged_tree_oid = index.write_tree_to(&self.repo).map_err(CascadeError::Git)?;
1071        let merged_tree = self
1072            .repo
1073            .find_tree(merged_tree_oid)
1074            .map_err(CascadeError::Git)?;
1075
1076        let signature = self.get_signature()?;
1077        let message = format!("Merge branch '{branch}' from origin");
1078
1079        self.repo
1080            .commit(
1081                Some("HEAD"),
1082                &signature,
1083                &signature,
1084                &message,
1085                &merged_tree,
1086                &[&head_commit, &remote_commit],
1087            )
1088            .map_err(CascadeError::Git)?;
1089
1090        tracing::info!("Pull completed successfully");
1091        Ok(())
1092    }
1093
1094    /// Push current branch to remote
1095    pub fn push(&self, branch: &str) -> Result<()> {
1096        // Pushing branch to remote
1097
1098        let mut remote = self
1099            .repo
1100            .find_remote("origin")
1101            .map_err(|e| CascadeError::branch(format!("No remote 'origin' found: {e}")))?;
1102
1103        let remote_url = remote.url().unwrap_or("unknown").to_string();
1104        tracing::debug!("Remote URL: {}", remote_url);
1105
1106        let refspec = format!("refs/heads/{branch}:refs/heads/{branch}");
1107        tracing::debug!("Push refspec: {}", refspec);
1108
1109        // Configure callbacks with enhanced SSL settings and error handling
1110        let mut callbacks = self.configure_remote_callbacks()?;
1111
1112        // Add enhanced progress and error callbacks for better debugging
1113        callbacks.push_update_reference(|refname, status| {
1114            if let Some(msg) = status {
1115                tracing::error!("Push failed for ref {}: {}", refname, msg);
1116                return Err(git2::Error::from_str(&format!("Push failed: {msg}")));
1117            }
1118            tracing::debug!("Push succeeded for ref: {}", refname);
1119            Ok(())
1120        });
1121
1122        // Push options with authentication and SSL config
1123        let mut push_options = git2::PushOptions::new();
1124        push_options.remote_callbacks(callbacks);
1125
1126        // Attempt push with enhanced error reporting
1127        match remote.push(&[&refspec], Some(&mut push_options)) {
1128            Ok(_) => {
1129                tracing::info!("Push completed successfully for branch: {}", branch);
1130                Ok(())
1131            }
1132            Err(e) => {
1133                // Check if this is a TLS/SSL or auth error that might be resolved by falling back to git CLI
1134                let error_string = e.to_string();
1135                tracing::debug!("git2 push error: {} (class: {:?})", error_string, e.class());
1136
1137                if error_string.contains("TLS stream")
1138                    || error_string.contains("SSL")
1139                    || e.class() == git2::ErrorClass::Ssl
1140                    || error_string.contains("authentication required")
1141                    || error_string.contains("no callback set")
1142                    || e.class() == git2::ErrorClass::Http
1143                {
1144                    // Silently fall back to git CLI without logging
1145                    return self.push_with_git_cli(branch);
1146                }
1147
1148                // Create concise error message
1149                let error_msg = if e.to_string().contains("authentication") {
1150                    format!(
1151                        "Authentication failed for branch '{branch}'. Try: git push origin {branch}"
1152                    )
1153                } else {
1154                    format!("Failed to push branch '{branch}': {e}")
1155                };
1156
1157                tracing::error!("{}", error_msg);
1158                Err(CascadeError::branch(error_msg))
1159            }
1160        }
1161    }
1162
1163    /// Fallback push method using git CLI instead of git2
1164    /// This is used when git2 has TLS/SSL or auth issues but git CLI works fine
1165    fn push_with_git_cli(&self, branch: &str) -> Result<()> {
1166        let output = std::process::Command::new("git")
1167            .args(["push", "origin", branch])
1168            .current_dir(&self.path)
1169            .output()
1170            .map_err(|e| CascadeError::branch(format!("Failed to execute git command: {e}")))?;
1171
1172        if output.status.success() {
1173            // Silent success - no need to log when fallback works
1174            Ok(())
1175        } else {
1176            let stderr = String::from_utf8_lossy(&output.stderr);
1177            let _stdout = String::from_utf8_lossy(&output.stdout);
1178            // Extract the most relevant error message
1179            let error_msg = if stderr.contains("SSL_connect") || stderr.contains("SSL_ERROR") {
1180                "Network error: Unable to connect to repository (VPN may be required)".to_string()
1181            } else if stderr.contains("repository") && stderr.contains("not found") {
1182                "Repository not found - check your Bitbucket configuration".to_string()
1183            } else if stderr.contains("authentication") || stderr.contains("403") {
1184                "Authentication failed - check your credentials".to_string()
1185            } else {
1186                // For other errors, just show the stderr without the verbose prefix
1187                stderr.trim().to_string()
1188            };
1189            tracing::error!("{}", error_msg);
1190            Err(CascadeError::branch(error_msg))
1191        }
1192    }
1193
1194    /// Fallback fetch method using git CLI instead of git2
1195    /// This is used when git2 has TLS/SSL issues but git CLI works fine
1196    fn fetch_with_git_cli(&self) -> Result<()> {
1197        tracing::info!("Using git CLI fallback for fetch operation");
1198
1199        let output = std::process::Command::new("git")
1200            .args(["fetch", "origin"])
1201            .current_dir(&self.path)
1202            .output()
1203            .map_err(|e| {
1204                CascadeError::Git(git2::Error::from_str(&format!(
1205                    "Failed to execute git command: {e}"
1206                )))
1207            })?;
1208
1209        if output.status.success() {
1210            tracing::info!("✅ Git CLI fetch succeeded");
1211            Ok(())
1212        } else {
1213            let stderr = String::from_utf8_lossy(&output.stderr);
1214            let stdout = String::from_utf8_lossy(&output.stdout);
1215            let error_msg = format!(
1216                "Git CLI fetch failed: {}\nStdout: {}\nStderr: {}",
1217                output.status, stdout, stderr
1218            );
1219            tracing::error!("{}", error_msg);
1220            Err(CascadeError::Git(git2::Error::from_str(&error_msg)))
1221        }
1222    }
1223
1224    /// Fallback pull method using git CLI instead of git2
1225    /// This is used when git2 has TLS/SSL issues but git CLI works fine
1226    fn pull_with_git_cli(&self, branch: &str) -> Result<()> {
1227        tracing::info!("Using git CLI fallback for pull operation: {}", branch);
1228
1229        let output = std::process::Command::new("git")
1230            .args(["pull", "origin", branch])
1231            .current_dir(&self.path)
1232            .output()
1233            .map_err(|e| {
1234                CascadeError::Git(git2::Error::from_str(&format!(
1235                    "Failed to execute git command: {e}"
1236                )))
1237            })?;
1238
1239        if output.status.success() {
1240            tracing::info!("✅ Git CLI pull succeeded for branch: {}", branch);
1241            Ok(())
1242        } else {
1243            let stderr = String::from_utf8_lossy(&output.stderr);
1244            let stdout = String::from_utf8_lossy(&output.stdout);
1245            let error_msg = format!(
1246                "Git CLI pull failed for branch '{}': {}\nStdout: {}\nStderr: {}",
1247                branch, output.status, stdout, stderr
1248            );
1249            tracing::error!("{}", error_msg);
1250            Err(CascadeError::Git(git2::Error::from_str(&error_msg)))
1251        }
1252    }
1253
1254    /// Fallback force push method using git CLI instead of git2
1255    /// This is used when git2 has TLS/SSL issues but git CLI works fine
1256    fn force_push_with_git_cli(&self, branch: &str) -> Result<()> {
1257        tracing::info!(
1258            "Using git CLI fallback for force push operation: {}",
1259            branch
1260        );
1261
1262        let output = std::process::Command::new("git")
1263            .args(["push", "--force", "origin", branch])
1264            .current_dir(&self.path)
1265            .output()
1266            .map_err(|e| CascadeError::branch(format!("Failed to execute git command: {e}")))?;
1267
1268        if output.status.success() {
1269            tracing::info!("✅ Git CLI force push succeeded for branch: {}", branch);
1270            Ok(())
1271        } else {
1272            let stderr = String::from_utf8_lossy(&output.stderr);
1273            let stdout = String::from_utf8_lossy(&output.stdout);
1274            let error_msg = format!(
1275                "Git CLI force push failed for branch '{}': {}\nStdout: {}\nStderr: {}",
1276                branch, output.status, stdout, stderr
1277            );
1278            tracing::error!("{}", error_msg);
1279            Err(CascadeError::branch(error_msg))
1280        }
1281    }
1282
1283    /// Delete a local branch
1284    pub fn delete_branch(&self, name: &str) -> Result<()> {
1285        self.delete_branch_with_options(name, false)
1286    }
1287
1288    /// Delete a local branch with force option to bypass safety checks
1289    pub fn delete_branch_unsafe(&self, name: &str) -> Result<()> {
1290        self.delete_branch_with_options(name, true)
1291    }
1292
1293    /// Internal branch deletion implementation with safety options
1294    fn delete_branch_with_options(&self, name: &str, force_unsafe: bool) -> Result<()> {
1295        info!("Attempting to delete branch: {}", name);
1296
1297        // Enhanced safety check: Detect unpushed commits before deletion
1298        if !force_unsafe {
1299            let safety_result = self.check_branch_deletion_safety(name)?;
1300            if let Some(safety_info) = safety_result {
1301                // Branch has unpushed commits, get user confirmation
1302                self.handle_branch_deletion_confirmation(name, &safety_info)?;
1303            }
1304        }
1305
1306        let mut branch = self
1307            .repo
1308            .find_branch(name, git2::BranchType::Local)
1309            .map_err(|e| CascadeError::branch(format!("Could not find branch '{name}': {e}")))?;
1310
1311        branch
1312            .delete()
1313            .map_err(|e| CascadeError::branch(format!("Could not delete branch '{name}': {e}")))?;
1314
1315        info!("Successfully deleted branch '{}'", name);
1316        Ok(())
1317    }
1318
1319    /// Get commits between two references
1320    pub fn get_commits_between(&self, from: &str, to: &str) -> Result<Vec<git2::Commit<'_>>> {
1321        let from_oid = self
1322            .repo
1323            .refname_to_id(&format!("refs/heads/{from}"))
1324            .or_else(|_| Oid::from_str(from))
1325            .map_err(|e| CascadeError::branch(format!("Invalid from reference '{from}': {e}")))?;
1326
1327        let to_oid = self
1328            .repo
1329            .refname_to_id(&format!("refs/heads/{to}"))
1330            .or_else(|_| Oid::from_str(to))
1331            .map_err(|e| CascadeError::branch(format!("Invalid to reference '{to}': {e}")))?;
1332
1333        let mut revwalk = self.repo.revwalk().map_err(CascadeError::Git)?;
1334
1335        revwalk.push(to_oid).map_err(CascadeError::Git)?;
1336        revwalk.hide(from_oid).map_err(CascadeError::Git)?;
1337
1338        let mut commits = Vec::new();
1339        for oid in revwalk {
1340            let oid = oid.map_err(CascadeError::Git)?;
1341            let commit = self.repo.find_commit(oid).map_err(CascadeError::Git)?;
1342            commits.push(commit);
1343        }
1344
1345        Ok(commits)
1346    }
1347
1348    /// Force push one branch's content to another branch name
1349    /// This is used to preserve PR history while updating branch contents after rebase
1350    pub fn force_push_branch(&self, target_branch: &str, source_branch: &str) -> Result<()> {
1351        self.force_push_branch_with_options(target_branch, source_branch, false)
1352    }
1353
1354    /// Force push with explicit force flag to bypass safety checks
1355    pub fn force_push_branch_unsafe(&self, target_branch: &str, source_branch: &str) -> Result<()> {
1356        self.force_push_branch_with_options(target_branch, source_branch, true)
1357    }
1358
1359    /// Internal force push implementation with safety options
1360    fn force_push_branch_with_options(
1361        &self,
1362        target_branch: &str,
1363        source_branch: &str,
1364        force_unsafe: bool,
1365    ) -> Result<()> {
1366        info!(
1367            "Force pushing {} content to {} to preserve PR history",
1368            source_branch, target_branch
1369        );
1370
1371        // Enhanced safety check: Detect potential data loss and get user confirmation
1372        if !force_unsafe {
1373            let safety_result = self.check_force_push_safety_enhanced(target_branch)?;
1374            if let Some(backup_info) = safety_result {
1375                // Create backup branch before force push
1376                self.create_backup_branch(target_branch, &backup_info.remote_commit_id)?;
1377                info!(
1378                    "✅ Created backup branch: {}",
1379                    backup_info.backup_branch_name
1380                );
1381            }
1382        }
1383
1384        // First, ensure we have the latest changes for the source branch
1385        let source_ref = self
1386            .repo
1387            .find_reference(&format!("refs/heads/{source_branch}"))
1388            .map_err(|e| {
1389                CascadeError::config(format!("Failed to find source branch {source_branch}: {e}"))
1390            })?;
1391        let source_commit = source_ref.peel_to_commit().map_err(|e| {
1392            CascadeError::config(format!(
1393                "Failed to get commit for source branch {source_branch}: {e}"
1394            ))
1395        })?;
1396
1397        // Update the target branch to point to the source commit
1398        let mut target_ref = self
1399            .repo
1400            .find_reference(&format!("refs/heads/{target_branch}"))
1401            .map_err(|e| {
1402                CascadeError::config(format!("Failed to find target branch {target_branch}: {e}"))
1403            })?;
1404
1405        target_ref
1406            .set_target(source_commit.id(), "Force push from rebase")
1407            .map_err(|e| {
1408                CascadeError::config(format!(
1409                    "Failed to update target branch {target_branch}: {e}"
1410                ))
1411            })?;
1412
1413        // Force push to remote
1414        let mut remote = self
1415            .repo
1416            .find_remote("origin")
1417            .map_err(|e| CascadeError::config(format!("Failed to find origin remote: {e}")))?;
1418
1419        let refspec = format!("+refs/heads/{target_branch}:refs/heads/{target_branch}");
1420
1421        // Configure callbacks with SSL settings from git config
1422        let callbacks = self.configure_remote_callbacks()?;
1423
1424        // Push options for force push with SSL config
1425        let mut push_options = git2::PushOptions::new();
1426        push_options.remote_callbacks(callbacks);
1427
1428        match remote.push(&[&refspec], Some(&mut push_options)) {
1429            Ok(_) => {}
1430            Err(e) => {
1431                // Check if this is a TLS/SSL error that might be resolved by falling back to git CLI
1432                let error_string = e.to_string();
1433                if error_string.contains("TLS stream") || error_string.contains("SSL") {
1434                    tracing::warn!(
1435                        "git2 TLS error detected: {}, falling back to git CLI for force push operation",
1436                        e
1437                    );
1438                    return self.force_push_with_git_cli(target_branch);
1439                }
1440                return Err(CascadeError::config(format!(
1441                    "Failed to force push {target_branch}: {e}"
1442                )));
1443            }
1444        }
1445
1446        info!(
1447            "✅ Successfully force pushed {} to preserve PR history",
1448            target_branch
1449        );
1450        Ok(())
1451    }
1452
1453    /// Enhanced safety check for force push operations with user confirmation
1454    /// Returns backup info if data would be lost and user confirms
1455    fn check_force_push_safety_enhanced(
1456        &self,
1457        target_branch: &str,
1458    ) -> Result<Option<ForceBackupInfo>> {
1459        // First fetch latest remote changes to ensure we have up-to-date information
1460        match self.fetch() {
1461            Ok(_) => {}
1462            Err(e) => {
1463                // If fetch fails, warn but don't block the operation
1464                warn!("Could not fetch latest changes for safety check: {}", e);
1465            }
1466        }
1467
1468        // Check if there are commits on the remote that would be lost
1469        let remote_ref = format!("refs/remotes/origin/{target_branch}");
1470        let local_ref = format!("refs/heads/{target_branch}");
1471
1472        // Try to find both local and remote references
1473        let local_commit = match self.repo.find_reference(&local_ref) {
1474            Ok(reference) => reference.peel_to_commit().ok(),
1475            Err(_) => None,
1476        };
1477
1478        let remote_commit = match self.repo.find_reference(&remote_ref) {
1479            Ok(reference) => reference.peel_to_commit().ok(),
1480            Err(_) => None,
1481        };
1482
1483        // If we have both commits, check for divergence
1484        if let (Some(local), Some(remote)) = (local_commit, remote_commit) {
1485            if local.id() != remote.id() {
1486                // Check if the remote has commits that the local doesn't have
1487                let merge_base_oid = self
1488                    .repo
1489                    .merge_base(local.id(), remote.id())
1490                    .map_err(|e| CascadeError::config(format!("Failed to find merge base: {e}")))?;
1491
1492                // If merge base != remote commit, remote has commits that would be lost
1493                if merge_base_oid != remote.id() {
1494                    let commits_to_lose = self.count_commits_between(
1495                        &merge_base_oid.to_string(),
1496                        &remote.id().to_string(),
1497                    )?;
1498
1499                    // Create backup branch name with timestamp
1500                    let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
1501                    let backup_branch_name = format!("{target_branch}_backup_{timestamp}");
1502
1503                    warn!(
1504                        "⚠️  Force push to '{}' would overwrite {} commits on remote",
1505                        target_branch, commits_to_lose
1506                    );
1507
1508                    // Check if we're in a non-interactive environment (CI/testing)
1509                    if std::env::var("CI").is_ok() || std::env::var("FORCE_PUSH_NO_CONFIRM").is_ok()
1510                    {
1511                        info!(
1512                            "Non-interactive environment detected, proceeding with backup creation"
1513                        );
1514                        return Ok(Some(ForceBackupInfo {
1515                            backup_branch_name,
1516                            remote_commit_id: remote.id().to_string(),
1517                            commits_that_would_be_lost: commits_to_lose,
1518                        }));
1519                    }
1520
1521                    // Interactive confirmation
1522                    println!("\n⚠️  FORCE PUSH WARNING ⚠️");
1523                    println!("Force push to '{target_branch}' would overwrite {commits_to_lose} commits on remote:");
1524
1525                    // Show the commits that would be lost
1526                    match self
1527                        .get_commits_between(&merge_base_oid.to_string(), &remote.id().to_string())
1528                    {
1529                        Ok(commits) => {
1530                            println!("\nCommits that would be lost:");
1531                            for (i, commit) in commits.iter().take(5).enumerate() {
1532                                let short_hash = &commit.id().to_string()[..8];
1533                                let summary = commit.summary().unwrap_or("<no message>");
1534                                println!("  {}. {} - {}", i + 1, short_hash, summary);
1535                            }
1536                            if commits.len() > 5 {
1537                                println!("  ... and {} more commits", commits.len() - 5);
1538                            }
1539                        }
1540                        Err(_) => {
1541                            println!("  (Unable to retrieve commit details)");
1542                        }
1543                    }
1544
1545                    println!("\nA backup branch '{backup_branch_name}' will be created before proceeding.");
1546
1547                    let confirmed = Confirm::with_theme(&ColorfulTheme::default())
1548                        .with_prompt("Do you want to proceed with the force push?")
1549                        .default(false)
1550                        .interact()
1551                        .map_err(|e| {
1552                            CascadeError::config(format!("Failed to get user confirmation: {e}"))
1553                        })?;
1554
1555                    if !confirmed {
1556                        return Err(CascadeError::config(
1557                            "Force push cancelled by user. Use --force to bypass this check."
1558                                .to_string(),
1559                        ));
1560                    }
1561
1562                    return Ok(Some(ForceBackupInfo {
1563                        backup_branch_name,
1564                        remote_commit_id: remote.id().to_string(),
1565                        commits_that_would_be_lost: commits_to_lose,
1566                    }));
1567                }
1568            }
1569        }
1570
1571        Ok(None)
1572    }
1573
1574    /// Create a backup branch pointing to the remote commit that would be lost
1575    fn create_backup_branch(&self, original_branch: &str, remote_commit_id: &str) -> Result<()> {
1576        let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
1577        let backup_branch_name = format!("{original_branch}_backup_{timestamp}");
1578
1579        // Parse the commit ID
1580        let commit_oid = Oid::from_str(remote_commit_id).map_err(|e| {
1581            CascadeError::config(format!("Invalid commit ID {remote_commit_id}: {e}"))
1582        })?;
1583
1584        // Find the commit
1585        let commit = self.repo.find_commit(commit_oid).map_err(|e| {
1586            CascadeError::config(format!("Failed to find commit {remote_commit_id}: {e}"))
1587        })?;
1588
1589        // Create the backup branch
1590        self.repo
1591            .branch(&backup_branch_name, &commit, false)
1592            .map_err(|e| {
1593                CascadeError::config(format!(
1594                    "Failed to create backup branch {backup_branch_name}: {e}"
1595                ))
1596            })?;
1597
1598        info!(
1599            "✅ Created backup branch '{}' pointing to {}",
1600            backup_branch_name,
1601            &remote_commit_id[..8]
1602        );
1603        Ok(())
1604    }
1605
1606    /// Check if branch deletion is safe by detecting unpushed commits
1607    /// Returns safety info if there are concerns that need user attention
1608    fn check_branch_deletion_safety(
1609        &self,
1610        branch_name: &str,
1611    ) -> Result<Option<BranchDeletionSafety>> {
1612        // First, try to fetch latest remote changes
1613        match self.fetch() {
1614            Ok(_) => {}
1615            Err(e) => {
1616                warn!(
1617                    "Could not fetch latest changes for branch deletion safety check: {}",
1618                    e
1619                );
1620            }
1621        }
1622
1623        // Find the branch
1624        let branch = self
1625            .repo
1626            .find_branch(branch_name, git2::BranchType::Local)
1627            .map_err(|e| {
1628                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
1629            })?;
1630
1631        let _branch_commit = branch.get().peel_to_commit().map_err(|e| {
1632            CascadeError::branch(format!(
1633                "Could not get commit for branch '{branch_name}': {e}"
1634            ))
1635        })?;
1636
1637        // Determine the main branch (try common names)
1638        let main_branch_name = self.detect_main_branch()?;
1639
1640        // Check if branch is merged to main
1641        let is_merged_to_main = self.is_branch_merged_to_main(branch_name, &main_branch_name)?;
1642
1643        // Find the upstream/remote tracking branch
1644        let remote_tracking_branch = self.get_remote_tracking_branch(branch_name);
1645
1646        let mut unpushed_commits = Vec::new();
1647
1648        // Check for unpushed commits compared to remote tracking branch
1649        if let Some(ref remote_branch) = remote_tracking_branch {
1650            match self.get_commits_between(remote_branch, branch_name) {
1651                Ok(commits) => {
1652                    unpushed_commits = commits.iter().map(|c| c.id().to_string()).collect();
1653                }
1654                Err(_) => {
1655                    // If we can't compare with remote, check against main branch
1656                    if !is_merged_to_main {
1657                        if let Ok(commits) =
1658                            self.get_commits_between(&main_branch_name, branch_name)
1659                        {
1660                            unpushed_commits = commits.iter().map(|c| c.id().to_string()).collect();
1661                        }
1662                    }
1663                }
1664            }
1665        } else if !is_merged_to_main {
1666            // No remote tracking branch, check against main
1667            if let Ok(commits) = self.get_commits_between(&main_branch_name, branch_name) {
1668                unpushed_commits = commits.iter().map(|c| c.id().to_string()).collect();
1669            }
1670        }
1671
1672        // If there are concerns, return safety info
1673        if !unpushed_commits.is_empty() || (!is_merged_to_main && remote_tracking_branch.is_none())
1674        {
1675            Ok(Some(BranchDeletionSafety {
1676                unpushed_commits,
1677                remote_tracking_branch,
1678                is_merged_to_main,
1679                main_branch_name,
1680            }))
1681        } else {
1682            Ok(None)
1683        }
1684    }
1685
1686    /// Handle user confirmation for branch deletion with safety concerns
1687    fn handle_branch_deletion_confirmation(
1688        &self,
1689        branch_name: &str,
1690        safety_info: &BranchDeletionSafety,
1691    ) -> Result<()> {
1692        // Check if we're in a non-interactive environment
1693        if std::env::var("CI").is_ok() || std::env::var("BRANCH_DELETE_NO_CONFIRM").is_ok() {
1694            return Err(CascadeError::branch(
1695                format!(
1696                    "Branch '{branch_name}' has {} unpushed commits and cannot be deleted in non-interactive mode. Use --force to override.",
1697                    safety_info.unpushed_commits.len()
1698                )
1699            ));
1700        }
1701
1702        // Interactive warning and confirmation
1703        println!("\n⚠️  BRANCH DELETION WARNING ⚠️");
1704        println!("Branch '{branch_name}' has potential issues:");
1705
1706        if !safety_info.unpushed_commits.is_empty() {
1707            println!(
1708                "\n🔍 Unpushed commits ({} total):",
1709                safety_info.unpushed_commits.len()
1710            );
1711
1712            // Show details of unpushed commits
1713            for (i, commit_id) in safety_info.unpushed_commits.iter().take(5).enumerate() {
1714                if let Ok(commit) = self.repo.find_commit(Oid::from_str(commit_id).unwrap()) {
1715                    let short_hash = &commit_id[..8];
1716                    let summary = commit.summary().unwrap_or("<no message>");
1717                    println!("  {}. {} - {}", i + 1, short_hash, summary);
1718                }
1719            }
1720
1721            if safety_info.unpushed_commits.len() > 5 {
1722                println!(
1723                    "  ... and {} more commits",
1724                    safety_info.unpushed_commits.len() - 5
1725                );
1726            }
1727        }
1728
1729        if !safety_info.is_merged_to_main {
1730            println!("\n📋 Branch status:");
1731            println!("  • Not merged to '{}'", safety_info.main_branch_name);
1732            if let Some(ref remote) = safety_info.remote_tracking_branch {
1733                println!("  • Remote tracking branch: {remote}");
1734            } else {
1735                println!("  • No remote tracking branch");
1736            }
1737        }
1738
1739        println!("\n💡 Safer alternatives:");
1740        if !safety_info.unpushed_commits.is_empty() {
1741            if let Some(ref _remote) = safety_info.remote_tracking_branch {
1742                println!("  • Push commits first: git push origin {branch_name}");
1743            } else {
1744                println!("  • Create and push to remote: git push -u origin {branch_name}");
1745            }
1746        }
1747        if !safety_info.is_merged_to_main {
1748            println!(
1749                "  • Merge to {} first: git checkout {} && git merge {branch_name}",
1750                safety_info.main_branch_name, safety_info.main_branch_name
1751            );
1752        }
1753
1754        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
1755            .with_prompt("Do you want to proceed with deleting this branch?")
1756            .default(false)
1757            .interact()
1758            .map_err(|e| CascadeError::branch(format!("Failed to get user confirmation: {e}")))?;
1759
1760        if !confirmed {
1761            return Err(CascadeError::branch(
1762                "Branch deletion cancelled by user. Use --force to bypass this check.".to_string(),
1763            ));
1764        }
1765
1766        Ok(())
1767    }
1768
1769    /// Detect the main branch name (main, master, develop)
1770    pub fn detect_main_branch(&self) -> Result<String> {
1771        let main_candidates = ["main", "master", "develop", "trunk"];
1772
1773        for candidate in &main_candidates {
1774            if self
1775                .repo
1776                .find_branch(candidate, git2::BranchType::Local)
1777                .is_ok()
1778            {
1779                return Ok(candidate.to_string());
1780            }
1781        }
1782
1783        // Fallback to HEAD's target if it's a symbolic reference
1784        if let Ok(head) = self.repo.head() {
1785            if let Some(name) = head.shorthand() {
1786                return Ok(name.to_string());
1787            }
1788        }
1789
1790        // Final fallback
1791        Ok("main".to_string())
1792    }
1793
1794    /// Check if a branch is merged to the main branch
1795    fn is_branch_merged_to_main(&self, branch_name: &str, main_branch: &str) -> Result<bool> {
1796        // Get the commits between main and the branch
1797        match self.get_commits_between(main_branch, branch_name) {
1798            Ok(commits) => Ok(commits.is_empty()),
1799            Err(_) => {
1800                // If we can't determine, assume not merged for safety
1801                Ok(false)
1802            }
1803        }
1804    }
1805
1806    /// Get the remote tracking branch for a local branch
1807    fn get_remote_tracking_branch(&self, branch_name: &str) -> Option<String> {
1808        // Try common remote tracking branch patterns
1809        let remote_candidates = [
1810            format!("origin/{branch_name}"),
1811            format!("remotes/origin/{branch_name}"),
1812        ];
1813
1814        for candidate in &remote_candidates {
1815            if self
1816                .repo
1817                .find_reference(&format!(
1818                    "refs/remotes/{}",
1819                    candidate.replace("remotes/", "")
1820                ))
1821                .is_ok()
1822            {
1823                return Some(candidate.clone());
1824            }
1825        }
1826
1827        None
1828    }
1829
1830    /// Check if checkout operation is safe
1831    fn check_checkout_safety(&self, _target: &str) -> Result<Option<CheckoutSafety>> {
1832        // Check if there are uncommitted changes
1833        let is_dirty = self.is_dirty()?;
1834        if !is_dirty {
1835            // No uncommitted changes, checkout is safe
1836            return Ok(None);
1837        }
1838
1839        // Get current branch for context
1840        let current_branch = self.get_current_branch().ok();
1841
1842        // Get detailed information about uncommitted changes
1843        let modified_files = self.get_modified_files()?;
1844        let staged_files = self.get_staged_files()?;
1845        let untracked_files = self.get_untracked_files()?;
1846
1847        let has_uncommitted_changes = !modified_files.is_empty() || !staged_files.is_empty();
1848
1849        if has_uncommitted_changes || !untracked_files.is_empty() {
1850            return Ok(Some(CheckoutSafety {
1851                has_uncommitted_changes,
1852                modified_files,
1853                staged_files,
1854                untracked_files,
1855                stash_created: None,
1856                current_branch,
1857            }));
1858        }
1859
1860        Ok(None)
1861    }
1862
1863    /// Handle user confirmation for checkout operations with uncommitted changes
1864    fn handle_checkout_confirmation(
1865        &self,
1866        target: &str,
1867        safety_info: &CheckoutSafety,
1868    ) -> Result<()> {
1869        // Check if we're in a non-interactive environment FIRST (before any output)
1870        let is_ci = std::env::var("CI").is_ok();
1871        let no_confirm = std::env::var("CHECKOUT_NO_CONFIRM").is_ok();
1872        let is_non_interactive = is_ci || no_confirm;
1873
1874        if is_non_interactive {
1875            return Err(CascadeError::branch(
1876                format!(
1877                    "Cannot checkout '{target}' with uncommitted changes in non-interactive mode. Commit your changes or use stash first."
1878                )
1879            ));
1880        }
1881
1882        // Interactive warning and confirmation
1883        println!("\n⚠️  CHECKOUT WARNING ⚠️");
1884        println!("You have uncommitted changes that could be lost:");
1885
1886        if !safety_info.modified_files.is_empty() {
1887            println!(
1888                "\n📝 Modified files ({}):",
1889                safety_info.modified_files.len()
1890            );
1891            for file in safety_info.modified_files.iter().take(10) {
1892                println!("   - {file}");
1893            }
1894            if safety_info.modified_files.len() > 10 {
1895                println!("   ... and {} more", safety_info.modified_files.len() - 10);
1896            }
1897        }
1898
1899        if !safety_info.staged_files.is_empty() {
1900            println!("\n📁 Staged files ({}):", safety_info.staged_files.len());
1901            for file in safety_info.staged_files.iter().take(10) {
1902                println!("   - {file}");
1903            }
1904            if safety_info.staged_files.len() > 10 {
1905                println!("   ... and {} more", safety_info.staged_files.len() - 10);
1906            }
1907        }
1908
1909        if !safety_info.untracked_files.is_empty() {
1910            println!(
1911                "\n❓ Untracked files ({}):",
1912                safety_info.untracked_files.len()
1913            );
1914            for file in safety_info.untracked_files.iter().take(5) {
1915                println!("   - {file}");
1916            }
1917            if safety_info.untracked_files.len() > 5 {
1918                println!("   ... and {} more", safety_info.untracked_files.len() - 5);
1919            }
1920        }
1921
1922        println!("\n🔄 Options:");
1923        println!("1. Stash changes and checkout (recommended)");
1924        println!("2. Force checkout (WILL LOSE UNCOMMITTED CHANGES)");
1925        println!("3. Cancel checkout");
1926
1927        // Use proper selection dialog instead of y/n confirmation
1928        let selection = Select::with_theme(&ColorfulTheme::default())
1929            .with_prompt("Choose an action")
1930            .items(&[
1931                "Stash changes and checkout (recommended)",
1932                "Force checkout (WILL LOSE UNCOMMITTED CHANGES)",
1933                "Cancel checkout",
1934            ])
1935            .default(0)
1936            .interact()
1937            .map_err(|e| CascadeError::branch(format!("Could not get user selection: {e}")))?;
1938
1939        match selection {
1940            0 => {
1941                // Option 1: Stash changes and checkout
1942                let stash_message = format!(
1943                    "Auto-stash before checkout to {} at {}",
1944                    target,
1945                    chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
1946                );
1947
1948                match self.create_stash(&stash_message) {
1949                    Ok(stash_id) => {
1950                        println!("✅ Created stash: {stash_message} ({stash_id})");
1951                        println!("💡 You can restore with: git stash pop");
1952                    }
1953                    Err(e) => {
1954                        println!("❌ Failed to create stash: {e}");
1955
1956                        // If stash failed, provide better options
1957                        use dialoguer::Select;
1958                        let stash_failed_options = vec![
1959                            "Commit staged changes and proceed",
1960                            "Force checkout (WILL LOSE CHANGES)",
1961                            "Cancel and handle manually",
1962                        ];
1963
1964                        let stash_selection = Select::with_theme(&ColorfulTheme::default())
1965                            .with_prompt("Stash failed. What would you like to do?")
1966                            .items(&stash_failed_options)
1967                            .default(0)
1968                            .interact()
1969                            .map_err(|e| {
1970                                CascadeError::branch(format!("Could not get user selection: {e}"))
1971                            })?;
1972
1973                        match stash_selection {
1974                            0 => {
1975                                // Try to commit staged changes
1976                                let staged_files = self.get_staged_files()?;
1977                                if !staged_files.is_empty() {
1978                                    println!(
1979                                        "📝 Committing {} staged files...",
1980                                        staged_files.len()
1981                                    );
1982                                    match self
1983                                        .commit_staged_changes("WIP: Auto-commit before checkout")
1984                                    {
1985                                        Ok(Some(commit_hash)) => {
1986                                            println!(
1987                                                "✅ Committed staged changes as {}",
1988                                                &commit_hash[..8]
1989                                            );
1990                                            println!("💡 You can undo with: git reset HEAD~1");
1991                                        }
1992                                        Ok(None) => {
1993                                            println!("ℹ️  No staged changes found to commit");
1994                                        }
1995                                        Err(commit_err) => {
1996                                            println!(
1997                                                "❌ Failed to commit staged changes: {commit_err}"
1998                                            );
1999                                            return Err(CascadeError::branch(
2000                                                "Could not commit staged changes".to_string(),
2001                                            ));
2002                                        }
2003                                    }
2004                                } else {
2005                                    println!("ℹ️  No staged changes to commit");
2006                                }
2007                            }
2008                            1 => {
2009                                // Force checkout anyway
2010                                println!("⚠️  Proceeding with force checkout - uncommitted changes will be lost!");
2011                            }
2012                            2 => {
2013                                // Cancel
2014                                return Err(CascadeError::branch(
2015                                    "Checkout cancelled. Please handle changes manually and try again.".to_string(),
2016                                ));
2017                            }
2018                            _ => unreachable!(),
2019                        }
2020                    }
2021                }
2022            }
2023            1 => {
2024                // Option 2: Force checkout (lose changes)
2025                println!("⚠️  Proceeding with force checkout - uncommitted changes will be lost!");
2026            }
2027            2 => {
2028                // Option 3: Cancel
2029                return Err(CascadeError::branch(
2030                    "Checkout cancelled by user".to_string(),
2031                ));
2032            }
2033            _ => unreachable!(),
2034        }
2035
2036        Ok(())
2037    }
2038
2039    /// Create a stash with uncommitted changes
2040    fn create_stash(&self, message: &str) -> Result<String> {
2041        tracing::info!("Creating stash: {}", message);
2042
2043        // Use git CLI for stashing since git2 stashing is complex and unreliable
2044        let output = std::process::Command::new("git")
2045            .args(["stash", "push", "-m", message])
2046            .current_dir(&self.path)
2047            .output()
2048            .map_err(|e| {
2049                CascadeError::branch(format!("Failed to execute git stash command: {e}"))
2050            })?;
2051
2052        if output.status.success() {
2053            let stdout = String::from_utf8_lossy(&output.stdout);
2054
2055            // Extract stash hash if available (git stash outputs like "Saved working directory and index state WIP on branch: message")
2056            let stash_id = if stdout.contains("Saved working directory") {
2057                // Get the most recent stash ID
2058                let stash_list_output = std::process::Command::new("git")
2059                    .args(["stash", "list", "-n", "1", "--format=%H"])
2060                    .current_dir(&self.path)
2061                    .output()
2062                    .map_err(|e| CascadeError::branch(format!("Failed to get stash ID: {e}")))?;
2063
2064                if stash_list_output.status.success() {
2065                    String::from_utf8_lossy(&stash_list_output.stdout)
2066                        .trim()
2067                        .to_string()
2068                } else {
2069                    "stash@{0}".to_string() // fallback
2070                }
2071            } else {
2072                "stash@{0}".to_string() // fallback
2073            };
2074
2075            tracing::info!("✅ Created stash: {} ({})", message, stash_id);
2076            Ok(stash_id)
2077        } else {
2078            let stderr = String::from_utf8_lossy(&output.stderr);
2079            let stdout = String::from_utf8_lossy(&output.stdout);
2080
2081            // Check for common stash failure reasons
2082            if stderr.contains("No local changes to save")
2083                || stdout.contains("No local changes to save")
2084            {
2085                return Err(CascadeError::branch("No local changes to save".to_string()));
2086            }
2087
2088            Err(CascadeError::branch(format!(
2089                "Failed to create stash: {}\nStderr: {}\nStdout: {}",
2090                output.status, stderr, stdout
2091            )))
2092        }
2093    }
2094
2095    /// Get modified files in working directory
2096    fn get_modified_files(&self) -> Result<Vec<String>> {
2097        let mut opts = git2::StatusOptions::new();
2098        opts.include_untracked(false).include_ignored(false);
2099
2100        let statuses = self
2101            .repo
2102            .statuses(Some(&mut opts))
2103            .map_err(|e| CascadeError::branch(format!("Could not get repository status: {e}")))?;
2104
2105        let mut modified_files = Vec::new();
2106        for status in statuses.iter() {
2107            let flags = status.status();
2108            if flags.contains(git2::Status::WT_MODIFIED) || flags.contains(git2::Status::WT_DELETED)
2109            {
2110                if let Some(path) = status.path() {
2111                    modified_files.push(path.to_string());
2112                }
2113            }
2114        }
2115
2116        Ok(modified_files)
2117    }
2118
2119    /// Get staged files in index
2120    pub fn get_staged_files(&self) -> Result<Vec<String>> {
2121        let mut opts = git2::StatusOptions::new();
2122        opts.include_untracked(false).include_ignored(false);
2123
2124        let statuses = self
2125            .repo
2126            .statuses(Some(&mut opts))
2127            .map_err(|e| CascadeError::branch(format!("Could not get repository status: {e}")))?;
2128
2129        let mut staged_files = Vec::new();
2130        for status in statuses.iter() {
2131            let flags = status.status();
2132            if flags.contains(git2::Status::INDEX_MODIFIED)
2133                || flags.contains(git2::Status::INDEX_NEW)
2134                || flags.contains(git2::Status::INDEX_DELETED)
2135            {
2136                if let Some(path) = status.path() {
2137                    staged_files.push(path.to_string());
2138                }
2139            }
2140        }
2141
2142        Ok(staged_files)
2143    }
2144
2145    /// Count commits between two references
2146    fn count_commits_between(&self, from: &str, to: &str) -> Result<usize> {
2147        let commits = self.get_commits_between(from, to)?;
2148        Ok(commits.len())
2149    }
2150
2151    /// Resolve a reference (branch name, tag, or commit hash) to a commit
2152    pub fn resolve_reference(&self, reference: &str) -> Result<git2::Commit<'_>> {
2153        // Try to parse as commit hash first
2154        if let Ok(oid) = Oid::from_str(reference) {
2155            if let Ok(commit) = self.repo.find_commit(oid) {
2156                return Ok(commit);
2157            }
2158        }
2159
2160        // Try to resolve as a reference (branch, tag, etc.)
2161        let obj = self.repo.revparse_single(reference).map_err(|e| {
2162            CascadeError::branch(format!("Could not resolve reference '{reference}': {e}"))
2163        })?;
2164
2165        obj.peel_to_commit().map_err(|e| {
2166            CascadeError::branch(format!(
2167                "Reference '{reference}' does not point to a commit: {e}"
2168            ))
2169        })
2170    }
2171
2172    /// Reset HEAD to a specific reference (soft reset)
2173    pub fn reset_soft(&self, target_ref: &str) -> Result<()> {
2174        let target_commit = self.resolve_reference(target_ref)?;
2175
2176        self.repo
2177            .reset(target_commit.as_object(), git2::ResetType::Soft, None)
2178            .map_err(CascadeError::Git)?;
2179
2180        Ok(())
2181    }
2182
2183    /// Find which branch contains a specific commit
2184    pub fn find_branch_containing_commit(&self, commit_hash: &str) -> Result<String> {
2185        let oid = Oid::from_str(commit_hash).map_err(|e| {
2186            CascadeError::branch(format!("Invalid commit hash '{commit_hash}': {e}"))
2187        })?;
2188
2189        // Get all local branches
2190        let branches = self
2191            .repo
2192            .branches(Some(git2::BranchType::Local))
2193            .map_err(CascadeError::Git)?;
2194
2195        for branch_result in branches {
2196            let (branch, _) = branch_result.map_err(CascadeError::Git)?;
2197
2198            if let Some(branch_name) = branch.name().map_err(CascadeError::Git)? {
2199                // Check if this branch contains the commit
2200                if let Ok(branch_head) = branch.get().peel_to_commit() {
2201                    // Walk the commit history from this branch's HEAD
2202                    let mut revwalk = self.repo.revwalk().map_err(CascadeError::Git)?;
2203                    revwalk.push(branch_head.id()).map_err(CascadeError::Git)?;
2204
2205                    for commit_oid in revwalk {
2206                        let commit_oid = commit_oid.map_err(CascadeError::Git)?;
2207                        if commit_oid == oid {
2208                            return Ok(branch_name.to_string());
2209                        }
2210                    }
2211                }
2212            }
2213        }
2214
2215        // If not found in any branch, might be on current HEAD
2216        Err(CascadeError::branch(format!(
2217            "Commit {commit_hash} not found in any local branch"
2218        )))
2219    }
2220
2221    // Async wrappers for potentially blocking operations
2222
2223    /// Fetch from remote origin (async)
2224    pub async fn fetch_async(&self) -> Result<()> {
2225        let repo_path = self.path.clone();
2226        crate::utils::async_ops::run_git_operation(move || {
2227            let repo = GitRepository::open(&repo_path)?;
2228            repo.fetch()
2229        })
2230        .await
2231    }
2232
2233    /// Pull changes from remote (async)
2234    pub async fn pull_async(&self, branch: &str) -> Result<()> {
2235        let repo_path = self.path.clone();
2236        let branch_name = branch.to_string();
2237        crate::utils::async_ops::run_git_operation(move || {
2238            let repo = GitRepository::open(&repo_path)?;
2239            repo.pull(&branch_name)
2240        })
2241        .await
2242    }
2243
2244    /// Push branch to remote (async)
2245    pub async fn push_branch_async(&self, branch_name: &str) -> Result<()> {
2246        let repo_path = self.path.clone();
2247        let branch = branch_name.to_string();
2248        crate::utils::async_ops::run_git_operation(move || {
2249            let repo = GitRepository::open(&repo_path)?;
2250            repo.push(&branch)
2251        })
2252        .await
2253    }
2254
2255    /// Cherry-pick commit (async)
2256    pub async fn cherry_pick_commit_async(&self, commit_hash: &str) -> Result<String> {
2257        let repo_path = self.path.clone();
2258        let hash = commit_hash.to_string();
2259        crate::utils::async_ops::run_git_operation(move || {
2260            let repo = GitRepository::open(&repo_path)?;
2261            repo.cherry_pick(&hash)
2262        })
2263        .await
2264    }
2265
2266    /// Get commit hashes between two refs (async)
2267    pub async fn get_commit_hashes_between_async(
2268        &self,
2269        from: &str,
2270        to: &str,
2271    ) -> Result<Vec<String>> {
2272        let repo_path = self.path.clone();
2273        let from_str = from.to_string();
2274        let to_str = to.to_string();
2275        crate::utils::async_ops::run_git_operation(move || {
2276            let repo = GitRepository::open(&repo_path)?;
2277            let commits = repo.get_commits_between(&from_str, &to_str)?;
2278            Ok(commits.into_iter().map(|c| c.id().to_string()).collect())
2279        })
2280        .await
2281    }
2282
2283    /// Reset a branch to point to a specific commit
2284    pub fn reset_branch_to_commit(&self, branch_name: &str, commit_hash: &str) -> Result<()> {
2285        info!(
2286            "Resetting branch '{}' to commit {}",
2287            branch_name,
2288            &commit_hash[..8]
2289        );
2290
2291        // Find the target commit
2292        let target_oid = git2::Oid::from_str(commit_hash).map_err(|e| {
2293            CascadeError::branch(format!("Invalid commit hash '{commit_hash}': {e}"))
2294        })?;
2295
2296        let _target_commit = self.repo.find_commit(target_oid).map_err(|e| {
2297            CascadeError::branch(format!("Could not find commit '{commit_hash}': {e}"))
2298        })?;
2299
2300        // Find the branch
2301        let _branch = self
2302            .repo
2303            .find_branch(branch_name, git2::BranchType::Local)
2304            .map_err(|e| {
2305                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
2306            })?;
2307
2308        // Update the branch reference to point to the target commit
2309        let branch_ref_name = format!("refs/heads/{branch_name}");
2310        self.repo
2311            .reference(
2312                &branch_ref_name,
2313                target_oid,
2314                true,
2315                &format!("Reset {branch_name} to {commit_hash}"),
2316            )
2317            .map_err(|e| {
2318                CascadeError::branch(format!(
2319                    "Could not reset branch '{branch_name}' to commit '{commit_hash}': {e}"
2320                ))
2321            })?;
2322
2323        tracing::info!(
2324            "Successfully reset branch '{}' to commit {}",
2325            branch_name,
2326            &commit_hash[..8]
2327        );
2328        Ok(())
2329    }
2330}
2331
2332#[cfg(test)]
2333mod tests {
2334    use super::*;
2335    use std::process::Command;
2336    use tempfile::TempDir;
2337
2338    fn create_test_repo() -> (TempDir, PathBuf) {
2339        let temp_dir = TempDir::new().unwrap();
2340        let repo_path = temp_dir.path().to_path_buf();
2341
2342        // Initialize git repository
2343        Command::new("git")
2344            .args(["init"])
2345            .current_dir(&repo_path)
2346            .output()
2347            .unwrap();
2348        Command::new("git")
2349            .args(["config", "user.name", "Test"])
2350            .current_dir(&repo_path)
2351            .output()
2352            .unwrap();
2353        Command::new("git")
2354            .args(["config", "user.email", "test@test.com"])
2355            .current_dir(&repo_path)
2356            .output()
2357            .unwrap();
2358
2359        // Create initial commit
2360        std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
2361        Command::new("git")
2362            .args(["add", "."])
2363            .current_dir(&repo_path)
2364            .output()
2365            .unwrap();
2366        Command::new("git")
2367            .args(["commit", "-m", "Initial commit"])
2368            .current_dir(&repo_path)
2369            .output()
2370            .unwrap();
2371
2372        (temp_dir, repo_path)
2373    }
2374
2375    fn create_commit(repo_path: &PathBuf, message: &str, filename: &str) {
2376        let file_path = repo_path.join(filename);
2377        std::fs::write(&file_path, format!("Content for {filename}\n")).unwrap();
2378
2379        Command::new("git")
2380            .args(["add", filename])
2381            .current_dir(repo_path)
2382            .output()
2383            .unwrap();
2384        Command::new("git")
2385            .args(["commit", "-m", message])
2386            .current_dir(repo_path)
2387            .output()
2388            .unwrap();
2389    }
2390
2391    #[test]
2392    fn test_repository_info() {
2393        let (_temp_dir, repo_path) = create_test_repo();
2394        let repo = GitRepository::open(&repo_path).unwrap();
2395
2396        let info = repo.get_info().unwrap();
2397        assert!(!info.is_dirty); // Should be clean after commit
2398        assert!(
2399            info.head_branch == Some("master".to_string())
2400                || info.head_branch == Some("main".to_string()),
2401            "Expected default branch to be 'master' or 'main', got {:?}",
2402            info.head_branch
2403        );
2404        assert!(info.head_commit.is_some()); // Just check it exists
2405        assert!(info.untracked_files.is_empty()); // Should be empty after commit
2406    }
2407
2408    #[test]
2409    fn test_force_push_branch_basic() {
2410        let (_temp_dir, repo_path) = create_test_repo();
2411        let repo = GitRepository::open(&repo_path).unwrap();
2412
2413        // Get the actual default branch name
2414        let default_branch = repo.get_current_branch().unwrap();
2415
2416        // Create source branch with commits
2417        create_commit(&repo_path, "Feature commit 1", "feature1.rs");
2418        Command::new("git")
2419            .args(["checkout", "-b", "source-branch"])
2420            .current_dir(&repo_path)
2421            .output()
2422            .unwrap();
2423        create_commit(&repo_path, "Feature commit 2", "feature2.rs");
2424
2425        // Create target branch
2426        Command::new("git")
2427            .args(["checkout", &default_branch])
2428            .current_dir(&repo_path)
2429            .output()
2430            .unwrap();
2431        Command::new("git")
2432            .args(["checkout", "-b", "target-branch"])
2433            .current_dir(&repo_path)
2434            .output()
2435            .unwrap();
2436        create_commit(&repo_path, "Target commit", "target.rs");
2437
2438        // Test force push from source to target
2439        let result = repo.force_push_branch("target-branch", "source-branch");
2440
2441        // Should succeed in test environment (even though it doesn't actually push to remote)
2442        // The important thing is that the function doesn't panic and handles the git2 operations
2443        assert!(result.is_ok() || result.is_err()); // Either is acceptable for unit test
2444    }
2445
2446    #[test]
2447    fn test_force_push_branch_nonexistent_branches() {
2448        let (_temp_dir, repo_path) = create_test_repo();
2449        let repo = GitRepository::open(&repo_path).unwrap();
2450
2451        // Get the actual default branch name
2452        let default_branch = repo.get_current_branch().unwrap();
2453
2454        // Test force push with nonexistent source branch
2455        let result = repo.force_push_branch("target", "nonexistent-source");
2456        assert!(result.is_err());
2457
2458        // Test force push with nonexistent target branch
2459        let result = repo.force_push_branch("nonexistent-target", &default_branch);
2460        assert!(result.is_err());
2461    }
2462
2463    #[test]
2464    fn test_force_push_workflow_simulation() {
2465        let (_temp_dir, repo_path) = create_test_repo();
2466        let repo = GitRepository::open(&repo_path).unwrap();
2467
2468        // Simulate the smart force push workflow:
2469        // 1. Original branch exists with PR
2470        Command::new("git")
2471            .args(["checkout", "-b", "feature-auth"])
2472            .current_dir(&repo_path)
2473            .output()
2474            .unwrap();
2475        create_commit(&repo_path, "Add authentication", "auth.rs");
2476
2477        // 2. Rebase creates versioned branch
2478        Command::new("git")
2479            .args(["checkout", "-b", "feature-auth-v2"])
2480            .current_dir(&repo_path)
2481            .output()
2482            .unwrap();
2483        create_commit(&repo_path, "Fix auth validation", "auth.rs");
2484
2485        // 3. Smart force push: update original branch from versioned branch
2486        let result = repo.force_push_branch("feature-auth", "feature-auth-v2");
2487
2488        // Verify the operation is handled properly (success or expected error)
2489        match result {
2490            Ok(_) => {
2491                // Force push succeeded - verify branch state if possible
2492                Command::new("git")
2493                    .args(["checkout", "feature-auth"])
2494                    .current_dir(&repo_path)
2495                    .output()
2496                    .unwrap();
2497                let log_output = Command::new("git")
2498                    .args(["log", "--oneline", "-2"])
2499                    .current_dir(&repo_path)
2500                    .output()
2501                    .unwrap();
2502                let log_str = String::from_utf8_lossy(&log_output.stdout);
2503                assert!(
2504                    log_str.contains("Fix auth validation")
2505                        || log_str.contains("Add authentication")
2506                );
2507            }
2508            Err(_) => {
2509                // Expected in test environment without remote - that's fine
2510                // The important thing is we tested the code path without panicking
2511            }
2512        }
2513    }
2514
2515    #[test]
2516    fn test_branch_operations() {
2517        let (_temp_dir, repo_path) = create_test_repo();
2518        let repo = GitRepository::open(&repo_path).unwrap();
2519
2520        // Test get current branch - accept either main or master
2521        let current = repo.get_current_branch().unwrap();
2522        assert!(
2523            current == "master" || current == "main",
2524            "Expected default branch to be 'master' or 'main', got '{current}'"
2525        );
2526
2527        // Test create branch
2528        Command::new("git")
2529            .args(["checkout", "-b", "test-branch"])
2530            .current_dir(&repo_path)
2531            .output()
2532            .unwrap();
2533        let current = repo.get_current_branch().unwrap();
2534        assert_eq!(current, "test-branch");
2535    }
2536
2537    #[test]
2538    fn test_commit_operations() {
2539        let (_temp_dir, repo_path) = create_test_repo();
2540        let repo = GitRepository::open(&repo_path).unwrap();
2541
2542        // Test get head commit
2543        let head = repo.get_head_commit().unwrap();
2544        assert_eq!(head.message().unwrap().trim(), "Initial commit");
2545
2546        // Test get commit by hash
2547        let hash = head.id().to_string();
2548        let same_commit = repo.get_commit(&hash).unwrap();
2549        assert_eq!(head.id(), same_commit.id());
2550    }
2551
2552    #[test]
2553    fn test_checkout_safety_clean_repo() {
2554        let (_temp_dir, repo_path) = create_test_repo();
2555        let repo = GitRepository::open(&repo_path).unwrap();
2556
2557        // Create a test branch
2558        create_commit(&repo_path, "Second commit", "test.txt");
2559        Command::new("git")
2560            .args(["checkout", "-b", "test-branch"])
2561            .current_dir(&repo_path)
2562            .output()
2563            .unwrap();
2564
2565        // Test checkout safety with clean repo
2566        let safety_result = repo.check_checkout_safety("main");
2567        assert!(safety_result.is_ok());
2568        assert!(safety_result.unwrap().is_none()); // Clean repo should return None
2569    }
2570
2571    #[test]
2572    fn test_checkout_safety_with_modified_files() {
2573        let (_temp_dir, repo_path) = create_test_repo();
2574        let repo = GitRepository::open(&repo_path).unwrap();
2575
2576        // Create a test branch
2577        Command::new("git")
2578            .args(["checkout", "-b", "test-branch"])
2579            .current_dir(&repo_path)
2580            .output()
2581            .unwrap();
2582
2583        // Modify a file to create uncommitted changes
2584        std::fs::write(repo_path.join("README.md"), "Modified content").unwrap();
2585
2586        // Test checkout safety with modified files
2587        let safety_result = repo.check_checkout_safety("main");
2588        assert!(safety_result.is_ok());
2589        let safety_info = safety_result.unwrap();
2590        assert!(safety_info.is_some());
2591
2592        let info = safety_info.unwrap();
2593        assert!(!info.modified_files.is_empty());
2594        assert!(info.modified_files.contains(&"README.md".to_string()));
2595    }
2596
2597    #[test]
2598    fn test_unsafe_checkout_methods() {
2599        let (_temp_dir, repo_path) = create_test_repo();
2600        let repo = GitRepository::open(&repo_path).unwrap();
2601
2602        // Create a test branch
2603        create_commit(&repo_path, "Second commit", "test.txt");
2604        Command::new("git")
2605            .args(["checkout", "-b", "test-branch"])
2606            .current_dir(&repo_path)
2607            .output()
2608            .unwrap();
2609
2610        // Modify a file to create uncommitted changes
2611        std::fs::write(repo_path.join("README.md"), "Modified content").unwrap();
2612
2613        // Test unsafe checkout methods bypass safety checks
2614        let _result = repo.checkout_branch_unsafe("master");
2615        // Note: This might still fail due to git2 restrictions, but shouldn't hit our safety code
2616        // The important thing is that it doesn't trigger our safety confirmation
2617
2618        // Test unsafe commit checkout
2619        let head_commit = repo.get_head_commit().unwrap();
2620        let commit_hash = head_commit.id().to_string();
2621        let _result = repo.checkout_commit_unsafe(&commit_hash);
2622        // Similar to above - testing that safety is bypassed
2623    }
2624
2625    #[test]
2626    fn test_get_modified_files() {
2627        let (_temp_dir, repo_path) = create_test_repo();
2628        let repo = GitRepository::open(&repo_path).unwrap();
2629
2630        // Initially should have no modified files
2631        let modified = repo.get_modified_files().unwrap();
2632        assert!(modified.is_empty());
2633
2634        // Modify a file
2635        std::fs::write(repo_path.join("README.md"), "Modified content").unwrap();
2636
2637        // Should now detect the modified file
2638        let modified = repo.get_modified_files().unwrap();
2639        assert_eq!(modified.len(), 1);
2640        assert!(modified.contains(&"README.md".to_string()));
2641    }
2642
2643    #[test]
2644    fn test_get_staged_files() {
2645        let (_temp_dir, repo_path) = create_test_repo();
2646        let repo = GitRepository::open(&repo_path).unwrap();
2647
2648        // Initially should have no staged files
2649        let staged = repo.get_staged_files().unwrap();
2650        assert!(staged.is_empty());
2651
2652        // Create and stage a new file
2653        std::fs::write(repo_path.join("staged.txt"), "Staged content").unwrap();
2654        Command::new("git")
2655            .args(["add", "staged.txt"])
2656            .current_dir(&repo_path)
2657            .output()
2658            .unwrap();
2659
2660        // Should now detect the staged file
2661        let staged = repo.get_staged_files().unwrap();
2662        assert_eq!(staged.len(), 1);
2663        assert!(staged.contains(&"staged.txt".to_string()));
2664    }
2665
2666    #[test]
2667    fn test_create_stash_fallback() {
2668        let (_temp_dir, repo_path) = create_test_repo();
2669        let repo = GitRepository::open(&repo_path).unwrap();
2670
2671        // Test stash creation - newer git versions allow empty stashes
2672        let result = repo.create_stash("test stash");
2673
2674        // Either succeeds (newer git with empty stash) or fails with helpful message
2675        match result {
2676            Ok(stash_id) => {
2677                // Modern git allows empty stashes, verify we got a stash ID
2678                assert!(!stash_id.is_empty());
2679                assert!(stash_id.contains("stash") || stash_id.len() >= 7); // SHA or stash@{n}
2680            }
2681            Err(error) => {
2682                // Older git should fail with helpful message
2683                let error_msg = error.to_string();
2684                assert!(
2685                    error_msg.contains("No local changes to save")
2686                        || error_msg.contains("git stash push")
2687                );
2688            }
2689        }
2690    }
2691
2692    #[test]
2693    fn test_delete_branch_unsafe() {
2694        let (_temp_dir, repo_path) = create_test_repo();
2695        let repo = GitRepository::open(&repo_path).unwrap();
2696
2697        // Create a test branch
2698        create_commit(&repo_path, "Second commit", "test.txt");
2699        Command::new("git")
2700            .args(["checkout", "-b", "test-branch"])
2701            .current_dir(&repo_path)
2702            .output()
2703            .unwrap();
2704
2705        // Add another commit to the test branch to make it different from master
2706        create_commit(&repo_path, "Branch-specific commit", "branch.txt");
2707
2708        // Go back to master
2709        Command::new("git")
2710            .args(["checkout", "master"])
2711            .current_dir(&repo_path)
2712            .output()
2713            .unwrap();
2714
2715        // Test unsafe delete bypasses safety checks
2716        // Note: This may still fail if the branch has unpushed commits, but it should bypass our safety confirmation
2717        let result = repo.delete_branch_unsafe("test-branch");
2718        // Even if it fails, the key is that it didn't prompt for user confirmation
2719        // So we just check that it attempted the operation without interactive prompts
2720        let _ = result; // Don't assert success since delete may fail for git reasons
2721    }
2722
2723    #[test]
2724    fn test_force_push_unsafe() {
2725        let (_temp_dir, repo_path) = create_test_repo();
2726        let repo = GitRepository::open(&repo_path).unwrap();
2727
2728        // Create a test branch
2729        create_commit(&repo_path, "Second commit", "test.txt");
2730        Command::new("git")
2731            .args(["checkout", "-b", "test-branch"])
2732            .current_dir(&repo_path)
2733            .output()
2734            .unwrap();
2735
2736        // Test unsafe force push bypasses safety checks
2737        // Note: This will likely fail due to no remote, but it tests the safety bypass
2738        let _result = repo.force_push_branch_unsafe("test-branch", "test-branch");
2739        // The key is that it doesn't trigger safety confirmation dialogs
2740    }
2741}