Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Integrations: your own model provider, alerts that open issues, tickets agents read | 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)] | |
| 6 | pub 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)] | |
| 15 | pub 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. | |
| 22 | pub 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`. | |
| 32 | pub fn project(key: &str) -> &str { | |
| 33 | key.split_once('-').map_or(key, |(project, _)| project) | |
| 34 | } | |
| 35 | ||
| 36 | fn 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. | |
| 71 | pub 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)] | |
| 96 | mod 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 | } |