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