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("<", "<").replace(">", ">"))
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}