cascade_cli/git/
repository.rs

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