From 6c6c07b50c856167335b9d60e3fee737e944e436 Mon Sep 17 00:00:00 2001 From: Cyborus Date: Tue, 9 Jul 2024 16:47:00 -0400 Subject: [PATCH] feat(pr): status command --- Cargo.lock | 11 +++++ Cargo.toml | 2 +- src/prs.rs | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 77cbdcd..8fe1892 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1186,6 +1186,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -1868,7 +1877,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", diff --git a/Cargo.toml b/Cargo.toml index 6a96148..0ff53c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ serde = { version = "1.0.170", features = ["derive"] } serde_json = "1.0.100" sha256 = "1.5.0" soft_assert = "0.1.1" -time = { version = "0.3.30", features = ["formatting", "macros"] } +time = { version = "0.3.30", features = ["formatting", "local-offset", "macros"] } tokio = { version = "1.29.1", features = ["full"] } url = "2.4.0" uuid = { version = "1.5.0", features = ["v4"] } diff --git a/src/prs.rs b/src/prs.rs index 0a46296..ed4fe2d 100644 --- a/src/prs.rs +++ b/src/prs.rs @@ -68,6 +68,11 @@ pub enum PrSubcommand { #[clap(subcommand)] command: Option, }, + /// View the mergability and CI status of a pull request + Status { + /// The pull request to view. + id: Option, + }, /// Checkout a pull request in a new branch Checkout { /// The pull request to check out. @@ -300,6 +305,7 @@ impl PrCommand { } } } + Status { id } => view_pr_status(&repo, &api, id.map(|id| id.number)).await?, Search { query, labels, @@ -351,6 +357,7 @@ impl PrCommand { Search { repo, .. } | Create { repo, .. } => repo.as_ref(), Checkout { .. } => None, View { id: pr, .. } + | Status { id: pr, .. } | Comment { pr, .. } | Edit { pr, .. } | Close { pr, .. } @@ -373,6 +380,7 @@ impl PrCommand { } } View { id: pr, .. } + | Status { id: pr, .. } | Comment { pr, .. } | Edit { pr, .. } | Close { pr, .. } @@ -578,6 +586,121 @@ fn darken(r: u8, g: u8, b: u8) -> (u8, u8, u8) { ) } +async fn view_pr_status(repo: &RepoName, api: &Forgejo, id: Option) -> eyre::Result<()> { + let pr = try_get_pr(repo, api, id).await?; + + let SpecialRender { + bright_magenta, + bright_red, + bright_green, + yellow, + light_grey, + dash, + bullet, + reset, + .. + } = *crate::special_render(); + + if pr.merged.ok_or_eyre("pr merge status unknown")? { + let merged_by = pr.merged_by.ok_or_eyre("pr not merged by anyone")?; + let merged_by = merged_by + .login + .as_deref() + .ok_or_eyre("pr merger does not have login")?; + let merged_at = pr.merged_at.ok_or_eyre("pr does not have merge date")?; + let date_format = time::macros::format_description!( + "on [month repr:long] [day], [year], at [hour repr:12]:[minute] [period]" + ); + let tz_format = time::macros::format_description!( + "[offset_hour padding:zero sign:mandatory]:[offset_minute]" + ); + let (merged_at, show_tz) = if let Ok(local_offset) = time::UtcOffset::current_local_offset() + { + let merged_at = merged_at.to_offset(local_offset); + (merged_at, false) + } else { + (merged_at, true) + }; + print!( + "{bright_magenta}Merged{reset} by {merged_by} {}", + merged_at.format(date_format)? + ); + if show_tz { + print!("{}", merged_at.format(tz_format)?); + } + println!(); + } else { + let pr_number = pr.number.ok_or_eyre("pr does not have number")?; + let query = forgejo_api::structs::RepoGetPullRequestCommitsQuery { + page: None, + limit: Some(u32::MAX), + verification: Some(false), + files: Some(false), + }; + let (_commit_headers, commits) = api + .repo_get_pull_request_commits(repo.owner(), repo.name(), pr_number, query) + .await?; + let latest_commit = commits + .iter() + .max_by_key(|x| x.created) + .ok_or_eyre("no commits in pr")?; + let sha = latest_commit + .sha + .as_deref() + .ok_or_eyre("commit does not have sha")?; + let query = forgejo_api::structs::RepoGetCombinedStatusByRefQuery { + page: None, + limit: Some(u32::MAX), + }; + let combined_status = api + .repo_get_combined_status_by_ref(repo.owner(), repo.name(), sha, query) + .await?; + + let state = pr.state.ok_or_eyre("pr does not have state")?; + let is_draft = pr.title.as_deref().is_some_and(|s| s.starts_with("WIP:")); + match state { + StateType::Open => { + if is_draft { + println!("{light_grey}Draft{reset} {dash} Can't merge draft PR") + } else { + print!("{bright_green}Open{reset} {dash} "); + let mergable = pr.mergeable.ok_or_eyre("pr does not have mergable")?; + if mergable { + println!("Can be merged"); + } else { + println!("{bright_red}Merge conflicts{reset}"); + } + } + } + StateType::Closed => println!("{bright_red}Closed{reset} {dash} Reopen to merge"), + } + + let commit_statuses = combined_status + .statuses + .ok_or_eyre("combined status does not have status list")?; + for status in commit_statuses { + let state = status + .status + .as_deref() + .ok_or_eyre("status does not have status")?; + let context = status + .context + .as_deref() + .ok_or_eyre("status does not have context")?; + print!("{bullet} "); + match state { + "success" => print!("{bright_green}Success{reset}"), + "pending" => print!("{yellow}Pending{reset}"), + "failure" => print!("{bright_red}Failure{reset}"), + _ => eyre::bail!("invalid status"), + }; + println!(" {dash} {context}"); + } + } + + Ok(()) +} + async fn edit_pr_labels( repo: &RepoName, api: &Forgejo,