use std::path::Path;
use super::AtomicEvent;
use crate::auth::auth_utils::transform_url_for_preference;
use crate::auth::git_auth::setup_auth_callbacks;
use crate::bgit_error::BGitError;
use crate::config::global::BGitGlobalConfig;
use crate::rules::Rule;
use git2::Repository;
use log::info;
pub struct GitPull<'a> {
pub pre_check_rules: Vec<Box<dyn Rule + Send + Sync>>,
pub rebase: bool,
pub global_config: &'a BGitGlobalConfig,
}
impl<'a> AtomicEvent<'a> for GitPull<'a> {
fn new(global_config: &'a BGitGlobalConfig) -> Self
where
Self: Sized,
{
GitPull {
pre_check_rules: vec![],
rebase: true,
global_config,
}
}
fn get_name(&self) -> &str {
"git_pull"
}
fn get_action_description(&self) -> &str {
"Pull changes from remote repository"
}
fn add_pre_check_rule(&mut self, rule: Box<dyn Rule + Send + Sync>) {
self.pre_check_rules.push(rule);
}
fn get_pre_check_rule(&self) -> &Vec<Box<dyn Rule + Send + Sync>> {
&self.pre_check_rules
}
fn raw_execute(&self) -> Result<bool, Box<BGitError>> {
let repo = Repository::discover(Path::new("."))
.map_err(|e| self.to_bgit_error(&format!("Failed to discover repository: {e}")))?;
let branch_name = match repo.head() {
Ok(head) => head
.shorthand()
.ok_or_else(|| self.to_bgit_error("Failed to get branch name"))?
.to_string(),
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {
return self.fetch_only(&repo);
}
Err(e) => {
return Err(self.to_bgit_error(&format!("Failed to get HEAD reference: {e}")));
}
};
let mut remote = match repo.find_remote("origin") {
Ok(remote) => remote,
Err(e) if e.code() == git2::ErrorCode::NotFound => {
return Err(self.to_bgit_error("No remote 'origin' configured. Please add a remote repository first with: git remote add origin <repository-url>"));
}
Err(e) => {
return Err(self.to_bgit_error(&format!("Failed to find remote 'origin': {e}")));
}
};
if let Some(url) = remote.url()
&& let Some(new_url) =
transform_url_for_preference(url, self.global_config.auth.preferred)
{
let preferred = self.global_config.auth.preferred;
info!(
"Using preferred auth ({:?}) URL: {} -> {}",
preferred, url, new_url
);
if let Ok(temp) = repo.remote_anonymous(new_url.as_str()) {
remote = temp;
}
}
let mut fetch_options = self.create_fetch_options();
remote.fetch(&[&"refs/heads/*:refs/remotes/origin/*".to_string()], Some(&mut fetch_options), None).map_err(|e| {
self.to_bgit_error(&format!("Failed to fetch from remote: {e}. Please check your SSH keys or authentication setup."))
})?;
let remote_branch_name = format!("refs/remotes/origin/{branch_name}");
let remote_ref = repo
.find_reference(&remote_branch_name)
.or_else(|_| {
let alternatives = vec![
format!("refs/remotes/origin/main"),
format!("refs/remotes/origin/master"),
format!("refs/remotes/origin/develop"),
];
for alt in alternatives {
if let Ok(reference) = repo.find_reference(&alt) {
return Ok(reference);
}
}
let remote_branches: Vec<String> = repo
.branches(Some(git2::BranchType::Remote))
.map_err(|e| format!("Failed to list remote branches: {e}"))
.unwrap()
.filter_map(|branch_result| {
branch_result.ok().and_then(|(branch, _)| {
branch.name().ok().flatten().map(|name| name.to_string())
})
})
.collect();
Err(git2::Error::new(
git2::ErrorCode::NotFound,
git2::ErrorClass::Reference,
format!(
"Remote branch 'origin/{branch_name}' not found. Available remote branches: {remote_branches:?}"
),
))
})
.map_err(|e| {
self.to_bgit_error(&format!("Failed to find remote reference: {e}"))
})?;
if self.rebase {
self.execute_rebase(&repo, &remote_ref)?;
} else {
self.execute_merge(&repo, &remote_ref)?;
}
Ok(true)
}
}
impl<'a> GitPull<'a> {
pub fn with_rebase(mut self, rebase: bool) -> Self {
self.rebase = rebase;
self
}
fn execute_rebase(
&self,
repo: &Repository,
remote_ref: &git2::Reference,
) -> Result<(), Box<BGitError>> {
let remote_commit = remote_ref
.peel_to_commit()
.map_err(|e| self.to_bgit_error(&format!("Failed to get remote commit: {e}")))?;
let head_commit = repo
.head()
.map_err(|e| self.to_bgit_error(&format!("Failed to get HEAD reference: {e}")))?
.peel_to_commit()
.map_err(|e| self.to_bgit_error(&format!("Failed to get HEAD commit: {e}")))?;
if head_commit.id() == remote_commit.id() {
return Ok(());
}
let merge_base = repo
.merge_base(head_commit.id(), remote_commit.id())
.map_err(|e| self.to_bgit_error(&format!("Failed to find merge base: {e}")))?;
if merge_base == remote_commit.id() {
return Ok(());
}
let upstream_annotated = repo
.find_annotated_commit(remote_commit.id())
.map_err(|e| {
self.to_bgit_error(&format!(
"Failed to create annotated commit for upstream: {e}"
))
})?;
let mut rebase = repo
.rebase(None, Some(&upstream_annotated), None, None)
.map_err(|e| {
self.to_bgit_error(&format!("Failed to start rebase: {e}. This might indicate conflicts or uncommitted changes."))
})?;
let mut operation_count = 0;
while let Some(_) = rebase.next() {
operation_count += 1;
let index = repo
.index()
.map_err(|e| self.to_bgit_error(&format!("Failed to get repository index: {e}")))?;
if index.has_conflicts() {
rebase.abort().map_err(|e| {
self.to_bgit_error(&format!("Failed to abort rebase after conflicts: {e}"))
})?;
return Err(self.to_bgit_error("Rebase conflicts detected. The rebase has been aborted to prevent data loss. Please resolve conflicts manually and retry."));
}
let signature = repo
.signature()
.map_err(|e| self.to_bgit_error(&format!("Failed to get signature: {e}")))?;
match rebase.commit(None, &signature, None) {
Ok(_commit_id) => {
}
Err(err) => {
if err.code() == git2::ErrorCode::Applied
|| err
.message()
.to_lowercase()
.contains("already been applied")
{
continue;
}
return Err(self.to_bgit_error(&format!(
"Failed to commit during rebase operation {operation_count}: {err}"
)));
}
}
}
rebase
.finish(None)
.map_err(|e| self.to_bgit_error(&format!("Failed to finish rebase: {e}")))?;
Ok(())
}
fn execute_merge(
&self,
repo: &Repository,
remote_ref: &git2::Reference,
) -> Result<(), Box<BGitError>> {
let remote_commit = remote_ref
.peel_to_commit()
.map_err(|e| self.to_bgit_error(&format!("Failed to get remote commit: {e}")))?;
let head_commit = {
let head = repo
.head()
.map_err(|e| self.to_bgit_error(&format!("Failed to get HEAD reference: {e}")))?;
head.peel_to_commit()
.map_err(|e| self.to_bgit_error(&format!("Failed to get HEAD commit: {e}")))?
};
if head_commit.id() == remote_commit.id() {
return Ok(());
}
let merge_base = repo
.merge_base(head_commit.id(), remote_commit.id())
.map_err(|e| self.to_bgit_error(&format!("Failed to find merge base: {e}")))?;
if merge_base == remote_commit.id() {
return Ok(());
}
if merge_base == head_commit.id() {
let mut head_ref = repo.head().map_err(|e| {
self.to_bgit_error(&format!(
"Failed to get HEAD reference for fast-forward: {e}"
))
})?;
head_ref
.set_target(remote_commit.id(), "Fast-forward merge")
.map_err(|e| self.to_bgit_error(&format!("Failed to fast-forward: {e}")))?;
repo.checkout_head(Some(git2::build::CheckoutBuilder::new().force()))
.map_err(|e| {
self.to_bgit_error(&format!("Failed to checkout after fast-forward: {e}"))
})?;
} else {
return Err(self.to_bgit_error("Merge conflicts detected - manual resolution required"));
}
Ok(())
}
fn create_fetch_options(&'a self) -> git2::FetchOptions<'a> {
let mut fetch_options = git2::FetchOptions::new();
fetch_options.remote_callbacks(setup_auth_callbacks(self.global_config));
fetch_options
}
fn fetch_only(&self, repo: &Repository) -> Result<bool, Box<BGitError>> {
let mut remote = match repo.find_remote("origin") {
Ok(remote) => remote,
Err(e) if e.code() == git2::ErrorCode::NotFound => {
return Err(self.to_bgit_error("No remote 'origin' configured. Please add a remote repository first with: git remote add origin <repository-url>"));
}
Err(e) => {
return Err(self.to_bgit_error(&format!("Failed to find remote 'origin': {e}")));
}
};
let mut fetch_options = self.create_fetch_options();
remote.fetch(&[&"refs/heads/*:refs/remotes/origin/*".to_string()], Some(&mut fetch_options), None).map_err(|e| {
self.to_bgit_error(&format!("Failed to fetch from remote: {e}. Please check your SSH keys or authentication setup."))
})?;
println!(
"Successfully fetched from remote (no merge/rebase performed - repository has no commits yet)"
);
Ok(true)
}
}