flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/integrations/src/refs.rs

146 lines5,004 bytesCodeBlame
1//! Finding references to outside things in text: `TECH-1234`, a Jira or
2//! Linear address, a Sentry issue's address.
3
4/// One reference, as written.
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub enum Reference {
7 /// A ticket key, `TECH-1234`, bare or from a Jira or Linear address.
8 /// `from` says which system the address was, when there was one.
9 Key { key: String, from: Option<Source> },
10 /// A Sentry issue, by its numeric id, from its address.
11 SentryIssue { id: String },
12}
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Source {
16 Jira,
17 Linear,
18}
19
20/// Whether `word` is a ticket key: a project of 2 to 10 capital letters or
21/// digits starting with a letter, a dash, and a number.
22pub fn as_key(word: &str) -> Option<String> {
23 let (project, number) = word.split_once('-')?;
24 let project_ok = (2..=10).contains(&project.len())
25 && project.chars().next().is_some_and(|c| c.is_ascii_uppercase())
26 && project.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit());
27 let number_ok = (1..=7).contains(&number.len()) && number.chars().all(|c| c.is_ascii_digit());
28 (project_ok && number_ok).then(|| word.to_owned())
29}
30
31/// The project part of a key: `TECH` in `TECH-1234`.
32pub fn project(key: &str) -> &str {
33 key.split_once('-').map_or(key, |(project, _)| project)
34}
35
36fn from_url(url: &str) -> Option<Reference> {
37 let path_segments: Vec<&str> = url
38 .split(['?', '#'])
39 .next()
40 .unwrap_or(url)
41 .split('/')
42 .filter(|segment| !segment.is_empty())
43 .collect();
44 let host = path_segments.get(1).copied().unwrap_or_default().to_ascii_lowercase();
45 let after = |name: &str| {
46 path_segments
47 .iter()
48 .position(|segment| *segment == name)
49 .and_then(|at| path_segments.get(at + 1).copied())
50 };
51 if host == "linear.app" || host.ends_with(".linear.app") {
52 let key = as_key(after("issue")?)?;
53 return Some(Reference::Key {
54 key,
55 from: Some(Source::Linear),
56 });
57 }
58 if host == "sentry.io" || host.ends_with(".sentry.io") {
59 let id = after("issues")?;
60 return id.chars().all(|c| c.is_ascii_digit()).then(|| Reference::SentryIssue { id: id.to_owned() });
61 }
62 // Jira, wherever it is hosted: `/browse/TECH-1234`.
63 let key = as_key(after("browse")?)?;
64 Some(Reference::Key {
65 key,
66 from: Some(Source::Jira),
67 })
68}
69
70/// Every reference in `text`, in order, each once.
71pub fn find(text: &str) -> Vec<Reference> {
72 let mut found: Vec<Reference> = Vec::new();
73 let mut push = |reference: Reference| {
74 let seen = found.iter().any(|existing| match (existing, &reference) {
75 (Reference::Key { key: a, .. }, Reference::Key { key: b, .. }) => a == b,
76 _ => *existing == reference,
77 });
78 if !seen {
79 found.push(reference);
80 }
81 };
82 for word in text.split(|c: char| c.is_whitespace() || matches!(c, '(' | ')' | '[' | ']' | '<' | '>' | '"' | '\'' | '`' | ',' | ';')) {
83 let word = word.trim_end_matches(['.', ':', '!', '?']);
84 if word.starts_with("https://") || word.starts_with("http://") {
85 if let Some(reference) = from_url(word) {
86 push(reference);
87 }
88 } else if let Some(key) = as_key(word) {
89 push(Reference::Key { key, from: None });
90 }
91 }
92 found
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 fn key(key: &str, from: Option<Source>) -> Reference {
100 Reference::Key {
101 key: key.to_owned(),
102 from,
103 }
104 }
105
106 #[test]
107 fn bare_keys_are_found_once() {
108 assert_eq!(
109 find("I need to accomplish TECH-1234, see also (OPS-7) and TECH-1234."),
110 vec![key("TECH-1234", None), key("OPS-7", None)]
111 );
112 }
113
114 #[test]
115 fn things_that_only_look_like_keys_are_not() {
116 assert!(find("UTF-8 is fine, so is A-1, x-12 and SHA-256abc").iter().all(|r| *r == key("UTF-8", None)));
117 assert!(find("covid-19 and ISO-8601-2").is_empty());
118 }
119
120 #[test]
121 fn addresses_say_which_system() {
122 assert_eq!(
123 find("https://acme.atlassian.net/browse/TECH-12?focusedCommentId=3"),
124 vec![key("TECH-12", Some(Source::Jira))]
125 );
126 assert_eq!(
127 find("https://linear.app/acme/issue/ENG-42/fix-the-thing"),
128 vec![key("ENG-42", Some(Source::Linear))]
129 );
130 assert_eq!(
131 find("Broke in https://acme.sentry.io/issues/4509812345/?project=1"),
132 vec![Reference::SentryIssue {
133 id: "4509812345".to_owned()
134 }]
135 );
136 assert_eq!(
137 find("https://sentry.io/organizations/acme/issues/77/"),
138 vec![Reference::SentryIssue { id: "77".to_owned() }]
139 );
140 }
141
142 #[test]
143 fn the_project_is_the_part_before_the_dash() {
144 assert_eq!(project("TECH-1234"), "TECH");
145 }
146}