pub mod forgejo;
pub mod github;
pub mod gitlab;
pub mod test;
use std::borrow::Cow;
use bon::Builder;
use enum_dispatch::enum_dispatch;
use serde::{Deserialize, Serialize};
use crate::{
config::{Config, ForgeType},
description::FormatMergeRequest,
error::Result,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForgeUser {
pub id: Option<String>,
pub username: String,
}
#[derive(Debug, Clone)]
pub enum ForgeMergeRequest {
GitLab(gitlab::MergeRequest),
GitHub(github::PullRequest),
Forgejo(forgejo::PullRequest),
Test(test::MergeRequest),
}
impl ForgeMergeRequest {
pub fn iid(&self) -> Cow<'_, str> {
match self {
ForgeMergeRequest::GitLab(mr) => Cow::Owned(mr.iid.to_string()),
ForgeMergeRequest::GitHub(pr) => Cow::Owned(pr.number.to_string()),
ForgeMergeRequest::Forgejo(pr) => Cow::Owned(pr.number.to_string()),
ForgeMergeRequest::Test(mr) => Cow::Owned(mr.id.clone()),
}
}
pub fn title(&self) -> &str {
match self {
ForgeMergeRequest::GitLab(mr) => &mr.title,
ForgeMergeRequest::GitHub(pr) => &pr.title,
ForgeMergeRequest::Forgejo(pr) => &pr.title,
ForgeMergeRequest::Test(mr) => &mr.title,
}
}
pub fn description(&self) -> &str {
match self {
ForgeMergeRequest::GitLab(mr) => mr.description.as_deref().unwrap_or(""),
ForgeMergeRequest::GitHub(pr) => pr.body.as_deref().unwrap_or(""),
ForgeMergeRequest::Forgejo(pr) => pr.body.as_deref().unwrap_or(""),
ForgeMergeRequest::Test(mr) => mr.description.as_deref().unwrap_or(""),
}
}
pub fn source_branch(&self) -> &str {
match self {
ForgeMergeRequest::GitLab(mr) => &mr.source_branch,
ForgeMergeRequest::GitHub(pr) => &pr.head.ref_name,
ForgeMergeRequest::Forgejo(pr) => &pr.head.ref_name,
ForgeMergeRequest::Test(mr) => &mr.source_branch,
}
}
pub fn target_branch(&self) -> &str {
match self {
ForgeMergeRequest::GitLab(mr) => &mr.target_branch,
ForgeMergeRequest::GitHub(pr) => &pr.base.ref_name,
ForgeMergeRequest::Forgejo(pr) => &pr.base.ref_name,
ForgeMergeRequest::Test(mr) => &mr.target_branch,
}
}
pub fn state(&self) -> ForgeMergeRequestState {
match self {
ForgeMergeRequest::GitLab(mr) => {
if mr.state == "opened" {
ForgeMergeRequestState::Open
} else if mr.state == "closed" {
ForgeMergeRequestState::Closed
} else if mr.state == "merged" {
ForgeMergeRequestState::Merged
} else {
ForgeMergeRequestState::Open
}
}
ForgeMergeRequest::GitHub(pr) => {
if pr.merged {
ForgeMergeRequestState::Merged
} else if pr.state == "open" {
ForgeMergeRequestState::Open
} else {
ForgeMergeRequestState::Closed
}
}
ForgeMergeRequest::Forgejo(pr) => {
if pr.merged {
ForgeMergeRequestState::Merged
} else if pr.state == "open" {
ForgeMergeRequestState::Open
} else {
ForgeMergeRequestState::Closed
}
}
ForgeMergeRequest::Test(mr) => mr.state,
}
}
pub fn url(&self) -> &str {
match self {
ForgeMergeRequest::GitLab(mr) => &mr.web_url,
ForgeMergeRequest::GitHub(pr) => &pr.html_url,
ForgeMergeRequest::Forgejo(pr) => &pr.html_url,
ForgeMergeRequest::Test(mr) => &mr.url,
}
}
pub fn edit_url(&self) -> Cow<'_, str> {
match self {
ForgeMergeRequest::GitLab(mr) => Cow::Owned(format!("{}/edit", mr.web_url)),
ForgeMergeRequest::GitHub(pr) => Cow::Borrowed(&pr.html_url),
ForgeMergeRequest::Forgejo(pr) => Cow::Borrowed(&pr.html_url),
ForgeMergeRequest::Test(mr) => Cow::Borrowed(&mr.url),
}
}
pub fn author_username(&self) -> &str {
match self {
ForgeMergeRequest::GitLab(mr) => &mr.author.username,
ForgeMergeRequest::GitHub(pr) => &pr.user.login,
ForgeMergeRequest::Forgejo(pr) => &pr.user.login,
ForgeMergeRequest::Test(mr) => &mr.author_username,
}
}
pub fn created_at(&self) -> jiff::Timestamp {
match self {
ForgeMergeRequest::GitLab(mr) => mr
.created_at
.parse()
.expect("Failed to parse created at timestamp from GitLab API response"),
ForgeMergeRequest::GitHub(pr) => pr
.created_at
.parse()
.expect("Failed to parse created at timestamp from GitHub API response"),
ForgeMergeRequest::Forgejo(pr) => pr
.created_at
.parse()
.expect("Failed to parse created at timestamp from Forgejo API response"),
ForgeMergeRequest::Test(mr) => mr.created_at,
}
}
pub fn assignees(&self) -> Vec<ForgeUser> {
match self {
ForgeMergeRequest::GitLab(mr) => mr
.assignees
.clone()
.into_iter()
.map(ForgeUser::from)
.collect(),
ForgeMergeRequest::GitHub(pr) => pr
.assignees
.clone()
.into_iter()
.map(ForgeUser::from)
.collect(),
ForgeMergeRequest::Forgejo(pr) => pr
.assignees
.clone()
.unwrap_or_default()
.into_iter()
.map(ForgeUser::from)
.collect(),
ForgeMergeRequest::Test(mr) => mr.assignees.clone(),
}
}
pub fn reviewers(&self) -> Vec<ForgeUser> {
match self {
ForgeMergeRequest::GitLab(mr) => mr
.reviewers
.clone()
.into_iter()
.map(ForgeUser::from)
.collect(),
ForgeMergeRequest::GitHub(pr) => pr
.requested_reviewers
.clone()
.into_iter()
.map(ForgeUser::from)
.collect(),
ForgeMergeRequest::Forgejo(pr) => pr
.requested_reviewers
.clone()
.unwrap_or_default()
.into_iter()
.map(ForgeUser::from)
.collect(),
ForgeMergeRequest::Test(mr) => mr.reviewers.clone(),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum ForgeMergeRequestState {
#[default]
Open,
Closed,
Merged,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum CheckStatus {
Success,
Pending,
Failed,
#[default]
None,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApprovalSatisfaction {
Satisfied,
Unsatisfied,
Unknown,
}
#[derive(Debug, Clone)]
pub struct ApprovalStatus {
pub approved_count: u32,
pub required_count: u32,
pub blocking_count: u32,
pub satisfaction: ApprovalSatisfaction,
}
impl Default for ApprovalStatus {
fn default() -> Self {
Self {
approved_count: 0,
required_count: 0,
blocking_count: 0,
satisfaction: ApprovalSatisfaction::Unknown,
}
}
}
#[derive(Debug, Clone)]
pub struct MergeRequestStatus {
pub iid: String,
pub check_status: CheckStatus,
pub approval_status: ApprovalStatus,
}
impl MergeRequestStatus {
pub fn ready_to_merge(&self) -> bool {
self.approval_status.satisfaction == ApprovalSatisfaction::Satisfied
&& (self.check_status == CheckStatus::Success || self.check_status == CheckStatus::None)
}
}
#[derive(Builder, Default)]
pub struct ForgeCreateMergeRequestOptions {
pub source_branch: String,
pub target_branch: String,
pub title: String,
#[builder(required)]
pub description: Option<String>,
pub assignee_usernames: Vec<String>,
pub reviewer_usernames: Vec<String>,
pub remove_source_branch: bool,
pub squash: bool,
pub open_as_draft: bool,
}
#[derive(Debug, Clone, Default)]
pub struct DiscussionCount {
pub all: u32,
pub unresolved: u32,
pub resolved: u32,
}
#[enum_dispatch]
pub trait Forge: Send + Sync + FormatMergeRequest {
fn project_id(&self) -> &str;
fn source_project_id(&self) -> &str;
fn target_project_id(&self) -> &str;
fn base_url(&self) -> &str;
fn project_url(&self) -> String {
format!("{}/{}", self.base_url(), self.project_id())
}
async fn current_user(&self) -> Result<ForgeUser>;
async fn user_by_username(&self, username: &str) -> Result<Option<ForgeUser>>;
async fn find_merge_request_by_source_branch(
&self,
branch: &str,
) -> Result<Option<ForgeMergeRequest>>;
async fn create_merge_request(
&self,
options: ForgeCreateMergeRequestOptions,
) -> Result<ForgeMergeRequest>;
async fn update_merge_request_base(
&self,
merge_request_iid: &str,
new_base: &str,
) -> Result<ForgeMergeRequest>;
async fn update_merge_request_description(
&self,
merge_request_iid: &str,
new_description: &str,
) -> Result<ForgeMergeRequest>;
async fn get_merge_request(&self, merge_request_iid: &str) -> Result<ForgeMergeRequest>;
async fn get_approval_status(&self, merge_request_iid: &str) -> Result<ApprovalStatus>;
async fn get_check_status(&self, merge_request_iid: &str) -> Result<CheckStatus>;
async fn get_merge_request_status(&self, merge_request_iid: &str)
-> Result<MergeRequestStatus>;
async fn num_open_discussions(&self, merge_request_iid: &str) -> Result<DiscussionCount>;
}
#[enum_dispatch(Forge, FormatMergeRequest)]
pub enum ForgeImpl {
GitLab(gitlab::GitLabForge),
GitHub(github::GitHubForge),
Forgejo(forgejo::ForgejoForge),
Test(test::TestForge),
}
impl ForgeImpl {
pub fn from_cwd() -> Result<Self> {
let cwd = std::env::current_dir()?;
let config = Config::load(&cwd)?;
Self::new(&config)
}
pub fn new(config: &Config) -> Result<Self> {
config.validate()?;
match config.forge {
ForgeType::GitLab => {
let source = config.gitlab.source_project();
let target = config.gitlab.target_project();
gitlab::GitLabForge::new(
config.gitlab.host.clone(),
source.to_string(),
target.to_string(),
config.gitlab.token.clone(),
config.ca_bundle.clone(),
config.tls_accept_non_compliant_certs,
)
.map(|forge| forge.into())
}
ForgeType::GitHub => {
let source = config.github.source_project();
let target = config.github.target_project();
github::GitHubForge::new(
config.github.host.clone(),
source.to_string(),
target.to_string(),
config.github.token.clone(),
config.ca_bundle.clone(),
config.tls_accept_non_compliant_certs,
)
.map(|forge| forge.into())
}
ForgeType::Forgejo => {
let source = config.forgejo.source_project();
let target = config.forgejo.target_project();
forgejo::ForgejoForge::new(
config.forgejo.host.clone(),
source.to_string(),
target.to_string(),
config.forgejo.token.clone(),
config.ca_bundle.clone(),
config.tls_accept_non_compliant_certs,
config.forgejo.wip_prefix.clone(),
)
.map(|forge| forge.into())
}
}
}
}