commit.rs

  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("<", "&lt;").replace(">", "&gt;"))
 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}