@@ -9,7 +9,7 @@ use tracing::{error, info, warn};
99
1010use crate :: extract_fixes;
1111use crate :: gitea:: { GiteaClient , PrSummary } ;
12- use crate :: types:: { EvalVerdict , MergeCoordinatorError , MergeOutcome } ;
12+ use crate :: types:: { BlockerKind , EvalVerdict , MergeCoordinatorError , MergeOutcome } ;
1313
1414/// One evaluation of one open PR.
1515#[ derive( Debug , Clone ) ]
@@ -22,6 +22,8 @@ pub struct PrEvaluation {
2222 pub fixes_issues : Vec < u64 > ,
2323 /// Verdict reached during evaluation.
2424 pub verdict : EvalVerdict ,
25+ /// Classified blocker kind when verdict is Hold (None for Merge).
26+ pub blocker_kind : Option < BlockerKind > ,
2527}
2628
2729/// Evaluate all open PRs in `owner/repo`, sequentially. Each PR gets
@@ -34,28 +36,63 @@ pub async fn evaluate_all(
3436 let prs = gitea. list_open_prs ( owner, repo) . await ?;
3537 let mut out = Vec :: with_capacity ( prs. len ( ) ) ;
3638 for pr in prs {
37- out. push ( evaluate_one ( & pr) ) ;
39+ out. push ( evaluate_one ( Some ( gitea ) , owner , repo , & pr) . await ) ;
3840 }
3941 info ! ( count = out. len( ) , owner, repo, "evaluated open PRs" ) ;
4042 Ok ( out)
4143}
4244
43- fn evaluate_one ( pr : & PrSummary ) -> PrEvaluation {
45+ async fn evaluate_one (
46+ gitea : Option < & GiteaClient > ,
47+ owner : & str ,
48+ repo : & str ,
49+ pr : & PrSummary ,
50+ ) -> PrEvaluation {
4451 let mergeable = pr. mergeable . unwrap_or ( false ) ;
4552 let fixes_issues = extract_fixes ( pr. body . as_deref ( ) . unwrap_or ( "" ) ) ;
46- let verdict = if !mergeable {
47- EvalVerdict :: Hold ( "not mergeable" . into ( ) )
48- } else if fixes_issues. is_empty ( ) {
49- // No Fixes #N -> safe to merge but nothing to auto-close.
50- EvalVerdict :: Merge
53+ let ( verdict, blocker_kind) = if !mergeable {
54+ let kind = classify_blocker ( gitea, owner, repo, pr) . await ;
55+ let reason = format ! ( "not mergeable ({kind})" ) ;
56+ ( EvalVerdict :: Hold ( reason) , Some ( kind) )
5157 } else {
52- EvalVerdict :: Merge
58+ ( EvalVerdict :: Merge , None )
5359 } ;
5460 PrEvaluation {
5561 pr_index : pr. number ,
5662 mergeable,
5763 fixes_issues,
5864 verdict,
65+ blocker_kind,
66+ }
67+ }
68+
69+ /// Query CI status and classify why a PR is blocked.
70+ async fn classify_blocker (
71+ gitea : Option < & GiteaClient > ,
72+ owner : & str ,
73+ repo : & str ,
74+ pr : & PrSummary ,
75+ ) -> BlockerKind {
76+ let gitea = match gitea {
77+ Some ( c) => c,
78+ None => return BlockerKind :: CiNoStatus ,
79+ } ;
80+ let sha = match pr. head_sha . as_deref ( ) {
81+ Some ( s) if !s. is_empty ( ) => s,
82+ _ => return BlockerKind :: CiNoStatus ,
83+ } ;
84+
85+ match gitea. get_commit_status ( owner, repo, sha) . await {
86+ Ok ( Some ( combined) ) => match combined. state . as_str ( ) {
87+ "failure" | "error" => BlockerKind :: CiFailed ,
88+ "pending" => BlockerKind :: CiPending ,
89+ _ => BlockerKind :: NotMergeable ,
90+ } ,
91+ Ok ( None ) => BlockerKind :: CiNoStatus ,
92+ Err ( e) => {
93+ warn ! ( pr = pr. number, error = %e, "failed to query CI status" ) ;
94+ BlockerKind :: CiNoStatus
95+ }
5996 }
6097}
6198
@@ -126,65 +163,71 @@ mod tests {
126163 body : Some ( body. into ( ) ) ,
127164 state : "open" . into ( ) ,
128165 mergeable : Some ( mergeable) ,
166+ head_sha : None ,
129167 }
130168 }
131169
132- #[ test]
133- fn evaluate_one_holds_when_not_mergeable ( ) {
170+ #[ tokio :: test]
171+ async fn evaluate_one_holds_when_not_mergeable ( ) {
134172 let p = pr ( 1 , "Fixes #2" , false ) ;
135- let e = evaluate_one ( & p) ;
173+ let e = evaluate_one ( None , "o" , "r" , & p) . await ;
136174 assert ! ( matches!( e. verdict, EvalVerdict :: Hold ( _) ) ) ;
137175 assert_eq ! ( e. fixes_issues, vec![ 2 ] ) ;
176+ assert_eq ! ( e. blocker_kind, Some ( BlockerKind :: CiNoStatus ) ) ;
138177 }
139178
140- #[ test]
141- fn evaluate_one_merge_with_fixes ( ) {
179+ #[ tokio :: test]
180+ async fn evaluate_one_merge_with_fixes ( ) {
142181 // Both "Fixes #42" and "Closes #43" are now recognised closing keywords.
143182 let p = pr ( 7 , "Fixes #42 Closes #43" , true ) ;
144- let e = evaluate_one ( & p) ;
183+ let e = evaluate_one ( None , "o" , "r" , & p) . await ;
145184 assert_eq ! ( e. verdict, EvalVerdict :: Merge ) ;
146185 assert_eq ! ( e. fixes_issues, vec![ 42 , 43 ] ) ;
186+ assert_eq ! ( e. blocker_kind, None ) ;
147187 }
148188
149- #[ test]
150- fn evaluate_one_merge_no_fixes_still_merges ( ) {
189+ #[ tokio :: test]
190+ async fn evaluate_one_merge_no_fixes_still_merges ( ) {
151191 let p = pr ( 9 , "feat: refactor" , true ) ;
152- let e = evaluate_one ( & p) ;
192+ let e = evaluate_one ( None , "o" , "r" , & p) . await ;
153193 assert_eq ! ( e. verdict, EvalVerdict :: Merge ) ;
154194 assert ! ( e. fixes_issues. is_empty( ) ) ;
195+ assert_eq ! ( e. blocker_kind, None ) ;
155196 }
156197
157- #[ test]
158- fn evaluate_one_handles_missing_body ( ) {
198+ #[ tokio :: test]
199+ async fn evaluate_one_handles_missing_body ( ) {
159200 let p = PrSummary {
160201 number : 11 ,
161202 title : "x" . into ( ) ,
162203 body : None ,
163204 state : "open" . into ( ) ,
164205 mergeable : Some ( true ) ,
206+ head_sha : None ,
165207 } ;
166- let e = evaluate_one ( & p) ;
208+ let e = evaluate_one ( None , "o" , "r" , & p) . await ;
167209 assert_eq ! ( e. verdict, EvalVerdict :: Merge ) ;
168210 assert ! ( e. fixes_issues. is_empty( ) ) ;
211+ assert_eq ! ( e. blocker_kind, None ) ;
169212 }
170213
171- #[ test]
172- fn evaluate_one_processes_51_prs_without_truncation ( ) {
173- // Regression test for issue #2850: ensure the evaluation loop does not
174- // impose an artificial cap at position 50. With list_open_prs returning
175- // up to OPEN_PRS_LIMIT (300) items, all PRs must receive a verdict.
214+ #[ tokio:: test]
215+ async fn evaluate_one_processes_51_prs_without_truncation ( ) {
176216 let prs: Vec < PrSummary > = ( 1u64 ..=51 )
177217 . map ( |n| pr ( n, & format ! ( "Fixes #{n}" ) , true ) )
178218 . collect ( ) ;
179- let evaluations: Vec < _ > = prs. iter ( ) . map ( evaluate_one) . collect ( ) ;
219+ let mut evaluations = Vec :: with_capacity ( prs. len ( ) ) ;
220+ for p in & prs {
221+ evaluations. push ( evaluate_one ( None , "o" , "r" , p) . await ) ;
222+ }
180223 assert_eq ! (
181224 evaluations. len( ) ,
182225 51 ,
183226 "all 51 PRs must receive an evaluation verdict"
184227 ) ;
185- // Spot-check: the PR at position 51 gets Merge (it is mergeable, clean)
186228 let last = & evaluations[ 50 ] ;
187229 assert_eq ! ( last. pr_index, 51 ) ;
188230 assert_eq ! ( last. verdict, EvalVerdict :: Merge ) ;
231+ assert_eq ! ( last. blocker_kind, None ) ;
189232 }
190233}
0 commit comments