Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:27:30.549Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

commit.rs

327 lines · 11.1 KB · rust
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("<", "&lt;").replace(">", "&gt;"))
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
Served at tenant.openagents/omega Member data and write actions are omitted.