commit.rs

  1use crate::{
  2    BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url,
  3    status::StatusCode,
  4};
  5use anyhow::{Context as _, Result};
  6use collections::HashMap;
  7use gpui::SharedString;
  8use std::{path::Path, sync::Arc};
  9
 10#[derive(Clone, Debug, Default)]
 11pub struct ParsedCommitMessage {
 12    pub message: SharedString,
 13    pub permalink: Option<url::Url>,
 14    pub pull_request: Option<crate::hosting_provider::PullRequest>,
 15    pub remote: Option<GitRemote>,
 16}
 17
 18impl ParsedCommitMessage {
 19    pub fn parse(
 20        sha: String,
 21        message: String,
 22        remote_url: Option<&str>,
 23        provider_registry: Option<Arc<GitHostingProviderRegistry>>,
 24    ) -> Self {
 25        if let Some((hosting_provider, remote)) = provider_registry
 26            .and_then(|reg| remote_url.and_then(|url| parse_git_remote_url(reg, url)))
 27        {
 28            let pull_request = hosting_provider.extract_pull_request(&remote, &message);
 29            Self {
 30                message: message.into(),
 31                permalink: Some(
 32                    hosting_provider
 33                        .build_commit_permalink(&remote, BuildCommitPermalinkParams { sha: &sha }),
 34                ),
 35                pull_request,
 36                remote: Some(GitRemote {
 37                    host: hosting_provider,
 38                    owner: remote.owner.into(),
 39                    repo: remote.repo.into(),
 40                }),
 41            }
 42        } else {
 43            Self {
 44                message: message.into(),
 45                ..Default::default()
 46            }
 47        }
 48    }
 49}
 50
 51pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
 52    if shas.is_empty() {
 53        return Ok(HashMap::default());
 54    }
 55
 56    let output = if cfg!(windows) {
 57        // Windows has a maximum invocable command length, so we chunk the input.
 58        // 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
 59        const MAX_CMD_LENGTH: usize = 30000;
 60        // 40 bytes of hash, 2 quotes and a separating space
 61        const SHA_LENGTH: usize = 40 + 2 + 1;
 62        const MAX_ENTRIES_PER_INVOCATION: usize = MAX_CMD_LENGTH / SHA_LENGTH;
 63
 64        let mut result = vec![];
 65        for shas in shas.chunks(MAX_ENTRIES_PER_INVOCATION) {
 66            let partial = get_messages_impl(working_directory, shas).await?;
 67            result.extend(partial);
 68        }
 69        result
 70    } else {
 71        get_messages_impl(working_directory, shas).await?
 72    };
 73
 74    Ok(shas
 75        .iter()
 76        .cloned()
 77        .zip(output)
 78        .collect::<HashMap<Oid, String>>())
 79}
 80
 81async fn get_messages_impl(working_directory: &Path, shas: &[Oid]) -> Result<Vec<String>> {
 82    const MARKER: &str = "<MARKER>";
 83    let mut cmd = util::command::new_smol_command("git");
 84    cmd.current_dir(working_directory)
 85        .arg("show")
 86        .arg("-s")
 87        .arg(format!("--format=%B{}", MARKER))
 88        .args(shas.iter().map(ToString::to_string));
 89    let output = cmd
 90        .output()
 91        .await
 92        .with_context(|| format!("starting git blame process: {:?}", cmd))?;
 93    anyhow::ensure!(
 94        output.status.success(),
 95        "'git show' failed with error {:?}",
 96        output.status
 97    );
 98    Ok(String::from_utf8_lossy(&output.stdout)
 99        .trim()
100        .split_terminator(MARKER)
101        .map(|str| str.trim().replace("<", "&lt;").replace(">", "&gt;"))
102        .collect::<Vec<_>>())
103}
104
105/// Parse the output of `git diff --name-status -z`
106pub fn parse_git_diff_name_status(content: &str) -> impl Iterator<Item = (&str, StatusCode)> {
107    let mut parts = content.split('\0');
108    std::iter::from_fn(move || {
109        loop {
110            let status_str = parts.next()?;
111            let path = parts.next()?;
112            let status = match status_str {
113                "M" => StatusCode::Modified,
114                "A" => StatusCode::Added,
115                "D" => StatusCode::Deleted,
116                _ => continue,
117            };
118            return Some((path, status));
119        }
120    })
121}
122
123#[cfg(test)]
124mod tests {
125
126    use super::*;
127
128    #[test]
129    fn test_parse_git_diff_name_status() {
130        let input = concat!(
131            "M\x00Cargo.lock\x00",
132            "M\x00crates/project/Cargo.toml\x00",
133            "M\x00crates/project/src/buffer_store.rs\x00",
134            "D\x00crates/project/src/git.rs\x00",
135            "A\x00crates/project/src/git_store.rs\x00",
136            "A\x00crates/project/src/git_store/git_traversal.rs\x00",
137            "M\x00crates/project/src/project.rs\x00",
138            "M\x00crates/project/src/worktree_store.rs\x00",
139            "M\x00crates/project_panel/src/project_panel.rs\x00",
140        );
141
142        let output = parse_git_diff_name_status(input).collect::<Vec<_>>();
143        assert_eq!(
144            output,
145            &[
146                ("Cargo.lock", StatusCode::Modified),
147                ("crates/project/Cargo.toml", StatusCode::Modified),
148                ("crates/project/src/buffer_store.rs", StatusCode::Modified),
149                ("crates/project/src/git.rs", StatusCode::Deleted),
150                ("crates/project/src/git_store.rs", StatusCode::Added),
151                (
152                    "crates/project/src/git_store/git_traversal.rs",
153                    StatusCode::Added,
154                ),
155                ("crates/project/src/project.rs", StatusCode::Modified),
156                ("crates/project/src/worktree_store.rs", StatusCode::Modified),
157                (
158                    "crates/project_panel/src/project_panel.rs",
159                    StatusCode::Modified
160                ),
161            ]
162        );
163    }
164}