1use std::env;
18use std::path::PathBuf;
19use std::process::{Command, Stdio};
20
21pub struct Commit {
22 pub hash: String,
23 pub title: String,
24}
25
26#[must_use]
27pub fn is_git_in_path() -> bool {
28 Command::new("git")
29 .arg("--version")
30 .stdin(Stdio::null())
31 .stdout(Stdio::null())
32 .stderr(Stdio::null())
33 .status()
34 .is_ok()
35}
36
37#[must_use]
38pub fn find_git_directory() -> Option<PathBuf> {
39 let mut current_dir = env::current_dir().ok()?;
40
41 loop {
42 let git_dir = current_dir.join(".git");
43 if git_dir.is_dir() {
44 return Some(git_dir);
45 }
46 if !current_dir.pop() {
47 break;
48 }
49 }
50
51 None
52}
53
54#[must_use]
55pub fn current_commit_hash() -> Option<String> {
56 let output = Command::new("git")
57 .arg("rev-parse")
58 .arg("--verify")
59 .arg("--quiet")
60 .arg("HEAD^{commit}")
61 .output();
62
63 if let Ok(output) = output {
64 if output.status.success() {
65 let hash = String::from_utf8_lossy(&output.stdout).trim().to_owned();
66 return Some(hash);
67 }
68 }
69
70 None
71}
72
73#[must_use]
74pub fn current_branch() -> Option<String> {
75 let output = Command::new("git")
76 .arg("symbolic-ref")
77 .arg("--short")
78 .arg("--quiet")
79 .arg("HEAD")
80 .output();
81
82 if let Ok(output) = output {
83 if output.status.success() {
84 let branch = String::from_utf8_lossy(&output.stdout).trim().to_owned();
85 return Some(branch);
86 }
87 }
88
89 None
90}
91
92#[must_use]
93pub fn ref_to_commit_hash(ref_: &str) -> Option<String> {
94 let output = Command::new("git")
95 .arg("rev-parse")
96 .arg("--verify")
97 .arg("--quiet")
98 .arg("--end-of-options")
99 .arg(format!("{ref_}^{{commit}}"))
100 .output();
101
102 if let Ok(output) = output {
103 if output.status.success() {
104 let hash = String::from_utf8_lossy(&output.stdout).trim().to_owned();
105 return Some(hash);
106 }
107 }
108
109 None
110}
111
112#[cfg(not(tarpaulin_include))] #[must_use]
114pub fn history_up_to_commit(commit: &str) -> Vec<Commit> {
115 let output = Command::new("git")
116 .arg("rev-list")
117 .arg("--first-parent")
118 .arg("--format=%H %s")
119 .arg("--no-commit-header")
120 .arg("--reverse")
121 .arg(commit)
122 .output();
123
124 if let Ok(output) = output {
125 if output.status.success() {
126 let commits: Vec<Commit> = String::from_utf8_lossy(&output.stdout)
127 .lines()
128 .filter_map(|line| {
129 let pieces = line.split_once(' ')?;
130 let hash = String::from(pieces.0);
131 let title = String::from(pieces.1);
132 Some(Commit { hash, title })
133 })
134 .collect();
135 return commits;
136 }
137 }
138
139 Vec::new()
141}
142
143#[cfg(not(tarpaulin_include))] #[must_use]
145pub fn checkout(commit: &str) -> bool {
146 let status = Command::new("git")
147 .arg("checkout")
148 .arg(commit)
149 .stdin(Stdio::null())
150 .stdout(Stdio::null())
151 .stderr(Stdio::null())
152 .status();
153
154 let Ok(status) = status else {
155 return false;
156 };
157
158 status.success()
159}
160
161#[cfg(not(tarpaulin_include))] #[must_use]
163pub fn is_working_directory_clean() -> bool {
164 let output = Command::new("git")
165 .arg("status")
166 .arg("--untracked-files=no")
167 .arg("--porcelain")
168 .output();
169
170 let Ok(output) = output else {
171 return false;
172 };
173
174 String::from_utf8_lossy(&output.stdout).trim().is_empty()
175}
176
177#[cfg(not(tarpaulin_include))] #[must_use]
179pub fn stash() -> bool {
180 let status = Command::new("git")
181 .arg("stash")
182 .stdin(Stdio::null())
183 .stdout(Stdio::null())
184 .stderr(Stdio::null())
185 .status();
186
187 let Ok(status) = status else {
188 return false;
189 };
190
191 status.success()
192}