use std::path::Path;
use processkit::ProcessRunner;
use vcs_gitlab::{
CiStatus as GlCi, GitLab, GitLabApi, Issue, MergeRequest, MergeStrategy as GlMs, MrCreate,
Project, Release,
};
use crate::dto::{
CiStatus, ForgeIssue, ForgeIssueState, ForgePr, ForgePrState, ForgeRelease, ForgeRepo,
MergeStrategy, PrCreate,
};
use crate::error::Result;
pub(crate) async fn auth_status<R: ProcessRunner>(glab: &GitLab<R>) -> Result<bool> {
Ok(glab.auth_status().await?)
}
pub(crate) async fn repo_view<R: ProcessRunner>(glab: &GitLab<R>, dir: &Path) -> Result<ForgeRepo> {
Ok(map_project(glab.repo_view(dir).await?))
}
pub(crate) async fn pr_list<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
) -> Result<Vec<ForgePr>> {
Ok(glab.mr_list(dir).await?.into_iter().map(map_mr).collect())
}
pub(crate) async fn pr_view<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
number: u64,
) -> Result<ForgePr> {
Ok(map_mr(glab.mr_view(dir, number).await?))
}
pub(crate) async fn pr_create<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
spec: PrCreate,
) -> Result<String> {
let mut create = MrCreate::new(spec.title, spec.body);
if let Some(source) = spec.source {
create = create.source(source);
}
if let Some(target) = spec.target {
create = create.target(target);
}
Ok(glab.mr_create(dir, create).await?)
}
pub(crate) async fn pr_merge<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
number: u64,
strategy: MergeStrategy,
) -> Result<()> {
let ms = match strategy {
MergeStrategy::Merge => GlMs::Merge,
MergeStrategy::Squash => GlMs::Squash,
MergeStrategy::Rebase => GlMs::Rebase,
};
glab.mr_merge(dir, number, ms).await?;
Ok(())
}
pub(crate) async fn pr_mark_ready<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
number: u64,
) -> Result<()> {
glab.mr_ready(dir, number).await?;
Ok(())
}
pub(crate) async fn pr_close<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
number: u64,
) -> Result<()> {
glab.mr_close(dir, number).await?;
Ok(())
}
pub(crate) async fn pr_checks<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
number: u64,
) -> Result<CiStatus> {
Ok(map_ci(glab.mr_checks(dir, number).await?))
}
pub(crate) async fn issue_list<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
) -> Result<Vec<ForgeIssue>> {
Ok(glab
.issue_list(dir)
.await?
.into_iter()
.map(map_issue)
.collect())
}
pub(crate) async fn issue_view<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
number: u64,
) -> Result<ForgeIssue> {
Ok(map_issue(glab.issue_view(dir, number).await?))
}
pub(crate) async fn issue_create<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
title: &str,
body: &str,
) -> Result<String> {
Ok(glab.issue_create(dir, title, body).await?)
}
pub(crate) async fn release_list<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
) -> Result<Vec<ForgeRelease>> {
Ok(glab
.release_list(dir)
.await?
.into_iter()
.map(map_release)
.collect())
}
pub(crate) async fn release_view<R: ProcessRunner>(
glab: &GitLab<R>,
dir: &Path,
tag: &str,
) -> Result<ForgeRelease> {
Ok(map_release(glab.release_view(dir, tag).await?))
}
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_mr(mr: MergeRequest) -> ForgePr {
ForgePr {
number: mr.iid,
state: state_of(&mr.state),
title: mr.title,
source_branch: mr.source_branch,
target_branch: mr.target_branch,
url: mr.web_url,
draft: mr.draft,
}
}
fn state_of(state: &str) -> ForgePrState {
match state.to_ascii_lowercase().as_str() {
"merged" => ForgePrState::Merged,
"closed" | "locked" => ForgePrState::Closed,
_ => ForgePrState::Open,
}
}
fn map_project(p: Project) -> ForgeRepo {
let owner = p
.path_with_namespace
.rsplit_once('/')
.map(|(ns, _)| ns.to_string())
.unwrap_or_default();
ForgeRepo {
name: p.name,
owner,
default_branch: p.default_branch,
url: p.web_url,
private: p.visibility.as_deref().is_some_and(|v| v != "public"),
}
}
fn map_ci(c: GlCi) -> CiStatus {
match c {
GlCi::Passing => CiStatus::Passing,
GlCi::Failing => CiStatus::Failing,
GlCi::Pending => CiStatus::Pending,
GlCi::None => CiStatus::None,
_ => CiStatus::Pending,
}
}
#[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_lowercase().as_str() {
"merged" => prop_assert_eq!(mapped, ForgePrState::Merged),
"closed" | "locked" => prop_assert_eq!(mapped, ForgePrState::Closed),
_ => prop_assert_eq!(mapped, ForgePrState::Open, "unknown must default to Open: {:?}", s),
}
}
}
}