git_workflow/git/
query.rs1use std::path::PathBuf;
4use std::process::Command;
5
6use crate::error::{GwError, Result};
7
8fn git_output(args: &[&str]) -> Result<String> {
10 let output = Command::new("git")
11 .args(args)
12 .output()
13 .map_err(|e| GwError::GitCommandFailed(format!("Failed to execute git: {e}")))?;
14
15 if output.status.success() {
16 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
17 } else {
18 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
19 Err(GwError::GitCommandFailed(stderr))
20 }
21}
22
23fn git_check(args: &[&str]) -> bool {
25 Command::new("git")
26 .args(args)
27 .output()
28 .map(|o| o.status.success())
29 .unwrap_or(false)
30}
31
32pub fn is_git_repo() -> bool {
34 git_check(&["rev-parse", "--git-dir"])
35}
36
37pub fn current_branch() -> Result<String> {
39 git_output(&["rev-parse", "--abbrev-ref", "HEAD"])
40}
41
42pub fn worktree_root() -> Result<PathBuf> {
44 git_output(&["rev-parse", "--show-toplevel"]).map(PathBuf::from)
45}
46
47pub fn git_dir() -> Result<PathBuf> {
49 git_output(&["rev-parse", "--git-dir"]).map(PathBuf::from)
50}
51
52pub fn git_common_dir() -> Result<PathBuf> {
54 git_output(&["rev-parse", "--git-common-dir"]).map(PathBuf::from)
55}
56
57pub fn is_worktree() -> Result<bool> {
59 let git_dir = git_dir()?;
60 let common_dir = git_common_dir()?;
61 Ok(git_dir != common_dir)
62}
63
64pub fn branch_exists(branch: &str) -> bool {
66 git_check(&[
67 "show-ref",
68 "--verify",
69 "--quiet",
70 &format!("refs/heads/{branch}"),
71 ])
72}
73
74pub fn remote_branch_exists(branch: &str) -> Result<bool> {
82 let output = Command::new("git")
83 .args(["ls-remote", "--exit-code", "--heads", "origin", branch])
84 .output()
85 .map_err(|e| GwError::GitCommandFailed(format!("Failed to execute git: {e}")))?;
86
87 match output.status.code() {
88 Some(0) => Ok(true),
89 Some(2) => Ok(false),
90 _ => {
91 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
92 Err(GwError::GitCommandFailed(format!(
93 "Could not query remote branch '{branch}': {stderr}"
94 )))
95 }
96 }
97}
98
99pub fn branch_base(branch: &str) -> Option<String> {
106 let value = git_output(&["config", "--get", &format!("branch.{branch}.gwBase")]).ok()?;
107 if value.is_empty() { None } else { Some(value) }
108}
109
110pub fn branch_base_sha(branch: &str) -> Option<String> {
116 let value = git_output(&["config", "--get", &format!("branch.{branch}.gwBaseSha")]).ok()?;
117 if value.is_empty() { None } else { Some(value) }
118}
119
120pub fn head_commit() -> Result<String> {
122 git_output(&["rev-parse", "HEAD"])
123}
124
125pub fn rev_parse(reference: &str) -> Result<String> {
127 git_output(&["rev-parse", "--verify", &format!("{reference}^{{commit}}")])
128}
129
130pub fn short_commit() -> Result<String> {
132 git_output(&["rev-parse", "--short", "HEAD"])
133}
134
135pub fn head_commit_message() -> Result<String> {
137 git_output(&["log", "-1", "--format=%s"])
138}
139
140pub fn has_unstaged_changes() -> bool {
142 !git_check(&["diff", "--quiet"])
143}
144
145pub fn has_staged_changes() -> bool {
147 !git_check(&["diff", "--cached", "--quiet"])
148}
149
150pub fn has_uncommitted_changes() -> bool {
152 has_unstaged_changes() || has_staged_changes()
153}
154
155pub fn has_untracked_files() -> bool {
157 git_output(&["ls-files", "--others", "--exclude-standard"])
158 .map(|s| !s.is_empty())
159 .unwrap_or(false)
160}
161
162pub fn get_upstream(branch: &str) -> Option<String> {
164 git_output(&[
165 "rev-parse",
166 "--abbrev-ref",
167 &format!("{branch}@{{upstream}}"),
168 ])
169 .ok()
170 .filter(|s| !s.is_empty())
171}
172
173pub fn has_remote_tracking(branch: &str) -> bool {
175 get_upstream(branch).is_some()
176}
177
178pub fn commit_count(from: &str, to: &str) -> Result<usize> {
180 let output = git_output(&["rev-list", "--count", &format!("{from}..{to}")])?;
181 output
182 .parse()
183 .map_err(|_| GwError::GitCommandFailed("Failed to parse commit count".to_string()))
184}
185
186pub fn unpushed_commit_count(branch: &str) -> Result<usize> {
188 let upstream = get_upstream(branch)
189 .ok_or_else(|| GwError::Other(format!("Branch '{branch}' has no upstream")))?;
190 commit_count(&upstream, branch)
191}
192
193pub fn behind_upstream_count(branch: &str) -> Result<usize> {
195 let upstream = get_upstream(branch)
196 .ok_or_else(|| GwError::Other(format!("Branch '{branch}' has no upstream")))?;
197 commit_count(branch, &upstream)
198}
199
200pub fn ref_exists(reference: &str) -> bool {
205 git_check(&[
206 "rev-parse",
207 "--verify",
208 "--quiet",
209 &format!("{reference}^{{commit}}"),
210 ])
211}
212
213pub fn is_ancestor(ancestor: &str, descendant: &str) -> bool {
219 git_check(&["merge-base", "--is-ancestor", ancestor, descendant])
220}
221
222pub fn behind_base_count(branch: &str, base: &str) -> usize {
228 commit_count(branch, base).unwrap_or(0)
229}
230
231pub fn stash_count() -> usize {
233 git_output(&["stash", "list"])
234 .map(|s| if s.is_empty() { 0 } else { s.lines().count() })
235 .unwrap_or(0)
236}
237
238pub fn get_latest_stash_message() -> Option<String> {
240 git_output(&["stash", "list", "-1", "--format=%gs"])
241 .ok()
242 .filter(|s| !s.is_empty())
243}
244
245pub fn has_commits_to_undo() -> bool {
247 git_check(&["rev-parse", "HEAD~1"])
248}
249
250pub fn current_dir_name() -> Result<String> {
252 std::env::current_dir()
253 .map_err(GwError::Io)?
254 .file_name()
255 .and_then(|s| s.to_str())
256 .map(String::from)
257 .ok_or_else(|| GwError::Other("Could not determine current directory name".to_string()))
258}
259
260pub fn get_default_remote_branch() -> Result<String> {
262 if remote_branch_exists("main").unwrap_or(false) {
265 Ok("origin/main".to_string())
266 } else if remote_branch_exists("master").unwrap_or(false) {
267 Ok("origin/master".to_string())
268 } else {
269 Err(GwError::Other(
270 "Neither origin/main nor origin/master exists".to_string(),
271 ))
272 }
273}
274
275pub fn default_branch_name() -> Result<String> {
281 if let Ok(remote) = get_default_remote_branch() {
282 return Ok(remote
283 .strip_prefix("origin/")
284 .unwrap_or(&remote)
285 .to_string());
286 }
287 if branch_exists("main") {
288 return Ok("main".to_string());
289 }
290 if branch_exists("master") {
291 return Ok("master".to_string());
292 }
293 Err(GwError::Other(
294 "Could not determine the default branch (no origin/main, origin/master, or local main/master)"
295 .to_string(),
296 ))
297}
298
299pub fn is_detached_head() -> bool {
301 current_branch().map(|b| b == "HEAD").unwrap_or(false)
302}
303
304pub fn repo_root() -> Result<PathBuf> {
306 git_output(&["rev-parse", "--show-toplevel"]).map(PathBuf::from)
307}