Skip to content
146 linesCodeBlameRaw

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.

The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor1//! 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}

This file's history is long; its oldest lines are credited to the oldest commit read.