Skip to main content

bb_cli/commands/
pr_build.rs

1use crate::api::models::{BuildState, BuildStatus};
2use crate::api::{self, Client};
3use crate::commands::pr::Ctx;
4use crate::error::Result;
5use crate::output::{self, Format};
6use crate::repo::RepoSlug;
7use futures::stream::{self, StreamExt, TryStreamExt};
8use serde::Serialize;
9use std::collections::HashMap;
10
11/// Bitbucket exposes build status only per pull request, so a column over N rows
12/// costs N requests. Cap how many are in flight: enough to be fast on a busy
13/// repository, few enough to stay clear of the rate limit.
14const MAX_IN_FLIGHT: usize = 8;
15
16pub async fn statuses(client: &Client, slug: &RepoSlug, id: u64) -> Result<Vec<BuildStatus>> {
17    client
18        .paginate(&api::repo_path(
19            slug,
20            &format!("/pullrequests/{id}/statuses"),
21        ))
22        .await
23}
24
25pub async fn statuses_for(
26    client: &Client,
27    slug: &RepoSlug,
28    ids: &[u64],
29) -> Result<HashMap<u64, Vec<BuildStatus>>> {
30    stream::iter(ids.iter().copied())
31        .map(|id| async move { statuses(client, slug, id).await.map(|s| (id, s)) })
32        .buffer_unordered(MAX_IN_FLIGHT)
33        .try_collect()
34        .await
35}
36
37#[derive(Debug, Serialize)]
38struct BuildReport {
39    build_state: BuildState,
40    statuses: Vec<BuildStatus>,
41}
42
43pub async fn run(ctx: &Ctx, id: u64) -> Result<()> {
44    let spinner = output::spinner("fetching build statuses");
45    let found = statuses(&ctx.client, &ctx.slug, id).await?;
46    spinner.finish_and_clear();
47
48    let report = BuildReport {
49        build_state: BuildState::rollup(&found),
50        statuses: found,
51    };
52
53    match ctx.format {
54        Format::Json => output::print_json(&report)?,
55        Format::Human => {
56            output::heading(&format!(
57                "build: {}",
58                output::colored_cell(
59                    report.build_state.label(),
60                    output::tone_for(report.build_state)
61                )
62            ));
63            if report.statuses.is_empty() {
64                output::info("no build statuses");
65            } else {
66                output::print_table(
67                    &["KEY", "NAME", "STATE", "URL"],
68                    report
69                        .statuses
70                        .iter()
71                        .map(|s| {
72                            let state = BuildState::from_api(s.state.as_deref());
73                            vec![
74                                s.key.clone().unwrap_or_else(|| "-".into()),
75                                s.name.clone().unwrap_or_else(|| "-".into()),
76                                output::colored_cell(state.label(), output::tone_for(state)),
77                                s.url.clone().unwrap_or_else(|| "-".into()),
78                            ]
79                        })
80                        .collect(),
81                );
82            }
83        }
84    }
85    Ok(())
86}