1414//! a tree.
1515//!
1616//! 404 responses are special-cased: the API returns 404 for "PR is
17- //! not currently in the merge queue", which is a routine caller
18- //! branch rather than a server failure. The command returns a
19- //! [`CliError::MergifyApi`] whose message is `PR #N is not in the
20- //! merge queue`; the binary's top-level handler prints it to
21- //! stderr as `mergify: PR #N is not in the merge queue` (the
22- //! `mergify: ` prefix is the binary's standard error envelope)
23- //! and exits with the Mergify-API error code. Live smoke tests
24- //! assert against the substring, which is stable across the
25- //! Python and Rust implementations.
17+ //! not currently in the merge queue", which is a routine queryable
18+ //! state rather than a server failure. The command reports it on
19+ //! stdout and exits 0 — a not-queued PR is a normal answer, not an
20+ //! error a script should branch on as an API failure. Under `--json`
21+ //! the not-queued state is a `{"number": N, "queued": false}`
22+ //! document so pipeline consumers always get parseable output; in
23+ //! human mode it is the line `PR #N is not in the merge queue`. Live
24+ //! smoke tests assert against that substring, which is stable across
25+ //! the Python and Rust implementations.
2626
2727use std:: io:: Write ;
2828
@@ -119,10 +119,8 @@ pub async fn run(opts: ShowOptions<'_>, output: &mut dyn Output) -> Result<(), C
119119
120120 let raw: Option < serde_json:: Value > = client. get_if_exists ( & path) . await ?;
121121 let Some ( raw) = raw else {
122- return Err ( CliError :: MergifyApi ( format ! (
123- "PR #{n} is not in the merge queue" ,
124- n = opts. pr_number,
125- ) ) ) ;
122+ emit_not_queued ( output, opts. pr_number , opts. output_json ) ?;
123+ return Ok ( ( ) ) ;
126124 } ;
127125
128126 if opts. output_json {
@@ -136,6 +134,28 @@ pub async fn run(opts: ShowOptions<'_>, output: &mut dyn Output) -> Result<(), C
136134 Ok ( ( ) )
137135}
138136
137+ /// Emit the "PR is not in the merge queue" state. This is a normal
138+ /// answer, not a failure, so the command exits 0 — see the module
139+ /// docs. Under `--json` we emit a `{number, queued: false}` document
140+ /// (a machine consumer always gets parseable output); in human mode
141+ /// a single notice line. The wording is load-bearing: live smoke
142+ /// tests assert on the "is not in the merge queue" substring.
143+ fn emit_not_queued (
144+ output : & mut dyn Output ,
145+ pr_number : u64 ,
146+ output_json : bool ,
147+ ) -> Result < ( ) , CliError > {
148+ if output_json {
149+ let payload = serde_json:: json!( { "number" : pr_number, "queued" : false } ) ;
150+ output. emit_json_value ( & payload) ?;
151+ } else {
152+ output. emit ( & ( ) , & mut |w : & mut dyn Write | {
153+ writeln ! ( w, "PR #{pr_number} is not in the merge queue" )
154+ } ) ?;
155+ }
156+ Ok ( ( ) )
157+ }
158+
139159fn emit_human ( output : & mut dyn Output , view : & PullView , verbose : bool ) -> std:: io:: Result < ( ) > {
140160 let now = Utc :: now ( ) ;
141161 let theme = Theme :: detect ( ) ;
@@ -657,7 +677,10 @@ mod tests {
657677 }
658678
659679 #[ tokio:: test]
660- async fn run_404_is_not_in_queue ( ) {
680+ async fn run_404_human_is_not_in_queue_and_succeeds ( ) {
681+ // A not-queued PR is a normal queryable state, not an API
682+ // failure: human mode prints the notice to stdout and the
683+ // command returns Ok (exit 0).
661684 let server = MockServer :: start ( ) . await ;
662685 Mock :: given ( method ( "GET" ) )
663686 . and ( path ( "/v1/repos/owner/repo/merge-queue/pull/999" ) )
@@ -668,7 +691,7 @@ mod tests {
668691
669692 let mut cap = Captured :: human ( ) ;
670693 let api_url = server. uri ( ) ;
671- let err = run (
694+ run (
672695 ShowOptions {
673696 repository : Some ( "owner/repo" ) ,
674697 token : Some ( "t" ) ,
@@ -680,19 +703,51 @@ mod tests {
680703 & mut cap. output ,
681704 )
682705 . await
683- . unwrap_err ( ) ;
706+ . unwrap ( ) ;
684707
708+ let stdout = cap. stdout ( ) ;
685709 assert ! (
686- matches!( err, CliError :: MergifyApi ( _) ) ,
687- "expected MergifyApi, got {err:?}" ,
688- ) ;
689- assert_eq ! ( err. exit_code( ) , mergify_core:: ExitCode :: MergifyApiError ) ;
690- assert ! (
691- err. to_string( ) . contains( "not in the merge queue" ) ,
692- "got: {err}"
710+ stdout. contains( "PR #999 is not in the merge queue" ) ,
711+ "got: {stdout:?}" ,
693712 ) ;
694713 }
695714
715+ #[ tokio:: test]
716+ async fn run_404_json_emits_not_queued_document ( ) {
717+ // Under `--json`, the not-queued state is a parseable
718+ // `{number, queued: false}` document on stdout (exit 0), so
719+ // pipeline consumers never get empty output for the common
720+ // case.
721+ let server = MockServer :: start ( ) . await ;
722+ Mock :: given ( method ( "GET" ) )
723+ . and ( path ( "/v1/repos/owner/repo/merge-queue/pull/999" ) )
724+ . respond_with ( ResponseTemplate :: new ( 404 ) )
725+ . expect ( 1 )
726+ . mount ( & server)
727+ . await ;
728+
729+ let mut cap = Captured :: new ( OutputMode :: Json ) ;
730+ let api_url = server. uri ( ) ;
731+ run (
732+ ShowOptions {
733+ repository : Some ( "owner/repo" ) ,
734+ token : Some ( "t" ) ,
735+ api_url : Some ( & api_url) ,
736+ pr_number : 999 ,
737+ verbose : false ,
738+ output_json : true ,
739+ } ,
740+ & mut cap. output ,
741+ )
742+ . await
743+ . unwrap ( ) ;
744+
745+ let stdout = cap. stdout ( ) ;
746+ let parsed: serde_json:: Value = serde_json:: from_str ( & stdout) . unwrap ( ) ;
747+ assert_eq ! ( parsed[ "number" ] , json!( 999 ) ) ;
748+ assert_eq ! ( parsed[ "queued" ] , json!( false ) ) ;
749+ }
750+
696751 #[ tokio:: test]
697752 async fn run_no_mergeability_check ( ) {
698753 let server = MockServer :: start ( ) . await ;
0 commit comments