Skip to repository content327 lines · 11.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:27:30.549Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
commit.rs
1use crate::{
2 BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url,
3 repository::GitBinary, status::StatusCode,
4};
5use anyhow::{Context as _, Result};
6use collections::{HashMap, HashSet};
7use gpui::SharedString;
8use std::{str::FromStr, 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(crate) async fn get_messages(git: &GitBinary, 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(git, shas).await?;
67 result.extend(partial);
68 }
69 result
70 } else {
71 get_messages_impl(git, shas).await?
72 };
73
74 Ok(shas
75 .iter()
76 .cloned()
77 .zip(output)
78 .collect::<HashMap<Oid, String>>())
79}
80
81pub(crate) async fn get_tag_names(
82 git: &GitBinary,
83 shas: &[Oid],
84) -> Result<HashMap<Oid, Vec<String>>> {
85 if shas.is_empty() {
86 return Ok(HashMap::default());
87 }
88
89 let output = git
90 .build_command(&[
91 "for-each-ref",
92 "refs/tags",
93 "--sort=-creatordate",
94 "--format=%(objectname)%00%(*objectname)%00%(refname:short)",
95 ])
96 .output()
97 .await
98 .context("starting git for-each-ref process")?;
99 anyhow::ensure!(
100 output.status.success(),
101 "'git for-each-ref' failed with error {:?}",
102 String::from_utf8_lossy(&output.stderr)
103 );
104
105 Ok(parse_tag_names(
106 &String::from_utf8_lossy(&output.stdout),
107 shas,
108 ))
109}
110
111fn parse_tag_names(output: &str, shas: &[Oid]) -> HashMap<Oid, Vec<String>> {
112 let shas = shas.iter().copied().collect::<HashSet<_>>();
113 let mut result = HashMap::<Oid, Vec<String>>::default();
114
115 for line in output.lines() {
116 let mut fields = line.split('\0');
117 let object_sha = fields.next();
118 let peeled_sha = fields.next().filter(|sha| !sha.is_empty());
119 let Some(sha) = peeled_sha
120 .or(object_sha)
121 .and_then(|sha| Oid::from_str(sha).ok())
122 else {
123 continue;
124 };
125 let Some(tag_name) = fields.next().filter(|tag_name| !tag_name.is_empty()) else {
126 continue;
127 };
128 result.entry(sha).or_default().push(tag_name.to_string());
129 }
130
131 result.retain(|sha, _| shas.contains(sha));
132 result
133}
134
135async fn get_messages_impl(git: &GitBinary, shas: &[Oid]) -> Result<Vec<String>> {
136 const MARKER: &str = "<MARKER>";
137 let output = git
138 .build_command(&["show"])
139 .arg("-s")
140 .arg(format!("--format=%B{}", MARKER))
141 .args(shas.iter().map(ToString::to_string))
142 .output()
143 .await
144 .context("starting git show process")?;
145 anyhow::ensure!(
146 output.status.success(),
147 "'git show' failed with error {:?}",
148 String::from_utf8_lossy(&output.stderr)
149 );
150 Ok(String::from_utf8_lossy(&output.stdout)
151 .trim()
152 .split_terminator(MARKER)
153 .map(|str| str.trim().replace("<", "<").replace(">", ">"))
154 .collect::<Vec<_>>())
155}
156
157pub(crate) const GITLINK_MODE: &str = "160000";
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub(crate) enum CommitDiffObjectKind {
161 Blob,
162 Gitlink,
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166pub(crate) struct CommitDiffObject<'a> {
167 pub oid: &'a str,
168 pub kind: CommitDiffObjectKind,
169}
170
171#[derive(Debug, PartialEq, Eq)]
172pub(crate) struct CommitDiffEntry<'a> {
173 pub path: &'a str,
174 pub status: StatusCode,
175 pub old_object: Option<CommitDiffObject<'a>>,
176 pub new_object: Option<CommitDiffObject<'a>>,
177}
178
179/// Parses the output of `git diff --raw --no-abbrev -z`.
180pub(crate) fn parse_git_diff_raw(
181 content: &str,
182) -> impl Iterator<Item = Result<CommitDiffEntry<'_>>> {
183 let mut parts = content.split('\0');
184 std::iter::from_fn(move || {
185 let metadata = parts.next()?;
186 if metadata.is_empty() {
187 return None;
188 }
189
190 let path = match parts.next() {
191 Some(path) => path,
192 None => return Some(Err(anyhow::anyhow!("raw diff is missing the path"))),
193 };
194 Some(parse_git_diff_raw_entry(metadata, path))
195 })
196}
197
198fn parse_git_diff_raw_entry<'a>(metadata: &'a str, path: &'a str) -> Result<CommitDiffEntry<'a>> {
199 let mut fields = metadata
200 .strip_prefix(':')
201 .context("raw diff metadata is missing its ':' prefix")?
202 .split_ascii_whitespace();
203 let old_mode = fields.next().context("raw diff is missing the old mode")?;
204 let new_mode = fields.next().context("raw diff is missing the new mode")?;
205 let old_oid = fields
206 .next()
207 .context("raw diff is missing the old object ID")?;
208 let new_oid = fields
209 .next()
210 .context("raw diff is missing the new object ID")?;
211 let status = match fields.next() {
212 Some("M") => StatusCode::Modified,
213 Some("T") => StatusCode::TypeChanged,
214 Some("A") => StatusCode::Added,
215 Some("D") => StatusCode::Deleted,
216 Some(status) => anyhow::bail!("unsupported raw diff status {status}"),
217 None => anyhow::bail!("raw diff is missing the status"),
218 };
219
220 Ok(CommitDiffEntry {
221 path,
222 status,
223 old_object: (!old_oid.bytes().all(|byte| byte == b'0')).then(|| CommitDiffObject {
224 oid: old_oid,
225 kind: if old_mode == GITLINK_MODE {
226 CommitDiffObjectKind::Gitlink
227 } else {
228 CommitDiffObjectKind::Blob
229 },
230 }),
231 new_object: (!new_oid.bytes().all(|byte| byte == b'0')).then(|| CommitDiffObject {
232 oid: new_oid,
233 kind: if new_mode == GITLINK_MODE {
234 CommitDiffObjectKind::Gitlink
235 } else {
236 CommitDiffObjectKind::Blob
237 },
238 }),
239 })
240}
241
242#[cfg(test)]
243mod tests {
244
245 use super::*;
246
247 #[test]
248 fn test_parse_git_diff_raw() {
249 let input = concat!(
250 ":100644 100644 1111111111111111111111111111111111111111 2222222222222222222222222222222222222222 M\x00file.txt\x00",
251 ":160000 160000 3333333333333333333333333333333333333333 4444444444444444444444444444444444444444 M\x00modules/example\x00",
252 ":000000 100644 0000000000000000000000000000000000000000 5555555555555555555555555555555555555555 A\x00added.txt\x00",
253 ":160000 000000 6666666666666666666666666666666666666666 0000000000000000000000000000000000000000 D\x00deleted-module\x00",
254 ":100644 160000 7777777777777777777777777777777777777777 8888888888888888888888888888888888888888 T\x00type-change\x00",
255 );
256
257 let entries = parse_git_diff_raw(input)
258 .collect::<Result<Vec<_>>>()
259 .unwrap();
260 let [file, gitlink, added, deleted, type_change] = entries.as_slice() else {
261 panic!("expected five raw diff entries");
262 };
263
264 assert_eq!(file.path, "file.txt");
265 assert_eq!(file.status, StatusCode::Modified);
266 assert_eq!(
267 file.new_object.map(|object| object.kind),
268 Some(CommitDiffObjectKind::Blob)
269 );
270 assert_eq!(gitlink.path, "modules/example");
271 assert_eq!(gitlink.status, StatusCode::Modified);
272 assert_eq!(
273 gitlink.old_object.map(|object| object.kind),
274 Some(CommitDiffObjectKind::Gitlink)
275 );
276 assert_eq!(
277 gitlink.new_object.map(|object| object.kind),
278 Some(CommitDiffObjectKind::Gitlink)
279 );
280 assert!(added.old_object.is_none());
281 assert_eq!(added.status, StatusCode::Added);
282 assert!(deleted.new_object.is_none());
283 assert_eq!(deleted.status, StatusCode::Deleted);
284 assert_eq!(type_change.status, StatusCode::TypeChanged);
285 assert_eq!(
286 type_change.old_object.map(|object| object.kind),
287 Some(CommitDiffObjectKind::Blob)
288 );
289 assert_eq!(
290 type_change.new_object.map(|object| object.kind),
291 Some(CommitDiffObjectKind::Gitlink)
292 );
293 }
294
295 #[test]
296 fn test_parse_git_diff_raw_rejects_malformed_metadata() {
297 let error = parse_git_diff_raw(":100644\x00file.txt\x00")
298 .next()
299 .expect("expected a raw diff entry")
300 .expect_err("expected malformed metadata to fail");
301 assert!(error.to_string().contains("new mode"));
302 }
303
304 #[test]
305 fn test_parse_tag_names_for_lightweight_and_annotated_tags() -> Result<()> {
306 let tagged_commit = Oid::from_str("1111111111111111111111111111111111111111")?;
307 let tag_object = Oid::from_str("2222222222222222222222222222222222222222")?;
308 let other_commit = Oid::from_str("3333333333333333333333333333333333333333")?;
309 let output = format!(
310 "{tagged_commit}\0\0v1.0.0\n\
311 {tag_object}\0{tagged_commit}\0v1.1.0\n\
312 {other_commit}\0\0ignored\n"
313 );
314
315 let parsed = parse_tag_names(&output, &[tagged_commit]);
316
317 assert_eq!(
318 parsed,
319 HashMap::from_iter([(
320 tagged_commit,
321 vec![String::from("v1.0.0"), String::from("v1.1.0")]
322 )])
323 );
324 Ok(())
325 }
326}
327