git: Work around windows command length limit message fetching (#39115)

Lukas Wirth created

Release Notes:

- Fix git blame failing on windows for files with lots of blame entries

Change summary

crates/git/src/commit.rs | 53 ++++++++++++++++++++++++++++-------------
1 file changed, 36 insertions(+), 17 deletions(-)

Detailed changes

crates/git/src/commit.rs 🔗

@@ -8,34 +8,53 @@ pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<Hash
         return Ok(HashMap::default());
     }
 
-    const MARKER: &str = "<MARKER>";
+    let output = if cfg!(windows) {
+        // Windows has a maximum invocable command length, so we chunk the input.
+        // 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
+        const MAX_CMD_LENGTH: usize = 30000;
+        // 40 bytes of hash, 2 quotes and a separating space
+        const SHA_LENGTH: usize = 40 + 2 + 1;
+        const MAX_ENTRIES_PER_INVOCATION: usize = MAX_CMD_LENGTH / SHA_LENGTH;
+
+        let mut result = vec![];
+        for shas in shas.chunks(MAX_ENTRIES_PER_INVOCATION) {
+            let partial = get_messages_impl(working_directory, shas).await?;
+            result.extend(partial);
+        }
+        result
+    } else {
+        get_messages_impl(working_directory, shas).await?
+    };
+
+    Ok(shas
+        .iter()
+        .cloned()
+        .zip(output)
+        .collect::<HashMap<Oid, String>>())
+}
 
-    let output = util::command::new_smol_command("git")
-        .current_dir(working_directory)
+async fn get_messages_impl(working_directory: &Path, shas: &[Oid]) -> Result<Vec<String>> {
+    const MARKER: &str = "<MARKER>";
+    let mut cmd = util::command::new_smol_command("git");
+    cmd.current_dir(working_directory)
         .arg("show")
         .arg("-s")
         .arg(format!("--format=%B{}", MARKER))
-        .args(shas.iter().map(ToString::to_string))
+        .args(shas.iter().map(ToString::to_string));
+    let output = cmd
         .output()
         .await
-        .context("starting git blame process")?;
-
+        .with_context(|| format!("starting git blame process: {:?}", cmd))?;
     anyhow::ensure!(
         output.status.success(),
         "'git show' failed with error {:?}",
         output.status
     );
-
-    Ok(shas
-        .iter()
-        .cloned()
-        .zip(
-            String::from_utf8_lossy(&output.stdout)
-                .trim()
-                .split_terminator(MARKER)
-                .map(|str| str.trim().replace("<", "&lt;").replace(">", "&gt;")),
-        )
-        .collect::<HashMap<Oid, String>>())
+    Ok(String::from_utf8_lossy(&output.stdout)
+        .trim()
+        .split_terminator(MARKER)
+        .map(|str| str.trim().replace("<", "&lt;").replace(">", "&gt;"))
+        .collect::<Vec<_>>())
 }
 
 /// Parse the output of `git diff --name-status -z`