use crate::binary::new_binary_command; use crate::error::Error::GenericError; use crate::error::Result; use crate::repository::open_repo; use crate::util::{get_current_branch_name, get_default_remote_in_repo}; use log::info; use serde::{Deserialize, Serialize}; use std::path::Path; use ts_rs::TS; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "snake_case", tag = "type")] #[ts(export, export_to = "gen_git.ts")] pub enum PullResult { Success { message: String }, UpToDate, NeedsCredentials { url: String, error: Option }, Diverged { remote: String, branch: String }, UncommittedChanges, } fn has_uncommitted_changes(dir: &Path) -> Result { let repo = open_repo(dir)?; let mut opts = git2::StatusOptions::new(); opts.include_ignored(false).include_untracked(false); let statuses = repo.statuses(Some(&mut opts))?; Ok(statuses.iter().any(|e| e.status() != git2::Status::CURRENT)) } pub async fn git_pull(dir: &Path) -> Result { if has_uncommitted_changes(dir)? { return Ok(PullResult::UncommittedChanges); } // Extract all git2 data before any await points (git2 types are not Send) let (branch_name, remote_name, remote_url) = { let repo = open_repo(dir)?; let branch_name = get_current_branch_name(&repo)?; let remote = get_default_remote_in_repo(&repo)?; let remote_name = remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?.to_string(); let remote_url = remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?.to_string(); (branch_name, remote_name, remote_url) }; // Step 1: fetch the specific branch // NOTE: We use fetch + merge instead of `git pull` to avoid conflicts with // global git config (e.g. pull.ff=only) and the background fetch --all. let fetch_out = new_binary_command(dir) .await? .args(["fetch", &remote_name, &branch_name]) .env("GIT_TERMINAL_PROMPT", "0") .output() .await .map_err(|e| GenericError(format!("failed to run git fetch: {e}")))?; let fetch_stdout = String::from_utf8_lossy(&fetch_out.stdout); let fetch_stderr = String::from_utf8_lossy(&fetch_out.stderr); let fetch_combined = format!("{fetch_stdout}{fetch_stderr}"); info!("Fetched status={} {fetch_combined}", fetch_out.status); if fetch_combined.to_lowercase().contains("could not read") { return Ok(PullResult::NeedsCredentials { url: remote_url.to_string(), error: None }); } if fetch_combined.to_lowercase().contains("unable to access") { return Ok(PullResult::NeedsCredentials { url: remote_url.to_string(), error: Some(fetch_combined.to_string()), }); } if !fetch_out.status.success() { return Err(GenericError(format!("Failed to fetch: {fetch_combined}"))); } // Step 2: merge the fetched branch let ref_name = format!("{}/{}", remote_name, branch_name); let merge_out = new_binary_command(dir) .await? .args(["merge", "--ff-only", &ref_name]) .output() .await .map_err(|e| GenericError(format!("failed to run git merge: {e}")))?; let merge_stdout = String::from_utf8_lossy(&merge_out.stdout); let merge_stderr = String::from_utf8_lossy(&merge_out.stderr); let merge_combined = format!("{merge_stdout}{merge_stderr}"); info!("Merged status={} {merge_combined}", merge_out.status); if !merge_out.status.success() { let merge_lower = merge_combined.to_lowercase(); if merge_lower.contains("cannot fast-forward") || merge_lower.contains("not possible to fast-forward") || merge_lower.contains("diverged") { return Ok(PullResult::Diverged { remote: remote_name, branch: branch_name }); } return Err(GenericError(format!("Failed to merge: {merge_combined}"))); } if merge_combined.to_lowercase().contains("up to date") { return Ok(PullResult::UpToDate); } Ok(PullResult::Success { message: format!("Pulled from {}/{}", remote_name, branch_name) }) } pub async fn git_pull_force_reset(dir: &Path, remote: &str, branch: &str) -> Result { // Step 1: fetch the remote let fetch_out = new_binary_command(dir) .await? .args(["fetch", remote]) .env("GIT_TERMINAL_PROMPT", "0") .output() .await .map_err(|e| GenericError(format!("failed to run git fetch: {e}")))?; if !fetch_out.status.success() { let stderr = String::from_utf8_lossy(&fetch_out.stderr); return Err(GenericError(format!("Failed to fetch: {stderr}"))); } // Step 2: reset --hard to remote/branch let ref_name = format!("{}/{}", remote, branch); let reset_out = new_binary_command(dir) .await? .args(["reset", "--hard", &ref_name]) .output() .await .map_err(|e| GenericError(format!("failed to run git reset: {e}")))?; if !reset_out.status.success() { let stderr = String::from_utf8_lossy(&reset_out.stderr); return Err(GenericError(format!("Failed to reset: {}", stderr.trim()))); } Ok(PullResult::Success { message: format!("Reset to {}/{}", remote, branch) }) } pub async fn git_pull_merge(dir: &Path, remote: &str, branch: &str) -> Result { let out = new_binary_command(dir) .await? .args(["pull", "--no-rebase", remote, branch]) .env("GIT_TERMINAL_PROMPT", "0") .output() .await .map_err(|e| GenericError(format!("failed to run git pull --no-rebase: {e}")))?; let stdout = String::from_utf8_lossy(&out.stdout); let stderr = String::from_utf8_lossy(&out.stderr); let combined = format!("{}{}", stdout, stderr); info!("Pull merge status={} {combined}", out.status); if !out.status.success() { if combined.to_lowercase().contains("conflict") { return Err(GenericError( "Merge conflicts detected. Please resolve them manually.".to_string(), )); } return Err(GenericError(format!("Failed to merge pull: {}", combined.trim()))); } Ok(PullResult::Success { message: format!("Merged from {}/{}", remote, branch) }) } // pub(crate) fn git_pull_old(dir: &Path) -> Result { // let repo = open_repo(dir)?; // // let branch = get_current_branch(&repo)?.ok_or(NoActiveBranch)?; // let branch_ref = branch.get(); // let branch_ref = bytes_to_string(branch_ref.name_bytes())?; // // let remote_name = repo.branch_upstream_remote(&branch_ref)?; // let remote_name = bytes_to_string(&remote_name)?; // debug!("Pulling from {remote_name}"); // // let mut remote = repo.find_remote(&remote_name)?; // // let mut options = FetchOptions::new(); // let callbacks = default_callbacks(); // options.remote_callbacks(callbacks); // // let mut proxy = ProxyOptions::new(); // proxy.auto(); // options.proxy_options(proxy); // // remote.fetch(&[&branch_ref], Some(&mut options), None)?; // // let stats = remote.stats(); // // let fetch_head = repo.find_reference("FETCH_HEAD")?; // let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?; // do_merge(&repo, &branch, &fetch_commit)?; // // Ok(PullResult::Success { // message: "Hello".to_string(), // // received_bytes: stats.received_bytes(), // // received_objects: stats.received_objects(), // }) // }