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 git_dir() -> Result<PathBuf> {
44 git_output(&["rev-parse", "--git-dir"]).map(PathBuf::from)
45}
46
47pub fn git_common_dir() -> Result<PathBuf> {
49 git_output(&["rev-parse", "--git-common-dir"]).map(PathBuf::from)
50}
51
52pub fn is_worktree() -> Result<bool> {
54 let git_dir = git_dir()?;
55 let common_dir = git_common_dir()?;
56 Ok(git_dir != common_dir)
57}
58
59pub fn branch_exists(branch: &str) -> bool {
61 git_check(&[
62 "show-ref",
63 "--verify",
64 "--quiet",
65 &format!("refs/heads/{branch}"),
66 ])
67}
68
69pub fn remote_branch_exists(branch: &str) -> bool {
71 git_check(&["ls-remote", "--exit-code", "--heads", "origin", branch])
72}
73
74pub fn head_commit() -> Result<String> {
76 git_output(&["rev-parse", "HEAD"])
77}
78
79pub fn short_commit() -> Result<String> {
81 git_output(&["rev-parse", "--short", "HEAD"])
82}
83
84pub fn head_commit_message() -> Result<String> {
86 git_output(&["log", "-1", "--format=%s"])
87}
88
89pub fn has_unstaged_changes() -> bool {
91 !git_check(&["diff", "--quiet"])
92}
93
94pub fn has_staged_changes() -> bool {
96 !git_check(&["diff", "--cached", "--quiet"])
97}
98
99pub fn has_uncommitted_changes() -> bool {
101 has_unstaged_changes() || has_staged_changes()
102}
103
104pub fn has_untracked_files() -> bool {
106 git_output(&["ls-files", "--others", "--exclude-standard"])
107 .map(|s| !s.is_empty())
108 .unwrap_or(false)
109}
110
111pub fn get_upstream(branch: &str) -> Option<String> {
113 git_output(&[
114 "rev-parse",
115 "--abbrev-ref",
116 &format!("{branch}@{{upstream}}"),
117 ])
118 .ok()
119 .filter(|s| !s.is_empty())
120}
121
122pub fn has_remote_tracking(branch: &str) -> bool {
124 get_upstream(branch).is_some()
125}
126
127pub fn commit_count(from: &str, to: &str) -> Result<usize> {
129 let output = git_output(&["rev-list", "--count", &format!("{from}..{to}")])?;
130 output
131 .parse()
132 .map_err(|_| GwError::GitCommandFailed("Failed to parse commit count".to_string()))
133}
134
135pub fn unpushed_commit_count(branch: &str) -> Result<usize> {
137 let upstream = get_upstream(branch)
138 .ok_or_else(|| GwError::Other(format!("Branch '{branch}' has no upstream")))?;
139 commit_count(&upstream, branch)
140}
141
142pub fn behind_upstream_count(branch: &str) -> Result<usize> {
144 let upstream = get_upstream(branch)
145 .ok_or_else(|| GwError::Other(format!("Branch '{branch}' has no upstream")))?;
146 commit_count(branch, &upstream)
147}
148
149pub fn stash_count() -> usize {
151 git_output(&["stash", "list"])
152 .map(|s| if s.is_empty() { 0 } else { s.lines().count() })
153 .unwrap_or(0)
154}
155
156pub fn get_latest_stash_message() -> Option<String> {
158 git_output(&["stash", "list", "-1", "--format=%gs"])
159 .ok()
160 .filter(|s| !s.is_empty())
161}
162
163pub fn has_commits_to_undo() -> bool {
165 git_check(&["rev-parse", "HEAD~1"])
166}
167
168pub fn current_dir_name() -> Result<String> {
170 std::env::current_dir()
171 .map_err(GwError::Io)?
172 .file_name()
173 .and_then(|s| s.to_str())
174 .map(String::from)
175 .ok_or_else(|| GwError::Other("Could not determine current directory name".to_string()))
176}