use std::path::Path;
use processkit::ProcessRunner;
use vcs_github::{
CheckRun, GitHub, GitHubApi, Issue, PrCreate as GhPrCreate, PrMerge, PullRequest, Release, Repo,
};
use crate::dto::{
CiStatus, ForgeIssue, ForgeIssueState, ForgePr, ForgePrState, ForgeRelease, ForgeRepo,
MergeStrategy, PrCreate,
};
use crate::error::Result;
pub(crate) async fn auth_status<R: ProcessRunner>(gh: &GitHub<R>) -> Result<bool> {
Ok(gh.auth_status().await?)
}
pub(crate) async fn repo_view<R: ProcessRunner>(gh: &GitHub<R>, dir: &Path) -> Result<ForgeRepo> {
Ok(map_repo(gh.repo_view(dir).await?))
}
pub(crate) async fn pr_list<R: ProcessRunner>(gh: &GitHub<R>, dir: &Path) -> Result<Vec<ForgePr>> {
Ok(gh.pr_list(dir).await?.into_iter().map(map_pr).collect())
}
pub(crate) async fn pr_view<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
number: u64,
) -> Result<ForgePr> {
Ok(map_pr(gh.pr_view(dir, number).await?))
}
pub(crate) async fn pr_create<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
spec: PrCreate,
) -> Result<String> {
let mut create = GhPrCreate::new(spec.title, spec.body);
if let Some(source) = spec.source {
create = create.head(source);
}
if let Some(target) = spec.target {
create = create.base(target);
}
Ok(gh.pr_create(dir, create).await?)
}
pub(crate) async fn pr_merge<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
number: u64,
strategy: MergeStrategy,
) -> Result<()> {
let merge = match strategy {
MergeStrategy::Merge => PrMerge::merge(),
MergeStrategy::Squash => PrMerge::squash(),
MergeStrategy::Rebase => PrMerge::rebase(),
};
gh.pr_merge(dir, number, merge).await?;
Ok(())
}
pub(crate) async fn pr_mark_ready<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
number: u64,
) -> Result<()> {
gh.pr_ready(dir, number).await?;
Ok(())
}
pub(crate) async fn pr_close<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
number: u64,
delete_branch: bool,
) -> Result<()> {
gh.pr_close(dir, number, delete_branch).await?;
Ok(())
}
pub(crate) async fn pr_checks<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
number: u64,
) -> Result<CiStatus> {
Ok(aggregate(&gh.pr_checks(dir, number).await?))
}
pub(crate) async fn issue_list<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
) -> Result<Vec<ForgeIssue>> {
Ok(gh
.issue_list(dir)
.await?
.into_iter()
.map(map_issue)
.collect())
}
pub(crate) async fn issue_view<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
number: u64,
) -> Result<ForgeIssue> {
Ok(map_issue(gh.issue_view(dir, number).await?))
}
pub(crate) async fn issue_create<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
title: &str,
body: &str,
) -> Result<String> {
Ok(gh.issue_create(dir, title, body).await?)
}
pub(crate) async fn release_list<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
) -> Result<Vec<ForgeRelease>> {
Ok(gh
.release_list(dir)
.await?
.into_iter()
.map(map_release)
.collect())
}
pub(crate) async fn release_view<R: ProcessRunner>(
gh: &GitHub<R>,
dir: &Path,
tag: &str,
) -> Result<ForgeRelease> {
Ok(map_release(gh.release_view(dir, tag).await?))
}
fn map_pr(pr: PullRequest) -> ForgePr {
ForgePr {
number: pr.number,
state: state_of(&pr.state),
title: pr.title,
source_branch: pr.head_ref_name,
target_branch: pr.base_ref_name,
url: pr.url,
draft: false,
}
}
fn state_of(state: &str) -> ForgePrState {
match state.to_ascii_uppercase().as_str() {
"MERGED" => ForgePrState::Merged,
"CLOSED" => ForgePrState::Closed,
_ => ForgePrState::Open,
}
}
fn map_issue(i: Issue) -> ForgeIssue {
ForgeIssue {
number: i.number,
title: i.title,
state: issue_state_of(&i.state),
body: i.body,
url: i.url,
}
}
fn issue_state_of(state: &str) -> ForgeIssueState {
if state.eq_ignore_ascii_case("closed") {
ForgeIssueState::Closed
} else {
ForgeIssueState::Open
}
}
fn map_release(r: Release) -> ForgeRelease {
ForgeRelease {
tag: r.tag_name,
title: r.name,
url: r.url,
published_at: Some(r.published_at).filter(|s| !s.is_empty()),
}
}
fn map_repo(r: Repo) -> ForgeRepo {
ForgeRepo {
name: r.name,
owner: r.owner,
default_branch: r.default_branch,
url: r.url,
private: r.is_private,
}
}
fn aggregate(checks: &[CheckRun]) -> CiStatus {
let mut any_pending = false;
let mut any_pass = false;
for c in checks {
match c.bucket.as_str() {
"fail" | "cancel" => return CiStatus::Failing,
"pending" => any_pending = true,
"pass" => any_pass = true,
_ => {} }
}
if any_pending {
CiStatus::Pending
} else if any_pass {
CiStatus::Passing
} else {
CiStatus::None
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn issue_state_mapping_never_panics_and_unknowns_default(s in any::<String>()) {
let mapped = issue_state_of(&s);
if s.eq_ignore_ascii_case("closed") {
prop_assert_eq!(mapped, ForgeIssueState::Closed);
} else {
prop_assert_eq!(mapped, ForgeIssueState::Open, "unknown must default to Open: {:?}", s);
}
}
#[test]
fn pr_state_mapping_never_panics_and_unknowns_default(s in any::<String>()) {
let mapped = state_of(&s);
match s.to_ascii_uppercase().as_str() {
"MERGED" => prop_assert_eq!(mapped, ForgePrState::Merged),
"CLOSED" => prop_assert_eq!(mapped, ForgePrState::Closed),
_ => prop_assert_eq!(mapped, ForgePrState::Open, "unknown must default to Open: {:?}", s),
}
}
}
}