1use anyhow::Context;
23use std::path::Path;
24
25use crate::git::error::{GitError, classify_push_error, git_output, git_run};
26
27pub fn upstream_ref(repo_root: &Path) -> Result<String, GitError> {
31 let output = git_output(
32 repo_root,
33 &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
34 )
35 .with_context(|| {
36 format!(
37 "run git rev-parse --abbrev-ref --symbolic-full-name @{{u}} in {}",
38 repo_root.display()
39 )
40 })?;
41
42 if !output.status.success() {
43 let stderr = String::from_utf8_lossy(&output.stderr);
44 return Err(classify_push_error(&stderr));
45 }
46
47 let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
48 if value.is_empty() {
49 return Err(GitError::NoUpstreamConfigured);
50 }
51 Ok(value)
52}
53
54pub fn is_ahead_of_upstream(repo_root: &Path) -> Result<bool, GitError> {
58 let upstream = upstream_ref(repo_root)?;
59 let (_behind, ahead) = rev_list_left_right_counts(repo_root, &format!("{upstream}...HEAD"))?;
60 Ok(ahead > 0)
61}
62
63pub fn push_upstream(repo_root: &Path) -> Result<(), GitError> {
68 let output = git_output(repo_root, &["push"])
69 .with_context(|| format!("run git push in {}", repo_root.display()))?;
70
71 if output.status.success() {
72 return Ok(());
73 }
74
75 let stderr = String::from_utf8_lossy(&output.stderr);
76 Err(classify_push_error(&stderr))
77}
78
79pub fn push_upstream_allow_create(repo_root: &Path) -> Result<(), GitError> {
83 let output = git_output(repo_root, &["push", "-u", "origin", "HEAD"])
84 .with_context(|| format!("run git push -u origin HEAD in {}", repo_root.display()))?;
85
86 if output.status.success() {
87 return Ok(());
88 }
89
90 let stderr = String::from_utf8_lossy(&output.stderr);
91 Err(classify_push_error(&stderr))
92}
93
94pub fn fetch_branch(repo_root: &Path, remote: &str, branch: &str) -> Result<(), GitError> {
96 git_run(repo_root, &["fetch", remote, branch])
97 .with_context(|| format!("fetch {} {} in {}", remote, branch, repo_root.display()))?;
98 Ok(())
99}
100
101pub fn is_behind_upstream(repo_root: &Path, branch: &str) -> Result<bool, GitError> {
105 fetch_branch(repo_root, "origin", branch)?;
106
107 let upstream = format!("origin/{}", branch);
108 let (_ahead, behind) = rev_list_left_right_counts(repo_root, &format!("HEAD...{upstream}"))?;
109 Ok(behind > 0)
110}
111
112pub fn rebase_onto(repo_root: &Path, target: &str) -> Result<(), GitError> {
114 git_run(repo_root, &["fetch", "origin", "--prune"])
115 .with_context(|| format!("fetch before rebase in {}", repo_root.display()))?;
116 git_run(repo_root, &["rebase", target])
117 .with_context(|| format!("rebase onto {} in {}", target, repo_root.display()))?;
118 Ok(())
119}
120
121pub fn abort_rebase(repo_root: &Path) -> Result<(), GitError> {
123 git_run(repo_root, &["rebase", "--abort"])
124 .with_context(|| format!("abort rebase in {}", repo_root.display()))?;
125 Ok(())
126}
127
128pub fn list_conflict_files(repo_root: &Path) -> Result<Vec<String>, GitError> {
132 let output =
133 git_output(repo_root, &["diff", "--name-only", "--diff-filter=U"]).with_context(|| {
134 format!(
135 "run git diff --name-only --diff-filter=U in {}",
136 repo_root.display()
137 )
138 })?;
139
140 if !output.status.success() {
141 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
142 return Err(GitError::CommandFailed {
143 args: "diff --name-only --diff-filter=U".to_string(),
144 code: output.status.code(),
145 stderr: stderr.trim().to_string(),
146 });
147 }
148
149 let stdout = String::from_utf8_lossy(&output.stdout);
150 Ok(stdout
151 .lines()
152 .map(|s| s.trim().to_string())
153 .filter(|s| !s.is_empty())
154 .collect())
155}
156
157pub fn push_current_branch(repo_root: &Path, remote: &str) -> Result<(), GitError> {
161 let output = git_output(repo_root, &["push", remote, "HEAD"])
162 .with_context(|| format!("run git push {} HEAD in {}", remote, repo_root.display()))?;
163
164 if output.status.success() {
165 return Ok(());
166 }
167
168 let stderr = String::from_utf8_lossy(&output.stderr);
169 Err(classify_push_error(&stderr))
170}
171
172pub fn push_head_to_branch(repo_root: &Path, remote: &str, branch: &str) -> Result<(), GitError> {
177 let refspec = format!("HEAD:{}", branch);
178 let output = git_output(repo_root, &["push", remote, &refspec]).with_context(|| {
179 format!(
180 "run git push {} HEAD:{} in {}",
181 remote,
182 branch,
183 repo_root.display()
184 )
185 })?;
186
187 if output.status.success() {
188 return Ok(());
189 }
190
191 let stderr = String::from_utf8_lossy(&output.stderr);
192 Err(classify_push_error(&stderr))
193}
194
195pub(super) fn reference_exists(repo_root: &Path, reference: &str) -> Result<bool, GitError> {
196 let output = git_output(repo_root, &["rev-parse", "--verify", "--quiet", reference])
197 .with_context(|| {
198 format!(
199 "run git rev-parse --verify --quiet {} in {}",
200 reference,
201 repo_root.display()
202 )
203 })?;
204 if output.status.success() {
205 return Ok(true);
206 }
207 if output.status.code() == Some(1) {
208 return Ok(false);
209 }
210 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
211 Err(GitError::CommandFailed {
212 args: format!("rev-parse --verify --quiet {}", reference),
213 code: output.status.code(),
214 stderr: stderr.trim().to_string(),
215 })
216}
217
218pub(super) fn is_ahead_of_ref(repo_root: &Path, reference: &str) -> Result<bool, GitError> {
219 let (_behind, ahead) = rev_list_left_right_counts(repo_root, &format!("{reference}...HEAD"))?;
220 Ok(ahead > 0)
221}
222
223pub(super) fn set_upstream_to(repo_root: &Path, upstream: &str) -> Result<(), GitError> {
224 git_run(repo_root, &["branch", "--set-upstream-to", upstream])
225 .with_context(|| format!("set upstream to {} in {}", upstream, repo_root.display()))?;
226 Ok(())
227}
228
229pub(super) fn rev_list_left_right_counts(
230 repo_root: &Path,
231 range: &str,
232) -> Result<(u32, u32), GitError> {
233 let output = git_output(repo_root, &["rev-list", "--left-right", "--count", range])
234 .with_context(|| {
235 format!(
236 "run git rev-list --left-right --count {} in {}",
237 range,
238 repo_root.display()
239 )
240 })?;
241
242 if !output.status.success() {
243 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
244 return Err(GitError::CommandFailed {
245 args: format!("rev-list --left-right --count {}", range),
246 code: output.status.code(),
247 stderr: stderr.trim().to_string(),
248 });
249 }
250
251 let counts = String::from_utf8_lossy(&output.stdout);
252 let parts: Vec<&str> = counts.split_whitespace().collect();
253 if parts.len() != 2 {
254 return Err(GitError::UnexpectedRevListOutput(counts.trim().to_string()));
255 }
256
257 let left: u32 = parts[0].parse().context("parse left count")?;
258 let right: u32 = parts[1].parse().context("parse right count")?;
259 Ok((left, right))
260}