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 fetch_head = match repo.find_reference(&remote_ref) {
41 Ok(r) => r,
42 Err(_) => {
43 return Ok(());
45 }
46 };
47 let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
48
49 try_fast_forward(&repo, &format!("refs/heads/{}", branch), &fetch_commit)?;
50 Ok(())
51}
52
53fn try_fast_forward(
54 repo: &Repository,
55 local_ref: &str,
56 fetch_commit: &AnnotatedCommit,
57) -> Result<()> {
58 let analysis = repo.merge_analysis(&[fetch_commit])?;
59 if analysis.0.is_up_to_date() {
60 return Ok(());
61 }
62 if analysis.0.is_fast_forward() {
63 if is_worktree_dirty(repo)? {
65 anyhow::bail!(
66 "Cannot fast-forward: working tree has uncommitted changes. Please commit or stash before pulling."
67 );
68 }
69 repo.set_head(local_ref)?;
73 let obj = repo.find_object(fetch_commit.id(), None)?;
75 repo.reset(
76 &obj,
77 git2::ResetType::Hard,
78 Some(git2::build::CheckoutBuilder::default().force()),
79 )?;
80 return Ok(());
81 }
82 anyhow::bail!(
83 "Non fast-forward update required (local and remote have diverged; rebase or merge needed)."
84 )
85}