1use anyhow::{Context, Result};
8use std::path::Path;
9use std::process::Command;
10
11#[derive(Debug, Clone)]
13pub struct GitState {
14 pub branch: String,
16 pub commit: String,
18 pub dirty: bool,
20}
21
22pub fn is_git_repo(root: impl AsRef<Path>) -> bool {
24 root.as_ref().join(".git").exists()
25}
26
27pub fn get_current_branch(root: impl AsRef<Path>) -> Result<String> {
31 let output = Command::new("git")
32 .arg("-C")
33 .arg(root.as_ref())
34 .args(["rev-parse", "--abbrev-ref", "HEAD"])
35 .output()
36 .context("Failed to execute git rev-parse")?;
37
38 if !output.status.success() {
39 anyhow::bail!(
40 "git rev-parse failed: {}",
41 String::from_utf8_lossy(&output.stderr)
42 );
43 }
44
45 let branch = String::from_utf8(output.stdout)
46 .context("Invalid UTF-8 in branch name")?
47 .trim()
48 .to_string();
49
50 Ok(branch)
51}
52
53pub fn get_current_commit(root: impl AsRef<Path>) -> Result<String> {
57 let output = Command::new("git")
58 .arg("-C")
59 .arg(root.as_ref())
60 .args(["rev-parse", "HEAD"])
61 .output()
62 .context("Failed to execute git rev-parse HEAD")?;
63
64 if !output.status.success() {
65 anyhow::bail!(
66 "git rev-parse HEAD failed: {}",
67 String::from_utf8_lossy(&output.stderr)
68 );
69 }
70
71 let commit = String::from_utf8(output.stdout)
72 .context("Invalid UTF-8 in commit SHA")?
73 .trim()
74 .to_string();
75
76 Ok(commit)
77}
78
79pub fn has_uncommitted_changes(root: impl AsRef<Path>) -> Result<bool> {
84 let output = Command::new("git")
85 .arg("-C")
86 .arg(root.as_ref())
87 .args(["status", "--porcelain"])
88 .output()
89 .context("Failed to execute git status")?;
90
91 if !output.status.success() {
92 anyhow::bail!(
93 "git status failed: {}",
94 String::from_utf8_lossy(&output.stderr)
95 );
96 }
97
98 let has_changes = !output.stdout.is_empty();
101
102 Ok(has_changes)
103}
104
105pub fn get_git_state(root: impl AsRef<Path>) -> Result<GitState> {
110 let root = root.as_ref();
111
112 if !is_git_repo(root) {
113 anyhow::bail!("Not a git repository");
114 }
115
116 let branch = get_current_branch(root)?;
117 let commit = get_current_commit(root)?;
118 let dirty = has_uncommitted_changes(root)?;
119
120 Ok(GitState {
121 branch,
122 commit,
123 dirty,
124 })
125}
126
127pub fn get_git_state_optional(root: impl AsRef<Path>) -> Result<Option<GitState>> {
131 if !is_git_repo(&root) {
132 return Ok(None);
133 }
134
135 match get_git_state(root) {
136 Ok(state) => Ok(Some(state)),
137 Err(e) => {
138 log::warn!("Failed to get git state: {}", e);
139 Ok(None)
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_is_git_repo() {
150 assert!(is_git_repo("."));
152
153 assert!(!is_git_repo("/tmp"));
155 }
156
157 #[test]
158 fn test_get_current_branch() {
159 let branch = get_current_branch(".").unwrap();
161 assert!(!branch.is_empty());
162 log::info!("Current branch: {}", branch);
163 }
164
165 #[test]
166 fn test_get_current_commit() {
167 let commit = get_current_commit(".").unwrap();
169 assert_eq!(commit.len(), 40);
170 assert!(commit.chars().all(|c| c.is_ascii_hexdigit()));
171 log::info!("Current commit: {}", commit);
172 }
173
174 #[test]
175 fn test_has_uncommitted_changes() {
176 let has_changes = has_uncommitted_changes(".").unwrap();
178 log::info!("Has uncommitted changes: {}", has_changes);
179 }
180
181 #[test]
182 fn test_get_git_state() {
183 let state = get_git_state(".").unwrap();
184 assert!(!state.branch.is_empty());
185 assert_eq!(state.commit.len(), 40);
186 log::info!("Git state: {:?}", state);
187 }
188}