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 priority:
561        // 1. Cascade SSL config (highest priority)
562        // 2. Git config (fallback)
563        // 3. Default SSL verification (default)
564
565        let mut ssl_configured = false;
566
567        // First, check for Cascade SSL configuration
568        if let Some(ssl_config) = &self.ssl_config {
569            if ssl_config.accept_invalid_certs {
570                tracing::warn!("SSL certificate verification disabled via Cascade config (accept_invalid_certs=true)");
571                callbacks.certificate_check(|_cert, _host| {
572                    Ok(git2::CertificateCheckStatus::CertificateOk)
573                });
574                ssl_configured = true;
575            } else if let Some(ca_path) = &ssl_config.ca_bundle_path {
576                tracing::info!("Using custom CA bundle from Cascade config: {}", ca_path);
577                // Note: git2 doesn't directly support custom CA bundles in callbacks
578                // The system libgit2 should respect this if we set it in git config
579                // For now, we'll log it and let the system handle it
580            }
581        }
582
583        // If no Cascade SSL config, fall back to git config
584        if !ssl_configured {
585            if let Ok(config) = self.repo.config() {
586                // Check if SSL verification is disabled in git config
587                let ssl_verify = config.get_bool("http.sslVerify").unwrap_or(true);
588
589                if !ssl_verify {
590                    // If http.sslVerify is false, accept any certificate
591                    callbacks.certificate_check(|_cert, _host| {
592                        tracing::warn!("SSL certificate verification disabled via git config (http.sslVerify=false)");
593                        Ok(git2::CertificateCheckStatus::CertificateOk)
594                    });
595                } else {
596                    // Check for custom CA bundle
597                    if let Ok(ca_path) = config.get_string("http.sslCAInfo") {
598                        // Note: git2 doesn't directly support custom CA bundles in callbacks
599                        // The system libgit2 should respect the git config automatically
600                        tracing::info!("Using custom CA bundle from git config: {}", ca_path);
601                    }
602                }
603            }
604        }
605
606        Ok(callbacks)
607    }
608
609    /// Get the tree ID from the current index
610    fn get_index_tree(&self) -> Result<Oid> {
611        let mut index = self.repo.index().map_err(CascadeError::Git)?;
612
613        index.write_tree().map_err(CascadeError::Git)
614    }
615
616    /// Get repository status
617    pub fn get_status(&self) -> Result<git2::Statuses<'_>> {
618        self.repo.statuses(None).map_err(CascadeError::Git)
619    }
620
621    /// Get remote URL for a given remote name
622    pub fn get_remote_url(&self, name: &str) -> Result<String> {
623        let remote = self.repo.find_remote(name).map_err(CascadeError::Git)?;
624
625        let url = remote.url().ok_or_else(|| {
626            CascadeError::Git(git2::Error::from_str("Remote URL is not valid UTF-8"))
627        })?;
628
629        Ok(url.to_string())
630    }
631
632    /// Cherry-pick a commit onto the current branch
633    pub fn cherry_pick(&self, commit_hash: &str) -> Result<String> {
634        tracing::debug!("Cherry-picking commit {}", commit_hash);
635
636        let oid = Oid::from_str(commit_hash).map_err(CascadeError::Git)?;
637        let commit = self.repo.find_commit(oid).map_err(CascadeError::Git)?;
638
639        // Get the commit's tree
640        let commit_tree = commit.tree().map_err(CascadeError::Git)?;
641
642        // Get parent tree for merge base
643        let parent_commit = if commit.parent_count() > 0 {
644            commit.parent(0).map_err(CascadeError::Git)?
645        } else {
646            // Root commit - use empty tree
647            let empty_tree_oid = self.repo.treebuilder(None)?.write()?;
648            let empty_tree = self.repo.find_tree(empty_tree_oid)?;
649            let sig = self.get_signature()?;
650            return self
651                .repo
652                .commit(
653                    Some("HEAD"),
654                    &sig,
655                    &sig,
656                    commit.message().unwrap_or("Cherry-picked commit"),
657                    &empty_tree,
658                    &[],
659                )
660                .map(|oid| oid.to_string())
661                .map_err(CascadeError::Git);
662        };
663
664        let parent_tree = parent_commit.tree().map_err(CascadeError::Git)?;
665
666        // Get current HEAD tree for 3-way merge
667        let head_commit = self.get_head_commit()?;
668        let head_tree = head_commit.tree().map_err(CascadeError::Git)?;
669
670        // Perform 3-way merge
671        let mut index = self
672            .repo
673            .merge_trees(&parent_tree, &head_tree, &commit_tree, None)
674            .map_err(CascadeError::Git)?;
675
676        // Check for conflicts
677        if index.has_conflicts() {
678            return Err(CascadeError::branch(format!(
679                "Cherry-pick of {commit_hash} has conflicts that need manual resolution"
680            )));
681        }
682
683        // Write merged tree
684        let merged_tree_oid = index.write_tree_to(&self.repo).map_err(CascadeError::Git)?;
685        let merged_tree = self
686            .repo
687            .find_tree(merged_tree_oid)
688            .map_err(CascadeError::Git)?;
689
690        // Create new commit
691        let signature = self.get_signature()?;
692        let message = format!("Cherry-pick: {}", commit.message().unwrap_or(""));
693
694        let new_commit_oid = self
695            .repo
696            .commit(
697                Some("HEAD"),
698                &signature,
699                &signature,
700                &message,
701                &merged_tree,
702                &[&head_commit],
703            )
704            .map_err(CascadeError::Git)?;
705
706        tracing::info!("Cherry-picked {} -> {}", commit_hash, new_commit_oid);
707        Ok(new_commit_oid.to_string())
708    }
709
710    /// Check for merge conflicts in the index
711    pub fn has_conflicts(&self) -> Result<bool> {
712        let index = self.repo.index().map_err(CascadeError::Git)?;
713        Ok(index.has_conflicts())
714    }
715
716    /// Get list of conflicted files
717    pub fn get_conflicted_files(&self) -> Result<Vec<String>> {
718        let index = self.repo.index().map_err(CascadeError::Git)?;
719
720        let mut conflicts = Vec::new();
721
722        // Iterate through index conflicts
723        let conflict_iter = index.conflicts().map_err(CascadeError::Git)?;
724
725        for conflict in conflict_iter {
726            let conflict = conflict.map_err(CascadeError::Git)?;
727            if let Some(our) = conflict.our {
728                if let Ok(path) = std::str::from_utf8(&our.path) {
729                    conflicts.push(path.to_string());
730                }
731            } else if let Some(their) = conflict.their {
732                if let Ok(path) = std::str::from_utf8(&their.path) {
733                    conflicts.push(path.to_string());
734                }
735            }
736        }
737
738        Ok(conflicts)
739    }
740
741    /// Fetch from remote origin
742    pub fn fetch(&self) -> Result<()> {
743        tracing::info!("Fetching from origin");
744
745        let mut remote = self
746            .repo
747            .find_remote("origin")
748            .map_err(|e| CascadeError::branch(format!("No remote 'origin' found: {e}")))?;
749
750        // Configure callbacks with SSL settings from git config
751        let callbacks = self.configure_remote_callbacks()?;
752
753        // Fetch options with authentication and SSL config
754        let mut fetch_options = git2::FetchOptions::new();
755        fetch_options.remote_callbacks(callbacks);
756
757        // Fetch with authentication
758        remote
759            .fetch::<&str>(&[], Some(&mut fetch_options), None)
760            .map_err(CascadeError::Git)?;
761
762        tracing::debug!("Fetch completed successfully");
763        Ok(())
764    }
765
766    /// Pull changes from remote (fetch + merge)
767    pub fn pull(&self, branch: &str) -> Result<()> {
768        tracing::info!("Pulling branch: {}", branch);
769
770        // First fetch
771        self.fetch()?;
772
773        // Get remote tracking branch
774        let remote_branch_name = format!("origin/{branch}");
775        let remote_oid = self
776            .repo
777            .refname_to_id(&format!("refs/remotes/{remote_branch_name}"))
778            .map_err(|e| {
779                CascadeError::branch(format!("Remote branch {remote_branch_name} not found: {e}"))
780            })?;
781
782        let remote_commit = self
783            .repo
784            .find_commit(remote_oid)
785            .map_err(CascadeError::Git)?;
786
787        // Get current HEAD
788        let head_commit = self.get_head_commit()?;
789
790        // Check if we need to merge
791        if head_commit.id() == remote_commit.id() {
792            tracing::debug!("Already up to date");
793            return Ok(());
794        }
795
796        // Perform merge
797        let head_tree = head_commit.tree().map_err(CascadeError::Git)?;
798        let remote_tree = remote_commit.tree().map_err(CascadeError::Git)?;
799
800        // Find merge base
801        let merge_base_oid = self
802            .repo
803            .merge_base(head_commit.id(), remote_commit.id())
804            .map_err(CascadeError::Git)?;
805        let merge_base_commit = self
806            .repo
807            .find_commit(merge_base_oid)
808            .map_err(CascadeError::Git)?;
809        let merge_base_tree = merge_base_commit.tree().map_err(CascadeError::Git)?;
810
811        // 3-way merge
812        let mut index = self
813            .repo
814            .merge_trees(&merge_base_tree, &head_tree, &remote_tree, None)
815            .map_err(CascadeError::Git)?;
816
817        if index.has_conflicts() {
818            return Err(CascadeError::branch(
819                "Pull has conflicts that need manual resolution".to_string(),
820            ));
821        }
822
823        // Write merged tree and create merge commit
824        let merged_tree_oid = index.write_tree_to(&self.repo).map_err(CascadeError::Git)?;
825        let merged_tree = self
826            .repo
827            .find_tree(merged_tree_oid)
828            .map_err(CascadeError::Git)?;
829
830        let signature = self.get_signature()?;
831        let message = format!("Merge branch '{branch}' from origin");
832
833        self.repo
834            .commit(
835                Some("HEAD"),
836                &signature,
837                &signature,
838                &message,
839                &merged_tree,
840                &[&head_commit, &remote_commit],
841            )
842            .map_err(CascadeError::Git)?;
843
844        tracing::info!("Pull completed successfully");
845        Ok(())
846    }
847
848    /// Push current branch to remote
849    pub fn push(&self, branch: &str) -> Result<()> {
850        tracing::info!("Pushing branch: {}", branch);
851
852        let mut remote = self
853            .repo
854            .find_remote("origin")
855            .map_err(|e| CascadeError::branch(format!("No remote 'origin' found: {e}")))?;
856
857        let refspec = format!("refs/heads/{branch}:refs/heads/{branch}");
858
859        // Configure callbacks with SSL settings from git config
860        let callbacks = self.configure_remote_callbacks()?;
861
862        // Push options with authentication and SSL config
863        let mut push_options = git2::PushOptions::new();
864        push_options.remote_callbacks(callbacks);
865
866        remote
867            .push(&[&refspec], Some(&mut push_options))
868            .map_err(CascadeError::Git)?;
869
870        tracing::info!("Push completed successfully");
871        Ok(())
872    }
873
874    /// Delete a local branch
875    pub fn delete_branch(&self, name: &str) -> Result<()> {
876        self.delete_branch_with_options(name, false)
877    }
878
879    /// Delete a local branch with force option to bypass safety checks
880    pub fn delete_branch_unsafe(&self, name: &str) -> Result<()> {
881        self.delete_branch_with_options(name, true)
882    }
883
884    /// Internal branch deletion implementation with safety options
885    fn delete_branch_with_options(&self, name: &str, force_unsafe: bool) -> Result<()> {
886        info!("Attempting to delete branch: {}", name);
887
888        // Enhanced safety check: Detect unpushed commits before deletion
889        if !force_unsafe {
890            let safety_result = self.check_branch_deletion_safety(name)?;
891            if let Some(safety_info) = safety_result {
892                // Branch has unpushed commits, get user confirmation
893                self.handle_branch_deletion_confirmation(name, &safety_info)?;
894            }
895        }
896
897        let mut branch = self
898            .repo
899            .find_branch(name, git2::BranchType::Local)
900            .map_err(|e| CascadeError::branch(format!("Could not find branch '{name}': {e}")))?;
901
902        branch
903            .delete()
904            .map_err(|e| CascadeError::branch(format!("Could not delete branch '{name}': {e}")))?;
905
906        info!("Successfully deleted branch '{}'", name);
907        Ok(())
908    }
909
910    /// Get commits between two references
911    pub fn get_commits_between(&self, from: &str, to: &str) -> Result<Vec<git2::Commit<'_>>> {
912        let from_oid = self
913            .repo
914            .refname_to_id(&format!("refs/heads/{from}"))
915            .or_else(|_| Oid::from_str(from))
916            .map_err(|e| CascadeError::branch(format!("Invalid from reference '{from}': {e}")))?;
917
918        let to_oid = self
919            .repo
920            .refname_to_id(&format!("refs/heads/{to}"))
921            .or_else(|_| Oid::from_str(to))
922            .map_err(|e| CascadeError::branch(format!("Invalid to reference '{to}': {e}")))?;
923
924        let mut revwalk = self.repo.revwalk().map_err(CascadeError::Git)?;
925
926        revwalk.push(to_oid).map_err(CascadeError::Git)?;
927        revwalk.hide(from_oid).map_err(CascadeError::Git)?;
928
929        let mut commits = Vec::new();
930        for oid in revwalk {
931            let oid = oid.map_err(CascadeError::Git)?;
932            let commit = self.repo.find_commit(oid).map_err(CascadeError::Git)?;
933            commits.push(commit);
934        }
935
936        Ok(commits)
937    }
938
939    /// Force push one branch's content to another branch name
940    /// This is used to preserve PR history while updating branch contents after rebase
941    pub fn force_push_branch(&self, target_branch: &str, source_branch: &str) -> Result<()> {
942        self.force_push_branch_with_options(target_branch, source_branch, false)
943    }
944
945    /// Force push with explicit force flag to bypass safety checks
946    pub fn force_push_branch_unsafe(&self, target_branch: &str, source_branch: &str) -> Result<()> {
947        self.force_push_branch_with_options(target_branch, source_branch, true)
948    }
949
950    /// Internal force push implementation with safety options
951    fn force_push_branch_with_options(
952        &self,
953        target_branch: &str,
954        source_branch: &str,
955        force_unsafe: bool,
956    ) -> Result<()> {
957        info!(
958            "Force pushing {} content to {} to preserve PR history",
959            source_branch, target_branch
960        );
961
962        // Enhanced safety check: Detect potential data loss and get user confirmation
963        if !force_unsafe {
964            let safety_result = self.check_force_push_safety_enhanced(target_branch)?;
965            if let Some(backup_info) = safety_result {
966                // Create backup branch before force push
967                self.create_backup_branch(target_branch, &backup_info.remote_commit_id)?;
968                info!(
969                    "✅ Created backup branch: {}",
970                    backup_info.backup_branch_name
971                );
972            }
973        }
974
975        // First, ensure we have the latest changes for the source branch
976        let source_ref = self
977            .repo
978            .find_reference(&format!("refs/heads/{source_branch}"))
979            .map_err(|e| {
980                CascadeError::config(format!("Failed to find source branch {source_branch}: {e}"))
981            })?;
982        let source_commit = source_ref.peel_to_commit().map_err(|e| {
983            CascadeError::config(format!(
984                "Failed to get commit for source branch {source_branch}: {e}"
985            ))
986        })?;
987
988        // Update the target branch to point to the source commit
989        let mut target_ref = self
990            .repo
991            .find_reference(&format!("refs/heads/{target_branch}"))
992            .map_err(|e| {
993                CascadeError::config(format!("Failed to find target branch {target_branch}: {e}"))
994            })?;
995
996        target_ref
997            .set_target(source_commit.id(), "Force push from rebase")
998            .map_err(|e| {
999                CascadeError::config(format!(
1000                    "Failed to update target branch {target_branch}: {e}"
1001                ))
1002            })?;
1003
1004        // Force push to remote
1005        let mut remote = self
1006            .repo
1007            .find_remote("origin")
1008            .map_err(|e| CascadeError::config(format!("Failed to find origin remote: {e}")))?;
1009
1010        let refspec = format!("+refs/heads/{target_branch}:refs/heads/{target_branch}");
1011
1012        // Configure callbacks with SSL settings from git config
1013        let callbacks = self.configure_remote_callbacks()?;
1014
1015        // Push options for force push with SSL config
1016        let mut push_options = git2::PushOptions::new();
1017        push_options.remote_callbacks(callbacks);
1018
1019        remote
1020            .push(&[&refspec], Some(&mut push_options))
1021            .map_err(|e| {
1022                CascadeError::config(format!("Failed to force push {target_branch}: {e}"))
1023            })?;
1024
1025        info!(
1026            "✅ Successfully force pushed {} to preserve PR history",
1027            target_branch
1028        );
1029        Ok(())
1030    }
1031
1032    /// Enhanced safety check for force push operations with user confirmation
1033    /// Returns backup info if data would be lost and user confirms
1034    fn check_force_push_safety_enhanced(
1035        &self,
1036        target_branch: &str,
1037    ) -> Result<Option<ForceBackupInfo>> {
1038        // First fetch latest remote changes to ensure we have up-to-date information
1039        match self.fetch() {
1040            Ok(_) => {}
1041            Err(e) => {
1042                // If fetch fails, warn but don't block the operation
1043                warn!("Could not fetch latest changes for safety check: {}", e);
1044            }
1045        }
1046
1047        // Check if there are commits on the remote that would be lost
1048        let remote_ref = format!("refs/remotes/origin/{target_branch}");
1049        let local_ref = format!("refs/heads/{target_branch}");
1050
1051        // Try to find both local and remote references
1052        let local_commit = match self.repo.find_reference(&local_ref) {
1053            Ok(reference) => reference.peel_to_commit().ok(),
1054            Err(_) => None,
1055        };
1056
1057        let remote_commit = match self.repo.find_reference(&remote_ref) {
1058            Ok(reference) => reference.peel_to_commit().ok(),
1059            Err(_) => None,
1060        };
1061
1062        // If we have both commits, check for divergence
1063        if let (Some(local), Some(remote)) = (local_commit, remote_commit) {
1064            if local.id() != remote.id() {
1065                // Check if the remote has commits that the local doesn't have
1066                let merge_base_oid = self
1067                    .repo
1068                    .merge_base(local.id(), remote.id())
1069                    .map_err(|e| CascadeError::config(format!("Failed to find merge base: {e}")))?;
1070
1071                // If merge base != remote commit, remote has commits that would be lost
1072                if merge_base_oid != remote.id() {
1073                    let commits_to_lose = self.count_commits_between(
1074                        &merge_base_oid.to_string(),
1075                        &remote.id().to_string(),
1076                    )?;
1077
1078                    // Create backup branch name with timestamp
1079                    let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
1080                    let backup_branch_name = format!("{target_branch}_backup_{timestamp}");
1081
1082                    warn!(
1083                        "⚠️  Force push to '{}' would overwrite {} commits on remote",
1084                        target_branch, commits_to_lose
1085                    );
1086
1087                    // Check if we're in a non-interactive environment (CI/testing)
1088                    if std::env::var("CI").is_ok() || std::env::var("FORCE_PUSH_NO_CONFIRM").is_ok()
1089                    {
1090                        info!(
1091                            "Non-interactive environment detected, proceeding with backup creation"
1092                        );
1093                        return Ok(Some(ForceBackupInfo {
1094                            backup_branch_name,
1095                            remote_commit_id: remote.id().to_string(),
1096                            commits_that_would_be_lost: commits_to_lose,
1097                        }));
1098                    }
1099
1100                    // Interactive confirmation
1101                    println!("\n⚠️  FORCE PUSH WARNING ⚠️");
1102                    println!("Force push to '{target_branch}' would overwrite {commits_to_lose} commits on remote:");
1103
1104                    // Show the commits that would be lost
1105                    match self
1106                        .get_commits_between(&merge_base_oid.to_string(), &remote.id().to_string())
1107                    {
1108                        Ok(commits) => {
1109                            println!("\nCommits that would be lost:");
1110                            for (i, commit) in commits.iter().take(5).enumerate() {
1111                                let short_hash = &commit.id().to_string()[..8];
1112                                let summary = commit.summary().unwrap_or("<no message>");
1113                                println!("  {}. {} - {}", i + 1, short_hash, summary);
1114                            }
1115                            if commits.len() > 5 {
1116                                println!("  ... and {} more commits", commits.len() - 5);
1117                            }
1118                        }
1119                        Err(_) => {
1120                            println!("  (Unable to retrieve commit details)");
1121                        }
1122                    }
1123
1124                    println!("\nA backup branch '{backup_branch_name}' will be created before proceeding.");
1125
1126                    let confirmed = Confirm::with_theme(&ColorfulTheme::default())
1127                        .with_prompt("Do you want to proceed with the force push?")
1128                        .default(false)
1129                        .interact()
1130                        .map_err(|e| {
1131                            CascadeError::config(format!("Failed to get user confirmation: {e}"))
1132                        })?;
1133
1134                    if !confirmed {
1135                        return Err(CascadeError::config(
1136                            "Force push cancelled by user. Use --force to bypass this check."
1137                                .to_string(),
1138                        ));
1139                    }
1140
1141                    return Ok(Some(ForceBackupInfo {
1142                        backup_branch_name,
1143                        remote_commit_id: remote.id().to_string(),
1144                        commits_that_would_be_lost: commits_to_lose,
1145                    }));
1146                }
1147            }
1148        }
1149
1150        Ok(None)
1151    }
1152
1153    /// Create a backup branch pointing to the remote commit that would be lost
1154    fn create_backup_branch(&self, original_branch: &str, remote_commit_id: &str) -> Result<()> {
1155        let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
1156        let backup_branch_name = format!("{original_branch}_backup_{timestamp}");
1157
1158        // Parse the commit ID
1159        let commit_oid = Oid::from_str(remote_commit_id).map_err(|e| {
1160            CascadeError::config(format!("Invalid commit ID {remote_commit_id}: {e}"))
1161        })?;
1162
1163        // Find the commit
1164        let commit = self.repo.find_commit(commit_oid).map_err(|e| {
1165            CascadeError::config(format!("Failed to find commit {remote_commit_id}: {e}"))
1166        })?;
1167
1168        // Create the backup branch
1169        self.repo
1170            .branch(&backup_branch_name, &commit, false)
1171            .map_err(|e| {
1172                CascadeError::config(format!(
1173                    "Failed to create backup branch {backup_branch_name}: {e}"
1174                ))
1175            })?;
1176
1177        info!(
1178            "✅ Created backup branch '{}' pointing to {}",
1179            backup_branch_name,
1180            &remote_commit_id[..8]
1181        );
1182        Ok(())
1183    }
1184
1185    /// Check if branch deletion is safe by detecting unpushed commits
1186    /// Returns safety info if there are concerns that need user attention
1187    fn check_branch_deletion_safety(
1188        &self,
1189        branch_name: &str,
1190    ) -> Result<Option<BranchDeletionSafety>> {
1191        // First, try to fetch latest remote changes
1192        match self.fetch() {
1193            Ok(_) => {}
1194            Err(e) => {
1195                warn!(
1196                    "Could not fetch latest changes for branch deletion safety check: {}",
1197                    e
1198                );
1199            }
1200        }
1201
1202        // Find the branch
1203        let branch = self
1204            .repo
1205            .find_branch(branch_name, git2::BranchType::Local)
1206            .map_err(|e| {
1207                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
1208            })?;
1209
1210        let _branch_commit = branch.get().peel_to_commit().map_err(|e| {
1211            CascadeError::branch(format!(
1212                "Could not get commit for branch '{branch_name}': {e}"
1213            ))
1214        })?;
1215
1216        // Determine the main branch (try common names)
1217        let main_branch_name = self.detect_main_branch()?;
1218
1219        // Check if branch is merged to main
1220        let is_merged_to_main = self.is_branch_merged_to_main(branch_name, &main_branch_name)?;
1221
1222        // Find the upstream/remote tracking branch
1223        let remote_tracking_branch = self.get_remote_tracking_branch(branch_name);
1224
1225        let mut unpushed_commits = Vec::new();
1226
1227        // Check for unpushed commits compared to remote tracking branch
1228        if let Some(ref remote_branch) = remote_tracking_branch {
1229            match self.get_commits_between(remote_branch, branch_name) {
1230                Ok(commits) => {
1231                    unpushed_commits = commits.iter().map(|c| c.id().to_string()).collect();
1232                }
1233                Err(_) => {
1234                    // If we can't compare with remote, check against main branch
1235                    if !is_merged_to_main {
1236                        if let Ok(commits) =
1237                            self.get_commits_between(&main_branch_name, branch_name)
1238                        {
1239                            unpushed_commits = commits.iter().map(|c| c.id().to_string()).collect();
1240                        }
1241                    }
1242                }
1243            }
1244        } else if !is_merged_to_main {
1245            // No remote tracking branch, check against main
1246            if let Ok(commits) = self.get_commits_between(&main_branch_name, branch_name) {
1247                unpushed_commits = commits.iter().map(|c| c.id().to_string()).collect();
1248            }
1249        }
1250
1251        // If there are concerns, return safety info
1252        if !unpushed_commits.is_empty() || (!is_merged_to_main && remote_tracking_branch.is_none())
1253        {
1254            Ok(Some(BranchDeletionSafety {
1255                unpushed_commits,
1256                remote_tracking_branch,
1257                is_merged_to_main,
1258                main_branch_name,
1259            }))
1260        } else {
1261            Ok(None)
1262        }
1263    }
1264
1265    /// Handle user confirmation for branch deletion with safety concerns
1266    fn handle_branch_deletion_confirmation(
1267        &self,
1268        branch_name: &str,
1269        safety_info: &BranchDeletionSafety,
1270    ) -> Result<()> {
1271        // Check if we're in a non-interactive environment
1272        if std::env::var("CI").is_ok() || std::env::var("BRANCH_DELETE_NO_CONFIRM").is_ok() {
1273            return Err(CascadeError::branch(
1274                format!(
1275                    "Branch '{branch_name}' has {} unpushed commits and cannot be deleted in non-interactive mode. Use --force to override.",
1276                    safety_info.unpushed_commits.len()
1277                )
1278            ));
1279        }
1280
1281        // Interactive warning and confirmation
1282        println!("\n⚠️  BRANCH DELETION WARNING ⚠️");
1283        println!("Branch '{branch_name}' has potential issues:");
1284
1285        if !safety_info.unpushed_commits.is_empty() {
1286            println!(
1287                "\n🔍 Unpushed commits ({} total):",
1288                safety_info.unpushed_commits.len()
1289            );
1290
1291            // Show details of unpushed commits
1292            for (i, commit_id) in safety_info.unpushed_commits.iter().take(5).enumerate() {
1293                if let Ok(commit) = self.repo.find_commit(Oid::from_str(commit_id).unwrap()) {
1294                    let short_hash = &commit_id[..8];
1295                    let summary = commit.summary().unwrap_or("<no message>");
1296                    println!("  {}. {} - {}", i + 1, short_hash, summary);
1297                }
1298            }
1299
1300            if safety_info.unpushed_commits.len() > 5 {
1301                println!(
1302                    "  ... and {} more commits",
1303                    safety_info.unpushed_commits.len() - 5
1304                );
1305            }
1306        }
1307
1308        if !safety_info.is_merged_to_main {
1309            println!("\n📋 Branch status:");
1310            println!("  • Not merged to '{}'", safety_info.main_branch_name);
1311            if let Some(ref remote) = safety_info.remote_tracking_branch {
1312                println!("  • Remote tracking branch: {remote}");
1313            } else {
1314                println!("  • No remote tracking branch");
1315            }
1316        }
1317
1318        println!("\n💡 Safer alternatives:");
1319        if !safety_info.unpushed_commits.is_empty() {
1320            if let Some(ref _remote) = safety_info.remote_tracking_branch {
1321                println!("  • Push commits first: git push origin {branch_name}");
1322            } else {
1323                println!("  • Create and push to remote: git push -u origin {branch_name}");
1324            }
1325        }
1326        if !safety_info.is_merged_to_main {
1327            println!(
1328                "  • Merge to {} first: git checkout {} && git merge {branch_name}",
1329                safety_info.main_branch_name, safety_info.main_branch_name
1330            );
1331        }
1332
1333        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
1334            .with_prompt("Do you want to proceed with deleting this branch?")
1335            .default(false)
1336            .interact()
1337            .map_err(|e| CascadeError::branch(format!("Failed to get user confirmation: {e}")))?;
1338
1339        if !confirmed {
1340            return Err(CascadeError::branch(
1341                "Branch deletion cancelled by user. Use --force to bypass this check.".to_string(),
1342            ));
1343        }
1344
1345        Ok(())
1346    }
1347
1348    /// Detect the main branch name (main, master, develop)
1349    fn detect_main_branch(&self) -> Result<String> {
1350        let main_candidates = ["main", "master", "develop", "trunk"];
1351
1352        for candidate in &main_candidates {
1353            if self
1354                .repo
1355                .find_branch(candidate, git2::BranchType::Local)
1356                .is_ok()
1357            {
1358                return Ok(candidate.to_string());
1359            }
1360        }
1361
1362        // Fallback to HEAD's target if it's a symbolic reference
1363        if let Ok(head) = self.repo.head() {
1364            if let Some(name) = head.shorthand() {
1365                return Ok(name.to_string());
1366            }
1367        }
1368
1369        // Final fallback
1370        Ok("main".to_string())
1371    }
1372
1373    /// Check if a branch is merged to the main branch
1374    fn is_branch_merged_to_main(&self, branch_name: &str, main_branch: &str) -> Result<bool> {
1375        // Get the commits between main and the branch
1376        match self.get_commits_between(main_branch, branch_name) {
1377            Ok(commits) => Ok(commits.is_empty()),
1378            Err(_) => {
1379                // If we can't determine, assume not merged for safety
1380                Ok(false)
1381            }
1382        }
1383    }
1384
1385    /// Get the remote tracking branch for a local branch
1386    fn get_remote_tracking_branch(&self, branch_name: &str) -> Option<String> {
1387        // Try common remote tracking branch patterns
1388        let remote_candidates = [
1389            format!("origin/{branch_name}"),
1390            format!("remotes/origin/{branch_name}"),
1391        ];
1392
1393        for candidate in &remote_candidates {
1394            if self
1395                .repo
1396                .find_reference(&format!(
1397                    "refs/remotes/{}",
1398                    candidate.replace("remotes/", "")
1399                ))
1400                .is_ok()
1401            {
1402                return Some(candidate.clone());
1403            }
1404        }
1405
1406        None
1407    }
1408
1409    /// Check if checkout operation is safe
1410    fn check_checkout_safety(&self, _target: &str) -> Result<Option<CheckoutSafety>> {
1411        // Check if there are uncommitted changes
1412        let is_dirty = self.is_dirty()?;
1413        if !is_dirty {
1414            // No uncommitted changes, checkout is safe
1415            return Ok(None);
1416        }
1417
1418        // Get current branch for context
1419        let current_branch = self.get_current_branch().ok();
1420
1421        // Get detailed information about uncommitted changes
1422        let modified_files = self.get_modified_files()?;
1423        let staged_files = self.get_staged_files()?;
1424        let untracked_files = self.get_untracked_files()?;
1425
1426        let has_uncommitted_changes = !modified_files.is_empty() || !staged_files.is_empty();
1427
1428        if has_uncommitted_changes || !untracked_files.is_empty() {
1429            return Ok(Some(CheckoutSafety {
1430                has_uncommitted_changes,
1431                modified_files,
1432                staged_files,
1433                untracked_files,
1434                stash_created: None,
1435                current_branch,
1436            }));
1437        }
1438
1439        Ok(None)
1440    }
1441
1442    /// Handle user confirmation for checkout operations with uncommitted changes
1443    fn handle_checkout_confirmation(
1444        &self,
1445        target: &str,
1446        safety_info: &CheckoutSafety,
1447    ) -> Result<()> {
1448        // Check if we're in a non-interactive environment FIRST (before any output)
1449        let is_ci = std::env::var("CI").is_ok();
1450        let no_confirm = std::env::var("CHECKOUT_NO_CONFIRM").is_ok();
1451        let is_non_interactive = is_ci || no_confirm;
1452
1453        if is_non_interactive {
1454            return Err(CascadeError::branch(
1455                format!(
1456                    "Cannot checkout '{target}' with uncommitted changes in non-interactive mode. Commit your changes or use stash first."
1457                )
1458            ));
1459        }
1460
1461        // Interactive warning and confirmation
1462        println!("\n⚠️  CHECKOUT WARNING ⚠️");
1463        println!("You have uncommitted changes that could be lost:");
1464
1465        if !safety_info.modified_files.is_empty() {
1466            println!(
1467                "\n📝 Modified files ({}):",
1468                safety_info.modified_files.len()
1469            );
1470            for file in safety_info.modified_files.iter().take(10) {
1471                println!("   - {file}");
1472            }
1473            if safety_info.modified_files.len() > 10 {
1474                println!("   ... and {} more", safety_info.modified_files.len() - 10);
1475            }
1476        }
1477
1478        if !safety_info.staged_files.is_empty() {
1479            println!("\n📁 Staged files ({}):", safety_info.staged_files.len());
1480            for file in safety_info.staged_files.iter().take(10) {
1481                println!("   - {file}");
1482            }
1483            if safety_info.staged_files.len() > 10 {
1484                println!("   ... and {} more", safety_info.staged_files.len() - 10);
1485            }
1486        }
1487
1488        if !safety_info.untracked_files.is_empty() {
1489            println!(
1490                "\n❓ Untracked files ({}):",
1491                safety_info.untracked_files.len()
1492            );
1493            for file in safety_info.untracked_files.iter().take(5) {
1494                println!("   - {file}");
1495            }
1496            if safety_info.untracked_files.len() > 5 {
1497                println!("   ... and {} more", safety_info.untracked_files.len() - 5);
1498            }
1499        }
1500
1501        println!("\n🔄 Options:");
1502        println!("1. Stash changes and checkout (recommended)");
1503        println!("2. Force checkout (WILL LOSE UNCOMMITTED CHANGES)");
1504        println!("3. Cancel checkout");
1505
1506        let confirmation = Confirm::with_theme(&ColorfulTheme::default())
1507            .with_prompt("Would you like to stash your changes and proceed with checkout?")
1508            .interact()
1509            .map_err(|e| CascadeError::branch(format!("Could not get user confirmation: {e}")))?;
1510
1511        if confirmation {
1512            // Create stash before checkout
1513            let stash_message = format!(
1514                "Auto-stash before checkout to {} at {}",
1515                target,
1516                chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
1517            );
1518
1519            match self.create_stash(&stash_message) {
1520                Ok(stash_oid) => {
1521                    println!("✅ Created stash: {stash_message} ({stash_oid})");
1522                    println!("💡 You can restore with: git stash pop");
1523                }
1524                Err(e) => {
1525                    println!("❌ Failed to create stash: {e}");
1526
1527                    let force_confirm = Confirm::with_theme(&ColorfulTheme::default())
1528                        .with_prompt("Stash failed. Force checkout anyway? (WILL LOSE CHANGES)")
1529                        .interact()
1530                        .map_err(|e| {
1531                            CascadeError::branch(format!("Could not get confirmation: {e}"))
1532                        })?;
1533
1534                    if !force_confirm {
1535                        return Err(CascadeError::branch(
1536                            "Checkout cancelled by user".to_string(),
1537                        ));
1538                    }
1539                }
1540            }
1541        } else {
1542            return Err(CascadeError::branch(
1543                "Checkout cancelled by user".to_string(),
1544            ));
1545        }
1546
1547        Ok(())
1548    }
1549
1550    /// Create a stash with uncommitted changes
1551    fn create_stash(&self, message: &str) -> Result<String> {
1552        // For now, we'll use a different approach that doesn't require mutable access
1553        // This is a simplified version that recommends manual stashing
1554
1555        warn!("Automatic stashing not yet implemented - please stash manually");
1556        Err(CascadeError::branch(format!(
1557            "Please manually stash your changes first: git stash push -m \"{message}\""
1558        )))
1559    }
1560
1561    /// Get modified files in working directory
1562    fn get_modified_files(&self) -> Result<Vec<String>> {
1563        let mut opts = git2::StatusOptions::new();
1564        opts.include_untracked(false).include_ignored(false);
1565
1566        let statuses = self
1567            .repo
1568            .statuses(Some(&mut opts))
1569            .map_err(|e| CascadeError::branch(format!("Could not get repository status: {e}")))?;
1570
1571        let mut modified_files = Vec::new();
1572        for status in statuses.iter() {
1573            let flags = status.status();
1574            if flags.contains(git2::Status::WT_MODIFIED) || flags.contains(git2::Status::WT_DELETED)
1575            {
1576                if let Some(path) = status.path() {
1577                    modified_files.push(path.to_string());
1578                }
1579            }
1580        }
1581
1582        Ok(modified_files)
1583    }
1584
1585    /// Get staged files in index
1586    fn get_staged_files(&self) -> Result<Vec<String>> {
1587        let mut opts = git2::StatusOptions::new();
1588        opts.include_untracked(false).include_ignored(false);
1589
1590        let statuses = self
1591            .repo
1592            .statuses(Some(&mut opts))
1593            .map_err(|e| CascadeError::branch(format!("Could not get repository status: {e}")))?;
1594
1595        let mut staged_files = Vec::new();
1596        for status in statuses.iter() {
1597            let flags = status.status();
1598            if flags.contains(git2::Status::INDEX_MODIFIED)
1599                || flags.contains(git2::Status::INDEX_NEW)
1600                || flags.contains(git2::Status::INDEX_DELETED)
1601            {
1602                if let Some(path) = status.path() {
1603                    staged_files.push(path.to_string());
1604                }
1605            }
1606        }
1607
1608        Ok(staged_files)
1609    }
1610
1611    /// Count commits between two references
1612    fn count_commits_between(&self, from: &str, to: &str) -> Result<usize> {
1613        let commits = self.get_commits_between(from, to)?;
1614        Ok(commits.len())
1615    }
1616
1617    /// Resolve a reference (branch name, tag, or commit hash) to a commit
1618    pub fn resolve_reference(&self, reference: &str) -> Result<git2::Commit<'_>> {
1619        // Try to parse as commit hash first
1620        if let Ok(oid) = Oid::from_str(reference) {
1621            if let Ok(commit) = self.repo.find_commit(oid) {
1622                return Ok(commit);
1623            }
1624        }
1625
1626        // Try to resolve as a reference (branch, tag, etc.)
1627        let obj = self.repo.revparse_single(reference).map_err(|e| {
1628            CascadeError::branch(format!("Could not resolve reference '{reference}': {e}"))
1629        })?;
1630
1631        obj.peel_to_commit().map_err(|e| {
1632            CascadeError::branch(format!(
1633                "Reference '{reference}' does not point to a commit: {e}"
1634            ))
1635        })
1636    }
1637
1638    /// Reset HEAD to a specific reference (soft reset)
1639    pub fn reset_soft(&self, target_ref: &str) -> Result<()> {
1640        let target_commit = self.resolve_reference(target_ref)?;
1641
1642        self.repo
1643            .reset(target_commit.as_object(), git2::ResetType::Soft, None)
1644            .map_err(CascadeError::Git)?;
1645
1646        Ok(())
1647    }
1648
1649    /// Find which branch contains a specific commit
1650    pub fn find_branch_containing_commit(&self, commit_hash: &str) -> Result<String> {
1651        let oid = Oid::from_str(commit_hash).map_err(|e| {
1652            CascadeError::branch(format!("Invalid commit hash '{commit_hash}': {e}"))
1653        })?;
1654
1655        // Get all local branches
1656        let branches = self
1657            .repo
1658            .branches(Some(git2::BranchType::Local))
1659            .map_err(CascadeError::Git)?;
1660
1661        for branch_result in branches {
1662            let (branch, _) = branch_result.map_err(CascadeError::Git)?;
1663
1664            if let Some(branch_name) = branch.name().map_err(CascadeError::Git)? {
1665                // Check if this branch contains the commit
1666                if let Ok(branch_head) = branch.get().peel_to_commit() {
1667                    // Walk the commit history from this branch's HEAD
1668                    let mut revwalk = self.repo.revwalk().map_err(CascadeError::Git)?;
1669                    revwalk.push(branch_head.id()).map_err(CascadeError::Git)?;
1670
1671                    for commit_oid in revwalk {
1672                        let commit_oid = commit_oid.map_err(CascadeError::Git)?;
1673                        if commit_oid == oid {
1674                            return Ok(branch_name.to_string());
1675                        }
1676                    }
1677                }
1678            }
1679        }
1680
1681        // If not found in any branch, might be on current HEAD
1682        Err(CascadeError::branch(format!(
1683            "Commit {commit_hash} not found in any local branch"
1684        )))
1685    }
1686
1687    // Async wrappers for potentially blocking operations
1688
1689    /// Fetch from remote origin (async)
1690    pub async fn fetch_async(&self) -> Result<()> {
1691        let repo_path = self.path.clone();
1692        crate::utils::async_ops::run_git_operation(move || {
1693            let repo = GitRepository::open(&repo_path)?;
1694            repo.fetch()
1695        })
1696        .await
1697    }
1698
1699    /// Pull changes from remote (async)
1700    pub async fn pull_async(&self, branch: &str) -> Result<()> {
1701        let repo_path = self.path.clone();
1702        let branch_name = branch.to_string();
1703        crate::utils::async_ops::run_git_operation(move || {
1704            let repo = GitRepository::open(&repo_path)?;
1705            repo.pull(&branch_name)
1706        })
1707        .await
1708    }
1709
1710    /// Push branch to remote (async)
1711    pub async fn push_branch_async(&self, branch_name: &str) -> Result<()> {
1712        let repo_path = self.path.clone();
1713        let branch = branch_name.to_string();
1714        crate::utils::async_ops::run_git_operation(move || {
1715            let repo = GitRepository::open(&repo_path)?;
1716            repo.push(&branch)
1717        })
1718        .await
1719    }
1720
1721    /// Cherry-pick commit (async)
1722    pub async fn cherry_pick_commit_async(&self, commit_hash: &str) -> Result<String> {
1723        let repo_path = self.path.clone();
1724        let hash = commit_hash.to_string();
1725        crate::utils::async_ops::run_git_operation(move || {
1726            let repo = GitRepository::open(&repo_path)?;
1727            repo.cherry_pick(&hash)
1728        })
1729        .await
1730    }
1731
1732    /// Get commit hashes between two refs (async)
1733    pub async fn get_commit_hashes_between_async(
1734        &self,
1735        from: &str,
1736        to: &str,
1737    ) -> Result<Vec<String>> {
1738        let repo_path = self.path.clone();
1739        let from_str = from.to_string();
1740        let to_str = to.to_string();
1741        crate::utils::async_ops::run_git_operation(move || {
1742            let repo = GitRepository::open(&repo_path)?;
1743            let commits = repo.get_commits_between(&from_str, &to_str)?;
1744            Ok(commits.into_iter().map(|c| c.id().to_string()).collect())
1745        })
1746        .await
1747    }
1748
1749    /// Reset a branch to point to a specific commit
1750    pub fn reset_branch_to_commit(&self, branch_name: &str, commit_hash: &str) -> Result<()> {
1751        info!(
1752            "Resetting branch '{}' to commit {}",
1753            branch_name,
1754            &commit_hash[..8]
1755        );
1756
1757        // Find the target commit
1758        let target_oid = git2::Oid::from_str(commit_hash).map_err(|e| {
1759            CascadeError::branch(format!("Invalid commit hash '{commit_hash}': {e}"))
1760        })?;
1761
1762        let _target_commit = self.repo.find_commit(target_oid).map_err(|e| {
1763            CascadeError::branch(format!("Could not find commit '{commit_hash}': {e}"))
1764        })?;
1765
1766        // Find the branch
1767        let _branch = self
1768            .repo
1769            .find_branch(branch_name, git2::BranchType::Local)
1770            .map_err(|e| {
1771                CascadeError::branch(format!("Could not find branch '{branch_name}': {e}"))
1772            })?;
1773
1774        // Update the branch reference to point to the target commit
1775        let branch_ref_name = format!("refs/heads/{branch_name}");
1776        self.repo
1777            .reference(
1778                &branch_ref_name,
1779                target_oid,
1780                true,
1781                &format!("Reset {branch_name} to {commit_hash}"),
1782            )
1783            .map_err(|e| {
1784                CascadeError::branch(format!(
1785                    "Could not reset branch '{branch_name}' to commit '{commit_hash}': {e}"
1786                ))
1787            })?;
1788
1789        tracing::info!(
1790            "Successfully reset branch '{}' to commit {}",
1791            branch_name,
1792            &commit_hash[..8]
1793        );
1794        Ok(())
1795    }
1796}
1797
1798#[cfg(test)]
1799mod tests {
1800    use super::*;
1801    use std::process::Command;
1802    use tempfile::TempDir;
1803
1804    fn create_test_repo() -> (TempDir, PathBuf) {
1805        let temp_dir = TempDir::new().unwrap();
1806        let repo_path = temp_dir.path().to_path_buf();
1807
1808        // Initialize git repository
1809        Command::new("git")
1810            .args(["init"])
1811            .current_dir(&repo_path)
1812            .output()
1813            .unwrap();
1814        Command::new("git")
1815            .args(["config", "user.name", "Test"])
1816            .current_dir(&repo_path)
1817            .output()
1818            .unwrap();
1819        Command::new("git")
1820            .args(["config", "user.email", "test@test.com"])
1821            .current_dir(&repo_path)
1822            .output()
1823            .unwrap();
1824
1825        // Create initial commit
1826        std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
1827        Command::new("git")
1828            .args(["add", "."])
1829            .current_dir(&repo_path)
1830            .output()
1831            .unwrap();
1832        Command::new("git")
1833            .args(["commit", "-m", "Initial commit"])
1834            .current_dir(&repo_path)
1835            .output()
1836            .unwrap();
1837
1838        (temp_dir, repo_path)
1839    }
1840
1841    fn create_commit(repo_path: &PathBuf, message: &str, filename: &str) {
1842        let file_path = repo_path.join(filename);
1843        std::fs::write(&file_path, format!("Content for {filename}\n")).unwrap();
1844
1845        Command::new("git")
1846            .args(["add", filename])
1847            .current_dir(repo_path)
1848            .output()
1849            .unwrap();
1850        Command::new("git")
1851            .args(["commit", "-m", message])
1852            .current_dir(repo_path)
1853            .output()
1854            .unwrap();
1855    }
1856
1857    #[test]
1858    fn test_repository_info() {
1859        let (_temp_dir, repo_path) = create_test_repo();
1860        let repo = GitRepository::open(&repo_path).unwrap();
1861
1862        let info = repo.get_info().unwrap();
1863        assert!(!info.is_dirty); // Should be clean after commit
1864        assert!(
1865            info.head_branch == Some("master".to_string())
1866                || info.head_branch == Some("main".to_string()),
1867            "Expected default branch to be 'master' or 'main', got {:?}",
1868            info.head_branch
1869        );
1870        assert!(info.head_commit.is_some()); // Just check it exists
1871        assert!(info.untracked_files.is_empty()); // Should be empty after commit
1872    }
1873
1874    #[test]
1875    fn test_force_push_branch_basic() {
1876        let (_temp_dir, repo_path) = create_test_repo();
1877        let repo = GitRepository::open(&repo_path).unwrap();
1878
1879        // Get the actual default branch name
1880        let default_branch = repo.get_current_branch().unwrap();
1881
1882        // Create source branch with commits
1883        create_commit(&repo_path, "Feature commit 1", "feature1.rs");
1884        Command::new("git")
1885            .args(["checkout", "-b", "source-branch"])
1886            .current_dir(&repo_path)
1887            .output()
1888            .unwrap();
1889        create_commit(&repo_path, "Feature commit 2", "feature2.rs");
1890
1891        // Create target branch
1892        Command::new("git")
1893            .args(["checkout", &default_branch])
1894            .current_dir(&repo_path)
1895            .output()
1896            .unwrap();
1897        Command::new("git")
1898            .args(["checkout", "-b", "target-branch"])
1899            .current_dir(&repo_path)
1900            .output()
1901            .unwrap();
1902        create_commit(&repo_path, "Target commit", "target.rs");
1903
1904        // Test force push from source to target
1905        let result = repo.force_push_branch("target-branch", "source-branch");
1906
1907        // Should succeed in test environment (even though it doesn't actually push to remote)
1908        // The important thing is that the function doesn't panic and handles the git2 operations
1909        assert!(result.is_ok() || result.is_err()); // Either is acceptable for unit test
1910    }
1911
1912    #[test]
1913    fn test_force_push_branch_nonexistent_branches() {
1914        let (_temp_dir, repo_path) = create_test_repo();
1915        let repo = GitRepository::open(&repo_path).unwrap();
1916
1917        // Get the actual default branch name
1918        let default_branch = repo.get_current_branch().unwrap();
1919
1920        // Test force push with nonexistent source branch
1921        let result = repo.force_push_branch("target", "nonexistent-source");
1922        assert!(result.is_err());
1923
1924        // Test force push with nonexistent target branch
1925        let result = repo.force_push_branch("nonexistent-target", &default_branch);
1926        assert!(result.is_err());
1927    }
1928
1929    #[test]
1930    fn test_force_push_workflow_simulation() {
1931        let (_temp_dir, repo_path) = create_test_repo();
1932        let repo = GitRepository::open(&repo_path).unwrap();
1933
1934        // Simulate the smart force push workflow:
1935        // 1. Original branch exists with PR
1936        Command::new("git")
1937            .args(["checkout", "-b", "feature-auth"])
1938            .current_dir(&repo_path)
1939            .output()
1940            .unwrap();
1941        create_commit(&repo_path, "Add authentication", "auth.rs");
1942
1943        // 2. Rebase creates versioned branch
1944        Command::new("git")
1945            .args(["checkout", "-b", "feature-auth-v2"])
1946            .current_dir(&repo_path)
1947            .output()
1948            .unwrap();
1949        create_commit(&repo_path, "Fix auth validation", "auth.rs");
1950
1951        // 3. Smart force push: update original branch from versioned branch
1952        let result = repo.force_push_branch("feature-auth", "feature-auth-v2");
1953
1954        // Verify the operation is handled properly (success or expected error)
1955        match result {
1956            Ok(_) => {
1957                // Force push succeeded - verify branch state if possible
1958                Command::new("git")
1959                    .args(["checkout", "feature-auth"])
1960                    .current_dir(&repo_path)
1961                    .output()
1962                    .unwrap();
1963                let log_output = Command::new("git")
1964                    .args(["log", "--oneline", "-2"])
1965                    .current_dir(&repo_path)
1966                    .output()
1967                    .unwrap();
1968                let log_str = String::from_utf8_lossy(&log_output.stdout);
1969                assert!(
1970                    log_str.contains("Fix auth validation")
1971                        || log_str.contains("Add authentication")
1972                );
1973            }
1974            Err(_) => {
1975                // Expected in test environment without remote - that's fine
1976                // The important thing is we tested the code path without panicking
1977            }
1978        }
1979    }
1980
1981    #[test]
1982    fn test_branch_operations() {
1983        let (_temp_dir, repo_path) = create_test_repo();
1984        let repo = GitRepository::open(&repo_path).unwrap();
1985
1986        // Test get current branch - accept either main or master
1987        let current = repo.get_current_branch().unwrap();
1988        assert!(
1989            current == "master" || current == "main",
1990            "Expected default branch to be 'master' or 'main', got '{current}'"
1991        );
1992
1993        // Test create branch
1994        Command::new("git")
1995            .args(["checkout", "-b", "test-branch"])
1996            .current_dir(&repo_path)
1997            .output()
1998            .unwrap();
1999        let current = repo.get_current_branch().unwrap();
2000        assert_eq!(current, "test-branch");
2001    }
2002
2003    #[test]
2004    fn test_commit_operations() {
2005        let (_temp_dir, repo_path) = create_test_repo();
2006        let repo = GitRepository::open(&repo_path).unwrap();
2007
2008        // Test get head commit
2009        let head = repo.get_head_commit().unwrap();
2010        assert_eq!(head.message().unwrap().trim(), "Initial commit");
2011
2012        // Test get commit by hash
2013        let hash = head.id().to_string();
2014        let same_commit = repo.get_commit(&hash).unwrap();
2015        assert_eq!(head.id(), same_commit.id());
2016    }
2017
2018    #[test]
2019    fn test_checkout_safety_clean_repo() {
2020        let (_temp_dir, repo_path) = create_test_repo();
2021        let repo = GitRepository::open(&repo_path).unwrap();
2022
2023        // Create a test branch
2024        create_commit(&repo_path, "Second commit", "test.txt");
2025        Command::new("git")
2026            .args(["checkout", "-b", "test-branch"])
2027            .current_dir(&repo_path)
2028            .output()
2029            .unwrap();
2030
2031        // Test checkout safety with clean repo
2032        let safety_result = repo.check_checkout_safety("main");
2033        assert!(safety_result.is_ok());
2034        assert!(safety_result.unwrap().is_none()); // Clean repo should return None
2035    }
2036
2037    #[test]
2038    fn test_checkout_safety_with_modified_files() {
2039        let (_temp_dir, repo_path) = create_test_repo();
2040        let repo = GitRepository::open(&repo_path).unwrap();
2041
2042        // Create a test branch
2043        Command::new("git")
2044            .args(["checkout", "-b", "test-branch"])
2045            .current_dir(&repo_path)
2046            .output()
2047            .unwrap();
2048
2049        // Modify a file to create uncommitted changes
2050        std::fs::write(repo_path.join("README.md"), "Modified content").unwrap();
2051
2052        // Test checkout safety with modified files
2053        let safety_result = repo.check_checkout_safety("main");
2054        assert!(safety_result.is_ok());
2055        let safety_info = safety_result.unwrap();
2056        assert!(safety_info.is_some());
2057
2058        let info = safety_info.unwrap();
2059        assert!(!info.modified_files.is_empty());
2060        assert!(info.modified_files.contains(&"README.md".to_string()));
2061    }
2062
2063    #[test]
2064    fn test_unsafe_checkout_methods() {
2065        let (_temp_dir, repo_path) = create_test_repo();
2066        let repo = GitRepository::open(&repo_path).unwrap();
2067
2068        // Create a test branch
2069        create_commit(&repo_path, "Second commit", "test.txt");
2070        Command::new("git")
2071            .args(["checkout", "-b", "test-branch"])
2072            .current_dir(&repo_path)
2073            .output()
2074            .unwrap();
2075
2076        // Modify a file to create uncommitted changes
2077        std::fs::write(repo_path.join("README.md"), "Modified content").unwrap();
2078
2079        // Test unsafe checkout methods bypass safety checks
2080        let _result = repo.checkout_branch_unsafe("master");
2081        // Note: This might still fail due to git2 restrictions, but shouldn't hit our safety code
2082        // The important thing is that it doesn't trigger our safety confirmation
2083
2084        // Test unsafe commit checkout
2085        let head_commit = repo.get_head_commit().unwrap();
2086        let commit_hash = head_commit.id().to_string();
2087        let _result = repo.checkout_commit_unsafe(&commit_hash);
2088        // Similar to above - testing that safety is bypassed
2089    }
2090
2091    #[test]
2092    fn test_get_modified_files() {
2093        let (_temp_dir, repo_path) = create_test_repo();
2094        let repo = GitRepository::open(&repo_path).unwrap();
2095
2096        // Initially should have no modified files
2097        let modified = repo.get_modified_files().unwrap();
2098        assert!(modified.is_empty());
2099
2100        // Modify a file
2101        std::fs::write(repo_path.join("README.md"), "Modified content").unwrap();
2102
2103        // Should now detect the modified file
2104        let modified = repo.get_modified_files().unwrap();
2105        assert_eq!(modified.len(), 1);
2106        assert!(modified.contains(&"README.md".to_string()));
2107    }
2108
2109    #[test]
2110    fn test_get_staged_files() {
2111        let (_temp_dir, repo_path) = create_test_repo();
2112        let repo = GitRepository::open(&repo_path).unwrap();
2113
2114        // Initially should have no staged files
2115        let staged = repo.get_staged_files().unwrap();
2116        assert!(staged.is_empty());
2117
2118        // Create and stage a new file
2119        std::fs::write(repo_path.join("staged.txt"), "Staged content").unwrap();
2120        Command::new("git")
2121            .args(["add", "staged.txt"])
2122            .current_dir(&repo_path)
2123            .output()
2124            .unwrap();
2125
2126        // Should now detect the staged file
2127        let staged = repo.get_staged_files().unwrap();
2128        assert_eq!(staged.len(), 1);
2129        assert!(staged.contains(&"staged.txt".to_string()));
2130    }
2131
2132    #[test]
2133    fn test_create_stash_fallback() {
2134        let (_temp_dir, repo_path) = create_test_repo();
2135        let repo = GitRepository::open(&repo_path).unwrap();
2136
2137        // Test that stash creation returns helpful error message
2138        let result = repo.create_stash("test stash");
2139        assert!(result.is_err());
2140        let error_msg = result.unwrap_err().to_string();
2141        assert!(error_msg.contains("git stash push"));
2142    }
2143
2144    #[test]
2145    fn test_delete_branch_unsafe() {
2146        let (_temp_dir, repo_path) = create_test_repo();
2147        let repo = GitRepository::open(&repo_path).unwrap();
2148
2149        // Create a test branch
2150        create_commit(&repo_path, "Second commit", "test.txt");
2151        Command::new("git")
2152            .args(["checkout", "-b", "test-branch"])
2153            .current_dir(&repo_path)
2154            .output()
2155            .unwrap();
2156
2157        // Add another commit to the test branch to make it different from master
2158        create_commit(&repo_path, "Branch-specific commit", "branch.txt");
2159
2160        // Go back to master
2161        Command::new("git")
2162            .args(["checkout", "master"])
2163            .current_dir(&repo_path)
2164            .output()
2165            .unwrap();
2166
2167        // Test unsafe delete bypasses safety checks
2168        // Note: This may still fail if the branch has unpushed commits, but it should bypass our safety confirmation
2169        let result = repo.delete_branch_unsafe("test-branch");
2170        // Even if it fails, the key is that it didn't prompt for user confirmation
2171        // So we just check that it attempted the operation without interactive prompts
2172        let _ = result; // Don't assert success since delete may fail for git reasons
2173    }
2174
2175    #[test]
2176    fn test_force_push_unsafe() {
2177        let (_temp_dir, repo_path) = create_test_repo();
2178        let repo = GitRepository::open(&repo_path).unwrap();
2179
2180        // Create a test branch
2181        create_commit(&repo_path, "Second commit", "test.txt");
2182        Command::new("git")
2183            .args(["checkout", "-b", "test-branch"])
2184            .current_dir(&repo_path)
2185            .output()
2186            .unwrap();
2187
2188        // Test unsafe force push bypasses safety checks
2189        // Note: This will likely fail due to no remote, but it tests the safety bypass
2190        let _result = repo.force_push_branch_unsafe("test-branch", "test-branch");
2191        // The key is that it doesn't trigger safety confirmation dialogs
2192    }
2193}