use anyhow::Context;
use anyhow::Result;
use git2::AnnotatedCommit;
use git2::Repository;
use std::path::Path;
use crate::git::shell_fetch;
use crate::git::utils::is_worktree_dirty;
pub fn pull_ff_only(repo_path: &Path, remote_name: &str, branch: Option<&str>) -> Result<()> {
{
let repo = Repository::open(repo_path)
.with_context(|| format!("Failed to open repository at {}", repo_path.display()))?;
if repo.find_remote(remote_name).is_err() {
return Ok(());
}
}
let branch = branch.unwrap_or("main");
shell_fetch::fetch(repo_path, remote_name).with_context(|| {
format!(
"Fetch failed for remote '{}' in '{}'",
remote_name,
repo_path.display()
)
})?;
let repo = Repository::open(repo_path)
.with_context(|| format!("Failed to re-open repository at {}", repo_path.display()))?;
let remote_ref = format!("refs/remotes/{remote_name}/{branch}");
let Ok(fetch_head) = repo.find_reference(&remote_ref) else {
return Ok(());
};
let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
try_fast_forward(&repo, &format!("refs/heads/{branch}"), &fetch_commit)?;
Ok(())
}
fn try_fast_forward(
repo: &Repository,
local_ref: &str,
fetch_commit: &AnnotatedCommit,
) -> Result<()> {
let analysis = repo.merge_analysis(&[fetch_commit])?;
if analysis.0.is_up_to_date() {
return Ok(());
}
if analysis.0.is_fast_forward() {
if is_worktree_dirty(repo)? {
anyhow::bail!(
"Cannot fast-forward: working tree has uncommitted changes. Please commit or stash before pulling."
);
}
repo.set_head(local_ref)?;
let obj = repo.find_object(fetch_commit.id(), None)?;
repo.reset(
&obj,
git2::ResetType::Hard,
Some(git2::build::CheckoutBuilder::default().force()),
)?;
return Ok(());
}
anyhow::bail!(
"Non fast-forward update required (local and remote have diverged; rebase or merge needed)."
)
}