thoughts_tool/git/
pull.rs1use anyhow::Context;
2use anyhow::Result;
3use git2::AnnotatedCommit;
4use git2::Repository;
5use std::path::Path;
6
7use crate::git::shell_fetch;
8use crate::git::utils::is_worktree_dirty;
9
10pub fn pull_ff_only(repo_path: &Path, remote_name: &str, branch: Option<&str>) -> Result<()> {
13 {
15 let repo = Repository::open(repo_path)
16 .with_context(|| format!("Failed to open repository at {}", repo_path.display()))?;
17 if repo.find_remote(remote_name).is_err() {
18 return Ok(());
20 }
21 }
22
23 let branch = branch.unwrap_or("main");
24
25 shell_fetch::fetch(repo_path, remote_name).with_context(|| {
27 format!(
28 "Fetch failed for remote '{}' in '{}'",
29 remote_name,
30 repo_path.display()
31 )
32 })?;
33
34 let repo = Repository::open(repo_path)
36 .with_context(|| format!("Failed to re-open repository at {}", repo_path.display()))?;
37
38 let remote_ref = format!("refs/remotes/{remote_name}/{branch}");
40 let Ok(fetch_head) = repo.find_reference(&remote_ref) else {
41 return Ok(());
43 };
44 let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
45
46 try_fast_forward(&repo, &format!("refs/heads/{branch}"), &fetch_commit)?;
47 Ok(())
48}
49
50fn try_fast_forward(
51 repo: &Repository,
52 local_ref: &str,
53 fetch_commit: &AnnotatedCommit,
54) -> Result<()> {
55 let analysis = repo.merge_analysis(&[fetch_commit])?;
56 if analysis.0.is_up_to_date() {
57 return Ok(());
58 }
59 if analysis.0.is_fast_forward() {
60 if is_worktree_dirty(repo)? {
62 anyhow::bail!(
63 "Cannot fast-forward: working tree has uncommitted changes. Please commit or stash before pulling."
64 );
65 }
66 repo.set_head(local_ref)?;
70 let obj = repo.find_object(fetch_commit.id(), None)?;
72 repo.reset(
73 &obj,
74 git2::ResetType::Hard,
75 Some(git2::build::CheckoutBuilder::default().force()),
76 )?;
77 return Ok(());
78 }
79 anyhow::bail!(
80 "Non fast-forward update required (local and remote have diverged; rebase or merge needed)."
81 )
82}