1use crate::{Oid, status::StatusCode};
2use anyhow::{Context as _, Result};
3use collections::HashMap;
4use std::path::Path;
5
6pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
7 if shas.is_empty() {
8 return Ok(HashMap::default());
9 }
10
11 let output = if cfg!(windows) {
12 // Windows has a maximum invocable command length, so we chunk the input.
13 // Actual max is 32767, but we leave some room for the rest of the command as we aren't in precise control of what std might do here
14 const MAX_CMD_LENGTH: usize = 30000;
15 // 40 bytes of hash, 2 quotes and a separating space
16 const SHA_LENGTH: usize = 40 + 2 + 1;
17 const MAX_ENTRIES_PER_INVOCATION: usize = MAX_CMD_LENGTH / SHA_LENGTH;
18
19 let mut result = vec![];
20 for shas in shas.chunks(MAX_ENTRIES_PER_INVOCATION) {
21 let partial = get_messages_impl(working_directory, shas).await?;
22 result.extend(partial);
23 }
24 result
25 } else {
26 get_messages_impl(working_directory, shas).await?
27 };
28
29 Ok(shas
30 .iter()
31 .cloned()
32 .zip(output)
33 .collect::<HashMap<Oid, String>>())
34}
35
36async fn get_messages_impl(working_directory: &Path, shas: &[Oid]) -> Result<Vec<String>> {
37 const MARKER: &str = "<MARKER>";
38 let mut cmd = util::command::new_smol_command("git");
39 cmd.current_dir(working_directory)
40 .arg("show")
41 .arg("-s")
42 .arg(format!("--format=%B{}", MARKER))
43 .args(shas.iter().map(ToString::to_string));
44 let output = cmd
45 .output()
46 .await
47 .with_context(|| format!("starting git blame process: {:?}", cmd))?;
48 anyhow::ensure!(
49 output.status.success(),
50 "'git show' failed with error {:?}",
51 output.status
52 );
53 Ok(String::from_utf8_lossy(&output.stdout)
54 .trim()
55 .split_terminator(MARKER)
56 .map(|str| str.trim().replace("<", "<").replace(">", ">"))
57 .collect::<Vec<_>>())
58}
59
60/// Parse the output of `git diff --name-status -z`
61#[profiling::function]
62pub fn parse_git_diff_name_status(content: &str) -> impl Iterator<Item = (&str, StatusCode)> {
63 let mut parts = content.split('\0');
64 std::iter::from_fn(move || {
65 loop {
66 let status_str = parts.next()?;
67 let path = parts.next()?;
68 let status = match status_str {
69 "M" => StatusCode::Modified,
70 "A" => StatusCode::Added,
71 "D" => StatusCode::Deleted,
72 _ => continue,
73 };
74 return Some((path, status));
75 }
76 })
77}
78
79#[cfg(test)]
80mod tests {
81
82 use super::*;
83
84 #[test]
85 fn test_parse_git_diff_name_status() {
86 let input = concat!(
87 "M\x00Cargo.lock\x00",
88 "M\x00crates/project/Cargo.toml\x00",
89 "M\x00crates/project/src/buffer_store.rs\x00",
90 "D\x00crates/project/src/git.rs\x00",
91 "A\x00crates/project/src/git_store.rs\x00",
92 "A\x00crates/project/src/git_store/git_traversal.rs\x00",
93 "M\x00crates/project/src/project.rs\x00",
94 "M\x00crates/project/src/worktree_store.rs\x00",
95 "M\x00crates/project_panel/src/project_panel.rs\x00",
96 );
97
98 let output = parse_git_diff_name_status(input).collect::<Vec<_>>();
99 assert_eq!(
100 output,
101 &[
102 ("Cargo.lock", StatusCode::Modified),
103 ("crates/project/Cargo.toml", StatusCode::Modified),
104 ("crates/project/src/buffer_store.rs", StatusCode::Modified),
105 ("crates/project/src/git.rs", StatusCode::Deleted),
106 ("crates/project/src/git_store.rs", StatusCode::Added),
107 (
108 "crates/project/src/git_store/git_traversal.rs",
109 StatusCode::Added,
110 ),
111 ("crates/project/src/project.rs", StatusCode::Modified),
112 ("crates/project/src/worktree_store.rs", StatusCode::Modified),
113 (
114 "crates/project_panel/src/project_panel.rs",
115 StatusCode::Modified
116 ),
117 ]
118 );
119 }
120}