// Copyright (C) 2023-2099 The crate authors. // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU Affero General Public License as published by the // Free Software Foundation, either version 3 of the License, or (at your // option) any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License // for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use crate::Error; pub use forgejo_hooks::Hook as ForgejoHook; pub use gitlab::webhooks::{ CommitHookAttrs, IssueAction as GlIssueAction, IssueHook as GlIssueHook, IssueHookAttrs as GlIssueHookAttrs, MergeRequestAction as GlMergeRequestAction, MergeRequestHook as GlMergeRequestHook, MergeRequestHookAttrs as GlMergeRequestHookAttrs, NoteHook as GlNoteHook, PushHook as GlPushHook, WebHook as GitlabHook, WikiPageAction, }; use log::debug; /// Defines a generic user that can be used for many purposes. #[derive(Debug, Clone)] pub(crate) struct User { /// Name of the user name: String, } #[derive(Debug, Clone)] pub(crate) struct Commit { /// Reference of the commit ref_: String, /// Commit message message: String, /// URL where the commit can be read at url: String, } #[derive(Debug, Clone)] pub(crate) struct Repository { /// Name of the project. name: String, } #[derive(Debug, Clone)] pub(crate) struct Push { /// Reference where commits have been pushed to. ref_: String, /// The event which occured. object_kind: String, /// Commit list. commits: Vec, /// Project repository. repository: Repository, /// Person who pushed the commits. It isn't necessarily the same as commit authors. pusher: User, } #[derive(Debug, Clone)] pub(crate) enum IssueAction { Update, Open, Close, Reopen, } #[derive(Debug, Clone)] pub(crate) struct Issue { action: Option, repository: Repository, author: User, id: u64, title: String, url: Option, } #[derive(Debug, Clone)] pub(crate) struct IssueAttrs { id: u64, title: String, } #[derive(Debug, Clone)] pub(crate) enum MergeRequestAction { Update, Open, Close, Reopen, Merge, Approved, Unapproved, Approval, Unapproval, } #[derive(Debug, Clone)] pub(crate) struct MergeRequest { action: Option, repository: Repository, author: User, id: u64, title: String, url: Option, } #[derive(Debug, Clone)] pub(crate) struct MergeRequestAttrs { id: u64, title: String, } #[derive(Debug, Clone)] pub(crate) struct Note { snippet: bool, commit: Option, issue: Option, merge_request: Option, repository: Repository, author: User, url: String, } /// Lowest common denominator struct so that we don't have to duplicate our code for each platform /// we support. #[derive(Debug)] pub(crate) enum Hook { /// Push event Push(Push), Issue(Issue), MergeRequest(MergeRequest), Note(Note), } impl From for Commit { fn from(other: CommitHookAttrs) -> Commit { Commit { ref_: other.id, message: other.message, url: other.url, } } } impl From for Push { fn from(other: GlPushHook) -> Push { Push { ref_: other.ref_, object_kind: other.object_kind, commits: other.commits.into_iter().map(Into::into).collect(), repository: Repository { name: other.project.name, }, pusher: User { name: other.user_name, }, } } } impl From for IssueAction { fn from(other: GlIssueAction) -> IssueAction { match other { GlIssueAction::Update => IssueAction::Update, GlIssueAction::Open => IssueAction::Open, GlIssueAction::Close => IssueAction::Close, GlIssueAction::Reopen => IssueAction::Reopen, } } } impl From for Issue { fn from(other: GlIssueHook) -> Issue { Issue { action: other.object_attributes.action.map(Into::into), repository: Repository { name: other.project.name, }, author: User { name: other.user.name, }, id: other.object_attributes.iid, title: other.object_attributes.title, url: other.object_attributes.url, } } } impl From for IssueAttrs { fn from(other: GlIssueHookAttrs) -> IssueAttrs { IssueAttrs { id: other.iid, title: other.title, } } } impl From for MergeRequestAction { fn from(other: GlMergeRequestAction) -> MergeRequestAction { match other { GlMergeRequestAction::Update => MergeRequestAction::Update, GlMergeRequestAction::Open => MergeRequestAction::Open, GlMergeRequestAction::Close => MergeRequestAction::Close, GlMergeRequestAction::Reopen => MergeRequestAction::Reopen, GlMergeRequestAction::Merge => MergeRequestAction::Merge, GlMergeRequestAction::Approved => MergeRequestAction::Approved, GlMergeRequestAction::Unapproved => MergeRequestAction::Unapproved, GlMergeRequestAction::Approval => MergeRequestAction::Approval, GlMergeRequestAction::Unapproval => MergeRequestAction::Unapproval, } } } impl From for MergeRequest { fn from(other: GlMergeRequestHook) -> MergeRequest { MergeRequest { action: other.object_attributes.action.map(Into::into), repository: Repository { name: other.project.name, }, author: User { name: other.user.name, }, id: other.object_attributes.iid, title: other.object_attributes.title, url: other.object_attributes.url, } } } impl From for MergeRequestAttrs { fn from(other: GlMergeRequestHookAttrs) -> MergeRequestAttrs { MergeRequestAttrs { id: other.id, title: other.title, } } } impl From for Note { fn from(other: GlNoteHook) -> Note { Note { snippet: other.snippet.is_some(), commit: other.commit.map(Into::into), issue: other.issue.map(Into::into), merge_request: other.merge_request.map(Into::into), repository: Repository { name: other.project.name, }, author: User { name: other.user.name, }, url: other.object_attributes.url, } } } impl TryFrom for Hook { type Error = Error; fn try_from(hook: GitlabHook) -> Result { Ok(match hook { GitlabHook::Push(push) => Hook::Push((*push).into()), GitlabHook::Issue(issue) => Hook::Issue((*issue).into()), GitlabHook::MergeRequest(mr) => Hook::MergeRequest((*mr).into()), GitlabHook::Note(note) => Hook::Note((*note).into()), _ => return Err(Error::UnsupportedHookConversion), }) } } impl TryFrom for Hook { type Error = Error; fn try_from(hook: ForgejoHook) -> Result { Ok(match hook { ForgejoHook::Push(push) => Hook::Push(Push { ref_: push.ref_, object_kind: String::from("push"), commits: push .commits .into_iter() .map(|commit| Commit { ref_: commit.id, message: commit.message, url: commit.url, }) .collect(), repository: Repository { name: push.repository.name, }, pusher: User { name: push.pusher.login, }, }), _ => return Err(Error::UnsupportedHookConversion), }) } } pub(crate) fn format_hook(hook: &Hook) -> Option { Some(match hook { Hook::Push(push) if push.object_kind == "tag_push" => { let ref_ = push.ref_.strip_prefix("refs/tags/").unwrap_or("?!"); format!( "[{}] {} pushed tag {}.", push.repository.name, push.pusher.name, ref_ ) } Hook::Push(push) => { if push.ref_ != "refs/heads/main" { // Ignore: Action not on 'main' branch return None; } // Unlikely to be reached as 'main' is probably never going to be deleted if push.commits.len() == 0 { // Ignore: Branch got deleted return None; } let mut text = format!( "[{}] {} pushed {} commits to main", push.repository.name, push.pusher.name, push.commits.len(), ); // Display max 3 commits for commit in push.commits.clone().into_iter().take(3) { match commit.message.lines().nth(0) { Some(subject) => { text = format!("{}\n• {} <{}>", text, subject, commit.url); } None => {} } } text } Hook::Issue(issue) => { let action = match issue.action { Some(IssueAction::Update) => return None, Some(IssueAction::Open) => "opened", Some(IssueAction::Close) => "closed", Some(IssueAction::Reopen) => "reopened", None => return None, }; format!( "[{}] {} {} issue {}: {}{}", issue.repository.name, issue.author.name, action, issue.id, issue.title, issue .url .as_ref() .map(|url| format!(" <{}>", url)) .unwrap_or("".to_owned()) ) } Hook::MergeRequest(merge_req) => { let action = match merge_req.action { Some(MergeRequestAction::Update) => return None, Some(MergeRequestAction::Open) => "opened", Some(MergeRequestAction::Close) => "closed", Some(MergeRequestAction::Reopen) => "reopened", Some(MergeRequestAction::Merge) => "merged", None => return None, _ => { log::warn!("Unsupported merge request action: {:?}", merge_req.action); return None; } }; format!( "[{}] {} {} merge request {}: {}{}", merge_req.repository.name, merge_req.author.name, action, merge_req.id, merge_req.title, merge_req .url .as_ref() .map(|url| format!(" <{}>", url)) .unwrap_or("".to_owned()) ) } Hook::Note(note) => { if note.snippet { return None; } if let Some(commit) = ¬e.commit { format!( "[{}] {} commented on commit {:?} <{}>", note.repository.name, note.author.name, commit.ref_, commit.url, ) } else if let Some(issue) = ¬e.issue { format!( "[{}] {} commented on issue {}: {} <{}>", note.repository.name, note.author.name, issue.id, issue.title, note.url, ) } else if let Some(mr) = ¬e.merge_request { format!( "[{}] {} commented on merge request {}: {} <{}>", note.repository.name, note.author.name, mr.id, mr.title, note.url, ) } else { unreachable!() } } /* Hook::Build(build) => { println!("Build: {:?}", build); return None; } Hook::WikiPage(page) => { let action = match page.object_attributes.action { WikiPageAction::Update => "updated", WikiPageAction::Create => "created", }; format!( "[{}] {} {} wiki page {} <{}>", page.project.name, page.user.name, action, page.object_attributes.title, page.object_attributes.url, ) } _ => { debug!("Hook not supported"); return None; } */ }) }