18e381ea Andy McCall 1 month ago ↶
1
2
3
4
5
6
pub struct Numbers < 'a > {
7
pub repo_url : & 'a str ,
8
pub issues : & 'a [i64 ],
9
pub proposals : & 'a [i64 ],
10
}
11
12
impl Numbers < '_ > {
13
fn url_for ( & self , n : i64 ) -> Option< String> {
14
if self .proposals.contains ( & n) {
15
Some ( format! ( " {} /proposal/{n} " , self .repo_url) )
16
} else if self .issues.contains ( & n) {
17
Some ( format! ( " {} /issue/{n} " , self .repo_url) )
18
} else {
19
None
20
}
21
}
22
}
23
24
25
26
pub fn link ( text : & str , numbers : & Numbers< '_ > ) -> String {
27
let mut out = String :: with_capacity( text.len ( ) + 64 ) ;
28
let mut in_fence = false ;
29
for line in text.split_inclusive ( ' \n ' ) {
30
if line.trim_start ( ) .starts_with ( " ```" ) {
31
in_fence = ! in_fence;
32
out.push_str ( line) ;
33
continue ;
34
}
35
if in_fence {
36
out.push_str ( line) ;
37
continue ;
38
}
39
40
for ( i, seg) in line.split ( ' `' ) .enumerate ( ) {
41
if i > 0 {
42
out.push ( ' `' ) ;
43
}
44
if i % 2 = = 0 {
45
out.push_str ( & link_prose ( seg, numbers) ) ;
46
} else {
47
out.push_str ( seg) ;
48
}
49
}
50
}
51
out
52
}
53
54
fn link_prose ( seg : & str , numbers : & Numbers< '_ > ) -> String {
55
let re = regex:: Regex:: new( r "(?P<pre>^|[^\w/\[])(?:(?P<slug>[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*)#(?P<xn>\d+)|#(?P<n>\d+)|@(?P<user>[A-Za-z0-9][A-Za-z0-9._-]{0,63})|(?P<hash>\b[0-9a-f]{12,64}\b))" ) .unwrap ( ) ;
56
re.replace_all ( seg, | c : & regex:: Captures| {
57
let pre = & c[ " pre" ] ;
58
if let Some ( slug) = c.name ( " slug" ) {
59
let n = & c[ " xn" ] ;
60
return format! ( " {pre} [{} #{n} ](/{} /issue/{n} )" , slug.as_str ( ) , slug.as_str ( ) ) ;
61
}
62
if let Some ( n) = c.name ( " n" ) {
63
let num: i64 = n.as_str ( ) .parse ( ) .unwrap_or ( 0 ) ;
64
return match numbers.url_for ( num) {
65
Some ( url) => format! ( " {pre} [#{num} ]({url} )" ) ,
66
None => c[ 0 ] .to_string ( ) ,
67
} ;
68
}
69
if let Some ( u) = c.name ( " user" ) {
70
let name = u.as_str ( ) .trim_end_matches ( ' .' ) ;
71
return format! ( " {pre} [@{name} ](/{name} )" ) ;
72
}
73
if let Some ( h) = c.name ( " hash" ) {
74
let hex = h.as_str ( ) ;
75
return format! ( " {pre} [{} ]({} /change/{hex} )" , & hex[ .. 12. min ( hex.len ( ) ) ] , numbers.repo_url) ;
76
}
77
c[ 0 ] .to_string ( )
78
} )
79
.into_owned ( )
80
}
81
82
83
78001759 Andy McCall 1 month ago ↶
84
85
86
pub fn references ( message : & str ) -> Vec< i64 > {
87
let re = regex:: Regex:: new( r "(?:^|[^\w/\[&])#(\d+)\b" ) .unwrap ( ) ;
88
let closing = closes ( message) ;
89
let mut out: Vec< i64 > = re.captures_iter ( message) .filter_map ( | c | c[ 1 ] .parse ( ) .ok ( ) ) .filter ( | n | ! closing.contains ( n) ) .collect ( ) ;
90
out.sort ( ) ;
91
out.dedup ( ) ;
92
out
93
}
94
18e381ea Andy McCall 1 month ago ↶
95
pub fn closes ( message : & str ) -> Vec< i64 > {
1966661b Andy McCall 22 days ago ↶
96
97
98
99
let re = regex:: Regex:: new(
100
r "(?i)\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s+((?:[A-Za-z0-9._-]+/[A-Za-z0-9._-]+)?#\d+(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+|\s*&\s*|\s+)(?:[A-Za-z0-9._-]+/[A-Za-z0-9._-]+)?#\d+)*)" ,
101
)
102
.unwrap ( ) ;
103
let item = regex:: Regex:: new( r "(?:^|[^\w/])#(\d+)" ) .unwrap ( ) ;
104
let mut out: Vec< i64 > =
105
re.captures_iter ( message) .flat_map ( | c | item.captures_iter ( & c[ 1 ] ) .filter_map ( | i | i[ 1 ] .parse ( ) .ok ( ) ) .collect:: < Vec< i64 > > ( ) ) .collect ( ) ;
18e381ea Andy McCall 1 month ago ↶
106
out.sort ( ) ;
107
out.dedup ( ) ;
108
out
109
}
110
111
# [ cfg ( test ) ]
112
mod tests {
113
use super :: * ;
78001759 Andy McCall 1 month ago ↶
114
115
# [ test ]
1966661b Andy McCall 22 days ago ↶
116
fn a_closing_keyword_closes_every_number_it_lists ( ) {
117
118
assert_eq! ( closes ( " Polish\n \n Closes #187, #188, #189, #190, #191, #192, #193, #194, #195, #196" ) , ( 187 .. = 196 ) .collect:: < Vec< _ > > ( ) ) ;
119
assert_eq! ( closes ( " Closes #1, #2 and #3" ) , vec! [ 1 , 2 , 3 ] ) ;
120
assert_eq! ( closes ( " Closes #1, and #2" ) , vec! [ 1 , 2 ] ) ;
121
assert_eq! ( closes ( " Fixes: #4 #5" ) , vec! [ 4 , 5 ] ) ;
122
assert_eq! ( closes ( " resolves #6 & #7" ) , vec! [ 6 , 7 ] ) ;
123
assert_eq! ( closes ( " Closes #1, closes #2\n Fixed #8." ) , vec! [ 1 , 2 , 8 ] ) ;
124
assert_eq! ( closes ( " Closes #9, other/repo#10, #11" ) , vec! [ 9 , 11 ] , " another repository's number is not ours" ) ;
125
assert_eq! ( closes ( " Refs #12, closes #13. See #14." ) , vec! [ 13 ] , " a list ends where the prose starts" ) ;
126
assert! ( closes ( " closest #1; disclosed #2" ) .is_empty ( ) ) ;
127
assert_eq! ( references ( " Closes #1, #2 and #3; refs #4" ) , vec! [ 4 ] , " listed closings are not plain references" ) ;
128
}
129
130
# [ test ]
78001759 Andy McCall 1 month ago ↶
131
fn finds_references_but_not_closings_or_other_repositories ( ) {
132
assert_eq! ( references ( " Faster blame (refs #14, #3)\n \n See #3 again; closes #9; other/repo#5; a#2" ) , vec! [ 3 , 14 ] ) ;
133
assert! ( references ( " Nothing to see; colour #" ) .is_empty ( ) ) ;
134
assert_eq! ( references ( " #7 at the start" ) , vec! [ 7 ] ) ;
135
}
18e381ea Andy McCall 1 month ago ↶
136
137
# [ test ]
138
fn links_references_outside_code ( ) {
139
let n = Numbers { repo_url: " /a/b" , issues: & [ 3 ] , proposals: & [ 7 ] } ;
140
let out = link ( " see #3 and #7 and #9, `#3` too, by @andy; a/b#3 and other/repo#5; hash deadbeefcafe1234\n ```\n #3 @x\n ```\n " , & n) ;
141
assert! ( out.contains ( " [#3](/a/b/issue/3)" ) , " {out}" ) ;
142
assert! ( out.contains ( " [#7](/a/b/proposal/7)" ) , " {out}" ) ;
143
assert! ( out.contains ( " #9," ) , " unknown numbers stay: {out}" ) ;
144
assert! ( out.contains ( " `#3`" ) , " code spans untouched: {out}" ) ;
145
assert! ( out.contains ( " [@andy](/andy)" ) , " {out}" ) ;
146
assert! ( out.contains ( " [other/repo#5](/other/repo/issue/5)" ) , " {out}" ) ;
147
assert! ( out.contains ( " [deadbeefcafe](/a/b/change/deadbeefcafe1234)" ) , " {out}" ) ;
148
assert! ( out.contains ( " ```\n #3 @x\n ```" ) , " fences untouched: {out}" ) ;
149
}
150
151
# [ test ]
152
fn closing_keywords ( ) {
153
assert_eq! ( closes ( " Fixes #12 and closes #3, resolved #12" ) , vec! [ 3 , 12 ] ) ;
154
assert! ( closes ( " mentions #4 only" ) .is_empty ( ) ) ;
155
}
156
}