11use clap:: Parser ;
2+ use gitpatch:: Patch ;
23use miette:: { IntoDiagnostic , bail} ;
34use nickel_lang_package:: {
45 config:: Config ,
@@ -67,7 +68,7 @@ impl Permission {
6768
6869enum Report {
6970 InvalidDiff ( package:: Error ) ,
70- PackageReports ( Vec < PackageReport > ) ,
71+ PackageReports ( Vec < Box < dyn ReportItem > > ) ,
7172}
7273
7374impl Report {
@@ -85,13 +86,19 @@ impl std::fmt::Display for Report {
8586 Report :: InvalidDiff ( e) => writeln ! ( f, "❌ invalid index changes: {e}" ) ,
8687 Report :: PackageReports ( package_reports) => {
8788 for r in package_reports {
88- r. format ( f, " - " ) ?;
89+ r. format_with_indent ( f, " - " ) ?;
8990 }
9091 Ok ( ( ) )
9192 }
9293 }
9394 }
9495}
96+
97+ trait ReportItem {
98+ fn is_good ( & self ) -> bool ;
99+ fn format_with_indent ( & self , f : & mut std:: fmt:: Formatter , indent : & str ) -> std:: fmt:: Result ;
100+ }
101+
95102struct PackageReport {
96103 pkg : Package ,
97104 permission : Permission ,
@@ -128,7 +135,9 @@ impl PackageReport {
128135 status,
129136 } )
130137 }
138+ }
131139
140+ impl ReportItem for PackageReport {
132141 fn is_good ( & self ) -> bool {
133142 self . permission . is_allowed
134143 && match & self . status {
@@ -137,7 +146,7 @@ impl PackageReport {
137146 }
138147 }
139148
140- fn format ( & self , f : & mut std:: fmt:: Formatter , indent : & str ) -> std:: fmt:: Result {
149+ fn format_with_indent ( & self , f : & mut std:: fmt:: Formatter , indent : & str ) -> std:: fmt:: Result {
141150 let PreciseId :: Github {
142151 org, name, path, ..
143152 } = & self . pkg . id ;
@@ -151,26 +160,26 @@ impl PackageReport {
151160 if perm. is_allowed {
152161 writeln ! (
153162 f,
154- "{indent_spaces}* ✅ this PR is by {}, a collaborator on {}/{}" ,
163+ "{indent_spaces}*✅ this PR is by {}, a collaborator on {}/{}" ,
155164 perm. user, perm. org, perm. repo
156165 ) ?;
157166 } else {
158167 writeln ! (
159168 f,
160- "{indent_spaces}* ❌ this PR is by {}, who is not a public member of {}" ,
169+ "{indent_spaces}*❌ this PR is by {}, who is not a public member of {}" ,
161170 perm. user, perm. org
162171 ) ?;
163172 } ;
164173
165174 if let PackageStatus :: FetchFailed ( e) = & self . status {
166- writeln ! ( f, "{indent_spaces}* ❌ failed to fetch package: {e}" , ) ?;
175+ writeln ! ( f, "{indent_spaces}*❌ failed to fetch package: {e}" , ) ?;
167176 } else {
168- writeln ! ( f, "{indent_spaces}* ✅ fetched package" , ) ?;
177+ writeln ! ( f, "{indent_spaces}*✅ fetched package" , ) ?;
169178
170179 if let PackageStatus :: EvalFailed ( e) = & self . status {
171- writeln ! ( f, "{indent_spaces}* ❌ failed to evaluate manifest: {e}" , ) ?;
180+ writeln ! ( f, "{indent_spaces}*❌ failed to evaluate manifest: {e}" , ) ?;
172181 } else {
173- writeln ! ( f, "{indent_spaces}* ✅ evaluated manifest" , ) ?;
182+ writeln ! ( f, "{indent_spaces}*✅ evaluated manifest" , ) ?;
174183
175184 let PackageStatus :: Manifest ( checks) = & self . status else {
176185 unreachable ! ( )
@@ -183,22 +192,78 @@ impl PackageReport {
183192 }
184193}
185194
195+ /// A diagnostic for showing that an unexpected path was modified.
196+ struct PathReport {
197+ is_good : bool ,
198+ path : String ,
199+ }
200+
201+ impl ReportItem for PathReport {
202+ fn is_good ( & self ) -> bool {
203+ self . is_good
204+ }
205+
206+ fn format_with_indent ( & self , f : & mut std:: fmt:: Formatter , indent : & str ) -> std:: fmt:: Result {
207+ let sym = if self . is_good { "⚠️" } else { "❌" } ;
208+ let path = & self . path ;
209+ writeln ! ( f, "{indent}{sym} this PR modifies {path}" )
210+ }
211+ }
212+
186213enum PackageStatus {
187214 FetchFailed ( String ) ,
188215 EvalFailed ( String ) ,
189216 Manifest ( Box < ManifestChecks > ) ,
190217}
191218
219+ /// Checks the paths of modified files. Removes the ones that aren't modifying
220+ /// packages and adds diagnostic messages for them.
221+ fn check_diff_paths ( patches : & mut Vec < Patch > , reports : & mut Vec < Box < dyn ReportItem > > ) {
222+ patches. retain ( |patch| {
223+ let path = & patch. new . path ;
224+ let mut parts = path. split ( '/' ) ;
225+ if parts. next ( ) != Some ( "b" ) {
226+ reports. push ( Box :: new ( PathReport {
227+ is_good : false ,
228+ path : patch. new . path . clone ( ) . into_owned ( ) ,
229+ } ) ) ;
230+ return false ;
231+ }
232+
233+ // Trim off the "b/" for a better error message.
234+ let path_without_prefix = & patch. new . path [ 2 ..] ;
235+ let dir = parts. next ( ) ;
236+ if dir != Some ( "github" ) {
237+ // Modifications to our CI are not necessarily bad. Any other path
238+ // is definitely a mistake.
239+ let is_good = dir == Some ( ".github" ) ;
240+ reports. push ( Box :: new ( PathReport {
241+ is_good,
242+ path : path_without_prefix. to_owned ( ) ,
243+ } ) ) ;
244+ return false ;
245+ }
246+ true
247+ } ) ;
248+ }
249+
192250async fn make_report ( diff : & str , client : & Octocrab , user : & str ) -> miette:: Result < Report > {
193- let pkgs = match package:: changed_packages ( diff) {
251+ let mut reports = Vec :: new ( ) ;
252+ let mut patches = match Patch :: from_multiple ( diff) {
253+ Ok ( p) => p,
254+ Err ( e) => return Ok ( Report :: InvalidDiff ( e. into ( ) ) ) ,
255+ } ;
256+ check_diff_paths ( & mut patches, & mut reports) ;
257+ let pkgs = match package:: changed_packages ( patches) {
194258 Ok ( p) => p,
195259 Err ( e) => return Ok ( Report :: InvalidDiff ( e) ) ,
196260 } ;
197261
198262 let index = PackageIndex :: refreshed ( Config :: new ( ) . into_diag ( ) ?) . into_diag ( ) ?;
199- let mut reports = Vec :: new ( ) ;
200263 for pkg in pkgs {
201- reports. push ( PackageReport :: new ( client, user, & index, pkg) . await ?) ;
264+ reports. push ( Box :: new (
265+ PackageReport :: new ( client, user, & index, pkg) . await ?,
266+ ) ) ;
202267 }
203268
204269 Ok ( Report :: PackageReports ( reports) )
@@ -230,3 +295,63 @@ async fn main() -> miette::Result<()> {
230295 bail ! ( "Failing report" )
231296 }
232297}
298+
299+ #[ cfg( test) ]
300+ mod tests {
301+ use gitpatch:: Patch ;
302+
303+ use crate :: { Report , check_diff_paths} ;
304+
305+ const SAMPLE_CI_DIFF : & str = r#"
306+ diff --git a/.github/workflows/foo.yaml b/.github/workflows/foo.yaml
307+ index df1cd2a..2229806 100644
308+ --- a/.github/workflows/foo.yaml
309+ +++ b/.github/workflows/foo.yaml
310+ @@ -1 +1,2 @@
311+ foo
312+ +bar
313+ "# ;
314+
315+ const BAD_PATH_DIFF : & str = r#"
316+ diff --git a/weird_path/foo.yaml b/weird_path/foo.yaml
317+ index df1cd2a..2229806 100644
318+ --- a/weird_path/foo.yaml
319+ +++ b/weird_path/foo.yaml
320+ @@ -1 +1,2 @@
321+ foo
322+ +bar
323+ "# ;
324+
325+ #[ test]
326+ fn test_ci_changes ( ) {
327+ let mut reports = Vec :: new ( ) ;
328+ let mut patches = Patch :: from_multiple ( SAMPLE_CI_DIFF ) . unwrap ( ) ;
329+ check_diff_paths ( & mut patches, & mut reports) ;
330+
331+ // The CI patch should have been removed from the list.
332+ assert ! ( patches. is_empty( ) ) ;
333+ let report = Report :: PackageReports ( reports) ;
334+ assert ! (
335+ report
336+ . to_string( )
337+ . contains( "this PR modifies .github/workflows/foo.yaml" )
338+ ) ;
339+ assert ! ( report. is_good( ) ) ;
340+ }
341+
342+ #[ test]
343+ fn test_bad_path_changes ( ) {
344+ let mut reports = Vec :: new ( ) ;
345+ let mut patches = Patch :: from_multiple ( BAD_PATH_DIFF ) . unwrap ( ) ;
346+ check_diff_paths ( & mut patches, & mut reports) ;
347+
348+ assert ! ( patches. is_empty( ) ) ;
349+ let report = Report :: PackageReports ( reports) ;
350+ assert ! (
351+ report
352+ . to_string( )
353+ . contains( "this PR modifies weird_path/foo.yaml" )
354+ ) ;
355+ assert ! ( !report. is_good( ) ) ;
356+ }
357+ }
0 commit comments