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 > {
96
let re = regex:: Regex:: new( r "(?i)\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)" ) .unwrap ( ) ;
97
let mut out: Vec< i64 > = re.captures_iter ( message) .filter_map ( | c | c[ 1 ] .parse ( ) .ok ( ) ) .collect ( ) ;
98
out.sort ( ) ;
99
out.dedup ( ) ;
100
out
101
}
102
103
# [ cfg ( test ) ]
104
mod tests {
105
use super :: * ;
78001759 Andy McCall 1 month ago ↶
106
107
# [ test ]
108
fn finds_references_but_not_closings_or_other_repositories ( ) {
109
assert_eq! ( references ( " Faster blame (refs #14, #3)\n \n See #3 again; closes #9; other/repo#5; a#2" ) , vec! [ 3 , 14 ] ) ;
110
assert! ( references ( " Nothing to see; colour #" ) .is_empty ( ) ) ;
111
assert_eq! ( references ( " #7 at the start" ) , vec! [ 7 ] ) ;
112
}
18e381ea Andy McCall 1 month ago ↶
113
114
# [ test ]
115
fn links_references_outside_code ( ) {
116
let n = Numbers { repo_url: " /a/b" , issues: & [ 3 ] , proposals: & [ 7 ] } ;
117
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) ;
118
assert! ( out.contains ( " [#3](/a/b/issue/3)" ) , " {out}" ) ;
119
assert! ( out.contains ( " [#7](/a/b/proposal/7)" ) , " {out}" ) ;
120
assert! ( out.contains ( " #9," ) , " unknown numbers stay: {out}" ) ;
121
assert! ( out.contains ( " `#3`" ) , " code spans untouched: {out}" ) ;
122
assert! ( out.contains ( " [@andy](/andy)" ) , " {out}" ) ;
123
assert! ( out.contains ( " [other/repo#5](/other/repo/issue/5)" ) , " {out}" ) ;
124
assert! ( out.contains ( " [deadbeefcafe](/a/b/change/deadbeefcafe1234)" ) , " {out}" ) ;
125
assert! ( out.contains ( " ```\n #3 @x\n ```" ) , " fences untouched: {out}" ) ;
126
}
127
128
# [ test ]
129
fn closing_keywords ( ) {
130
assert_eq! ( closes ( " Fixes #12 and closes #3, resolved #12" ) , vec! [ 3 , 12 ] ) ;
131
assert! ( closes ( " mentions #4 only" ) .is_empty ( ) ) ;
132
}
133
}