gitkraft_core/features/remotes/
ops.rs1use anyhow::{Context, Result};
4use git2::Repository;
5use tracing::debug;
6
7use super::types::RemoteInfo;
8
9pub fn list_remotes(repo: &Repository) -> Result<Vec<RemoteInfo>> {
11 let remote_names = repo.remotes().context("failed to list remotes")?;
12 let mut remotes = Vec::with_capacity(remote_names.len());
13
14 for name in remote_names.iter() {
15 let name = name.unwrap_or("<invalid utf-8>");
16 let remote = repo
17 .find_remote(name)
18 .with_context(|| format!("failed to find remote '{name}'"))?;
19
20 let url = remote.url().map(String::from);
21
22 let mut fetch_refspecs = Vec::new();
23 let refspecs = remote.refspecs();
24 for refspec in refspecs {
25 if refspec.direction() == git2::Direction::Fetch {
26 if let Some(s) = refspec.str() {
27 fetch_refspecs.push(s.to_string());
28 }
29 }
30 }
31
32 remotes.push(RemoteInfo {
33 name: name.to_string(),
34 url,
35 fetch_refspecs,
36 });
37 }
38
39 debug!("found {} remotes", remotes.len());
40 Ok(remotes)
41}
42
43pub fn fetch_remote(repo: &Repository, remote_name: &str) -> Result<()> {
49 let mut remote = repo
50 .find_remote(remote_name)
51 .with_context(|| format!("remote '{remote_name}' not found"))?;
52
53 debug!("fetching from remote '{}'", remote_name);
54
55 remote
56 .fetch(&[] as &[&str], None, None)
57 .with_context(|| format!("failed to fetch from remote '{remote_name}'"))?;
58
59 Ok(())
60}
61
62pub fn pull(repo: &Repository, remote_name: &str, branch: &str) -> Result<()> {
68 fetch_remote(repo, remote_name)?;
70
71 let fetch_head = repo
73 .find_reference("FETCH_HEAD")
74 .context("FETCH_HEAD not found after fetch")?;
75 let fetch_commit_oid = fetch_head
76 .target()
77 .context("FETCH_HEAD is not a direct reference")?;
78 let fetch_commit = repo
79 .find_commit(fetch_commit_oid)
80 .context("failed to find FETCH_HEAD commit")?;
81
82 let refname = format!("refs/heads/{branch}");
84 match repo.find_reference(&refname) {
85 Ok(mut local_ref) => {
86 let local_oid = local_ref
87 .target()
88 .context("local branch ref is not direct")?;
89
90 let (ahead, behind) = repo
92 .graph_ahead_behind(local_oid, fetch_commit_oid)
93 .context("failed to compute ahead/behind")?;
94
95 if behind == 0 {
96 debug!(
97 "local branch '{}' is already up to date (ahead by {})",
98 branch, ahead
99 );
100 return Ok(());
101 }
102
103 if ahead > 0 {
104 anyhow::bail!(
105 "cannot fast-forward: local branch '{branch}' has diverged \
106 ({ahead} ahead, {behind} behind). Use merge instead."
107 );
108 }
109
110 debug!(
112 "fast-forwarding '{}' from {} to {}",
113 branch, local_oid, fetch_commit_oid
114 );
115 local_ref
116 .set_target(
117 fetch_commit_oid,
118 &format!("pull: fast-forward {branch} to {fetch_commit_oid}"),
119 )
120 .context("failed to fast-forward branch reference")?;
121
122 repo.set_head(&refname)
124 .context("failed to set HEAD after fast-forward")?;
125 repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))
126 .context("failed to checkout HEAD after fast-forward")?;
127 }
128 Err(_) => {
129 debug!(
131 "local branch '{}' not found, creating at {}",
132 branch, fetch_commit_oid
133 );
134 repo.branch(branch, &fetch_commit, false)
135 .with_context(|| format!("failed to create branch '{branch}'"))?;
136 }
137 }
138
139 Ok(())
140}
141
142pub fn push(repo: &Repository, remote_name: &str, branch: &str) -> Result<()> {
147 let mut remote = repo
148 .find_remote(remote_name)
149 .with_context(|| format!("remote '{remote_name}' not found"))?;
150
151 let refspec = format!("refs/heads/{branch}:refs/heads/{branch}");
152
153 debug!(
154 "pushing '{}' to remote '{}' with refspec '{}'",
155 branch, remote_name, refspec
156 );
157
158 remote
159 .push(&[&refspec], None)
160 .with_context(|| format!("failed to push '{branch}' to remote '{remote_name}'"))?;
161
162 Ok(())
163}