anodizer_core/git/commits/
query.rs1use super::*;
2use anyhow::Result;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone)]
6pub struct Commit {
7 pub hash: String,
8 pub short_hash: String,
9 pub message: String,
10 pub author_name: String,
11 pub author_email: String,
12 pub body: String,
15}
16
17pub fn parse_commit_output(output: &str) -> Vec<Commit> {
28 if output.is_empty() {
29 return vec![];
30 }
31 output
32 .split('\x1e')
33 .filter(|record| !record.trim().is_empty())
34 .filter_map(|record| {
35 let fields: Vec<&str> = record.split('\x1f').collect();
36 if fields.len() >= 5 {
37 Some(Commit {
38 hash: fields[0].trim().to_string(),
39 short_hash: fields[1].to_string(),
40 message: fields[2].to_string(),
41 author_name: fields[3].to_string(),
42 author_email: fields[4].to_string(),
43 body: fields.get(5).unwrap_or(&"").trim().to_string(),
44 })
45 } else {
46 None
47 }
48 })
49 .collect()
50}
51
52pub(super) fn cwd_or_dot() -> PathBuf {
53 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
54}
55
56pub fn get_commits_between(from: &str, to: &str, path_filter: Option<&str>) -> Result<Vec<Commit>> {
58 get_commits_between_in(&cwd_or_dot(), from, to, path_filter)
59}
60
61pub fn get_commits_between_in(
63 cwd: &Path,
64 from: &str,
65 to: &str,
66 path_filter: Option<&str>,
67) -> Result<Vec<Commit>> {
68 get_commits_between_paths_in(
69 cwd,
70 from,
71 to,
72 &path_filter
73 .into_iter()
74 .map(String::from)
75 .collect::<Vec<_>>(),
76 )
77}
78
79pub fn get_commits_between_paths(from: &str, to: &str, paths: &[String]) -> Result<Vec<Commit>> {
81 get_commits_between_paths_in(&cwd_or_dot(), from, to, paths)
82}
83
84pub fn get_commits_between_paths_in(
86 cwd: &Path,
87 from: &str,
88 to: &str,
89 paths: &[String],
90) -> Result<Vec<Commit>> {
91 let range = format!("{}..{}", from, to);
92 let mut args = vec![
93 "-c".to_string(),
94 "log.showSignature=false".to_string(),
95 "log".to_string(),
96 "--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
97 range,
98 ];
99 if !paths.is_empty() {
100 args.push("--".to_string());
101 for p in paths {
102 args.push(p.clone());
103 }
104 }
105 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
106 let output = git_output_in(cwd, &arg_refs)?;
107 Ok(parse_commit_output(&output))
108}
109
110pub fn get_all_commits(path_filter: Option<&str>) -> Result<Vec<Commit>> {
113 get_all_commits_in(&cwd_or_dot(), path_filter)
114}
115
116pub fn get_all_commits_in(cwd: &Path, path_filter: Option<&str>) -> Result<Vec<Commit>> {
118 get_all_commits_paths_in(
119 cwd,
120 &path_filter
121 .into_iter()
122 .map(String::from)
123 .collect::<Vec<_>>(),
124 )
125}
126
127pub fn get_all_commits_paths(paths: &[String]) -> Result<Vec<Commit>> {
129 get_all_commits_paths_in(&cwd_or_dot(), paths)
130}
131
132pub fn get_all_commits_paths_in(cwd: &Path, paths: &[String]) -> Result<Vec<Commit>> {
134 let mut args = vec![
135 "-c".to_string(),
136 "log.showSignature=false".to_string(),
137 "log".to_string(),
138 "--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
139 "HEAD".to_string(),
140 ];
141 if !paths.is_empty() {
142 args.push("--".to_string());
143 for p in paths {
144 args.push(p.clone());
145 }
146 }
147 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
148 let output = git_output_in(cwd, &arg_refs)?;
149 Ok(parse_commit_output(&output))
150}
151
152#[derive(Debug, Clone)]
158pub struct CommitWithFiles {
159 pub commit: Commit,
161 pub files: Vec<String>,
163}
164
165pub fn parse_commit_output_with_files(output: &str) -> Vec<CommitWithFiles> {
182 if output.is_empty() {
183 return vec![];
184 }
185 let segments: Vec<&str> = output.split('\x1e').collect();
186 let mut out: Vec<CommitWithFiles> = Vec::new();
187 for (idx, seg) in segments.iter().enumerate() {
193 let metadata = if idx == 0 {
201 seg.trim_start_matches(['\n', '\r']).to_string()
202 } else {
203 let lines: Vec<&str> = seg.split('\n').collect();
204 match lines.iter().position(|line| line.contains('\x1f')) {
205 Some(start) => lines[start..].join("\n"),
206 None => String::new(),
207 }
208 };
209 if metadata.trim().is_empty() {
210 continue;
211 }
212 let commits = parse_commit_output(&metadata);
213 let Some(commit) = commits.into_iter().next() else {
214 continue;
215 };
216 let files = match segments.get(idx + 1) {
219 Some(next) => next
220 .split('\n')
221 .map(str::trim)
222 .take_while(|line| !line.contains('\x1f'))
223 .filter(|line| !line.is_empty())
224 .map(str::to_string)
225 .collect(),
226 None => Vec::new(),
227 };
228 out.push(CommitWithFiles { commit, files });
229 }
230 out
231}
232
233pub fn get_commits_between_paths_with_files_in(
237 cwd: &Path,
238 from: &str,
239 to: &str,
240 paths: &[String],
241) -> Result<Vec<CommitWithFiles>> {
242 let range = format!("{}..{}", from, to);
243 let mut args = vec![
244 "-c".to_string(),
245 "log.showSignature=false".to_string(),
246 "log".to_string(),
247 "--name-only".to_string(),
248 "--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
249 range,
250 ];
251 if !paths.is_empty() {
252 args.push("--".to_string());
253 for p in paths {
254 args.push(p.clone());
255 }
256 }
257 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
258 let output = git_output_in(cwd, &arg_refs)?;
259 Ok(parse_commit_output_with_files(&output))
260}
261
262pub fn get_all_commits_paths_with_files_in(
264 cwd: &Path,
265 paths: &[String],
266) -> Result<Vec<CommitWithFiles>> {
267 let mut args = vec![
268 "-c".to_string(),
269 "log.showSignature=false".to_string(),
270 "log".to_string(),
271 "--name-only".to_string(),
272 "--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
273 "HEAD".to_string(),
274 ];
275 if !paths.is_empty() {
276 args.push("--".to_string());
277 for p in paths {
278 args.push(p.clone());
279 }
280 }
281 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
282 let output = git_output_in(cwd, &arg_refs)?;
283 Ok(parse_commit_output_with_files(&output))
284}
285
286pub fn get_commits_reachable_paths_in(
291 cwd: &Path,
292 rev: &str,
293 paths: &[String],
294) -> Result<Vec<Commit>> {
295 let mut args = vec![
296 "-c".to_string(),
297 "log.showSignature=false".to_string(),
298 "log".to_string(),
299 "--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
300 rev.to_string(),
301 ];
302 if !paths.is_empty() {
303 args.push("--".to_string());
304 for p in paths {
305 args.push(p.clone());
306 }
307 }
308 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
309 let output = git_output_in(cwd, &arg_refs)?;
310 Ok(parse_commit_output(&output))
311}
312
313pub fn get_commits_reachable_paths_with_files_in(
315 cwd: &Path,
316 rev: &str,
317 paths: &[String],
318) -> Result<Vec<CommitWithFiles>> {
319 let mut args = vec![
320 "-c".to_string(),
321 "log.showSignature=false".to_string(),
322 "log".to_string(),
323 "--name-only".to_string(),
324 "--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
325 rev.to_string(),
326 ];
327 if !paths.is_empty() {
328 args.push("--".to_string());
329 for p in paths {
330 args.push(p.clone());
331 }
332 }
333 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
334 let output = git_output_in(cwd, &arg_refs)?;
335 Ok(parse_commit_output_with_files(&output))
336}
337
338pub fn get_last_commit_messages(count: usize) -> Result<Vec<String>> {
340 get_last_commit_messages_in(&cwd_or_dot(), count)
341}
342
343pub fn get_last_commit_messages_in(cwd: &Path, count: usize) -> Result<Vec<String>> {
345 let output = git_output_in(
346 cwd,
347 &[
348 "-c",
349 "log.showSignature=false",
350 "log",
351 &format!("-{count}"),
352 "--pretty=format:%s",
353 ],
354 )?;
355 Ok(output.lines().map(str::to_string).collect())
356}
357
358pub fn get_commit_messages_between(from: &str, to: &str) -> Result<Vec<String>> {
360 get_commit_messages_between_in(&cwd_or_dot(), from, to)
361}
362
363pub fn get_commit_messages_between_in(cwd: &Path, from: &str, to: &str) -> Result<Vec<String>> {
365 let output = git_output_in(
366 cwd,
367 &[
368 "-c",
369 "log.showSignature=false",
370 "log",
371 "--pretty=format:%s",
372 &format!("{from}..{to}"),
373 ],
374 )?;
375 Ok(output.lines().map(str::to_string).collect())
376}