// 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::{ IssueAction as GlIssueAction, MergeRequestAction, 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 { /// 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, } /// 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), } impl TryFrom for Hook { type Error = Error; fn try_from(hook: GitlabHook) -> Result { Ok(match hook { GitlabHook::Push(push) => Hook::Push(Push { ref_: push.ref_, object_kind: push.object_kind, commits: push .commits .into_iter() .map(|commit| Commit { message: commit.message, url: commit.url, }) .collect(), repository: Repository { name: push.project.name, }, pusher: User { name: push.user_name, }, }), GitlabHook::Issue(issue) => Hook::Issue(Issue { action: issue.object_attributes.action.map(|action| match action { GlIssueAction::Update => IssueAction::Update, GlIssueAction::Open => IssueAction::Open, GlIssueAction::Close => IssueAction::Close, GlIssueAction::Reopen => IssueAction::Reopen, }), repository: Repository { name: issue.project.name, }, author: User { name: issue.user.name, }, id: issue.object_attributes.iid, title: issue.object_attributes.title, url: issue.object_attributes.url, }), _ => 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 { 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.object_attributes.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.object_attributes.action ); return None; } }; format!( "[{}] {} {} merge request {}: {}{}", merge_req.project.name, merge_req.user.name, action, merge_req.object_attributes.iid, merge_req.object_attributes.title, merge_req .object_attributes .url .as_ref() .map(|url| format!(" <{}>", url)) .unwrap_or("".to_owned()) ) } Hook::Note(note) => { if let Some(_) = note.snippet { return None; } if let Some(commit) = ¬e.commit { format!( "[{}] {} commented on commit {:?} <{}>", note.project.name, note.user.name, commit.id, commit.url, ) } else if let Some(issue) = ¬e.issue { format!( "[{}] {} commented on issue {}: {} <{}>", note.project.name, note.user.name, issue.iid, issue.title, note.object_attributes.url, ) } else if let Some(mr) = ¬e.merge_request { format!( "[{}] {} commented on merge request {}: {} <{}>", note.project.name, note.user.name, mr.iid, mr.title, note.object_attributes.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; } */ }) }