amont_runtime/hooks/
prepare_commit_msg.rs1use crate::check::Verdict;
8use crate::git;
9use std::fs::OpenOptions;
10use std::io::Write;
11
12pub fn find_jira(s: &str) -> Option<String> {
19 let b = s.as_bytes();
20 for start in 0..b.len() {
21 let mut e = start;
22 while e < b.len() && b[e].is_ascii_uppercase() {
23 e += 1;
24 }
25 let letters = e - start;
26 if !(3..=32).contains(&letters) || e >= b.len() || b[e] != b'-' {
27 continue;
28 }
29 let d0 = e + 1;
30 if d0 >= b.len() || !(b'1'..=b'9').contains(&b[d0]) {
32 continue;
33 }
34 let mut d = d0 + 1;
36 while d < b.len() && b[d].is_ascii_digit() && d - d0 < 32 {
37 d += 1;
38 }
39 if d - d0 < 2 {
40 continue;
41 }
42 return Some(s[start..d].to_string());
43 }
44 None
45}
46
47pub fn find_digits(s: &str, min: usize) -> Option<String> {
49 let b = s.as_bytes();
50 let mut i = 0;
51 while i < b.len() {
52 if !b[i].is_ascii_digit() {
53 i += 1;
54 continue;
55 }
56 let start = i;
57 while i < b.len() && b[i].is_ascii_digit() {
58 i += 1;
59 }
60 if i - start >= min {
61 return Some(s[start..i].to_string());
62 }
63 }
64 None
65}
66
67fn append(path: &str, line: &str) {
74 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
77 let _ = writeln!(file, "\n{line}");
78 }
79}
80
81pub fn run(args: &[std::ffi::OsString]) -> Verdict {
82 let Some(msg_file) = args.first().and_then(|a| a.to_str()) else {
83 return Verdict::Proceed;
84 };
85 let source = args.get(1).and_then(|a| a.to_str()).unwrap_or("");
92 if matches!(
93 source,
94 "message" | "template" | "merge" | "squash" | "commit"
95 ) {
96 return Verdict::Proceed;
97 }
98
99 let branch = git::stdout(&["branch", "--show-current"]).unwrap_or_default();
100
101 if let Some(id) = find_jira(&branch) {
102 append(msg_file, &format!("Issue: {id}"));
103 return Verdict::Proceed;
104 }
105 if let Some(id) = find_digits(&branch, 3) {
110 append(msg_file, &format!("Issue: #id {id}"));
111 return Verdict::Proceed;
112 }
113 Verdict::Proceed
114}
115
116#[cfg(test)]
117mod tests {
118 use super::{find_digits, find_jira};
119
120 #[test]
121 fn finds_a_jira_id() {
122 assert_eq!(
123 find_jira("feat/JIRA-1234-description").as_deref(),
124 Some("JIRA-1234")
125 );
126 assert_eq!(find_jira("ABC-12").as_deref(), Some("ABC-12"));
127 }
128
129 #[test]
130 fn rejects_shapes_the_regex_rejects() {
131 assert_eq!(find_jira("AB-1234"), None); assert_eq!(find_jira("ABC-1"), None); assert_eq!(find_jira("ABC-0123"), None); assert_eq!(find_jira("abc-1234"), None); assert_eq!(find_jira("feat/nothing-here"), None);
136 }
137
138 #[test]
141 fn long_letter_runs_match_at_a_later_offset() {
142 let s = format!("{}-12", "A".repeat(40));
143 assert_eq!(
144 find_jira(&s).as_deref(),
145 Some(&*format!("{}-12", "A".repeat(32)))
146 );
147 }
148
149 #[test]
150 fn finds_bare_ids_of_at_least_three_digits() {
151 assert_eq!(
152 find_digits("fix/1234-something", 3).as_deref(),
153 Some("1234")
154 );
155 assert_eq!(find_digits("fix/12-something", 3), None);
156 assert_eq!(find_digits("release/007", 3).as_deref(), Some("007"));
157 assert_eq!(find_digits("no-digits-here", 3), None);
158 }
159}