git_next_forge_forgejo/
lib.rs

1//
2#[cfg(test)]
3mod tests;
4
5mod webhook;
6
7use std::borrow::ToOwned;
8
9use git_next_core::{
10    self as core,
11    git::{self, forge::commit::Status},
12    server::RepoListenUrl,
13    ForgeNotification, RegisteredWebhook, WebhookAuth, WebhookId,
14};
15
16use kxio::net::Net;
17
18#[derive(Clone, Debug)]
19pub struct ForgeJo {
20    repo_details: git::RepoDetails,
21    net: Net,
22}
23impl ForgeJo {
24    #[must_use]
25    pub const fn new(repo_details: git::RepoDetails, net: Net) -> Self {
26        Self { repo_details, net }
27    }
28}
29#[async_trait::async_trait]
30impl git::ForgeLike for ForgeJo {
31    fn duplicate(&self) -> Box<dyn git::ForgeLike> {
32        Box::new(self.clone())
33    }
34    fn name(&self) -> String {
35        "forgejo".to_string()
36    }
37
38    fn is_message_authorised(&self, msg: &ForgeNotification, expected: &WebhookAuth) -> bool {
39        let authorization = msg.header("authorization");
40        tracing::info!(?authorization, %expected, "is message authorised?");
41        authorization
42            .and_then(|header| header.strip_prefix("Basic ").map(ToOwned::to_owned))
43            .and_then(|value| WebhookAuth::try_new(value.as_str()).ok())
44            .is_some_and(|auth| &auth == expected)
45    }
46
47    fn parse_webhook_body(
48        &self,
49        body: &core::webhook::forge_notification::Body,
50    ) -> git::forge::webhook::Result<core::webhook::Push> {
51        webhook::parse_body(body)
52    }
53
54    async fn commit_status(&self, commit: &git::Commit) -> git::forge::webhook::Result<Status> {
55        let repo_details = &self.repo_details;
56        let hostname = &repo_details.forge.hostname();
57        let repo_path = &repo_details.repo_path;
58        let api_token = &repo_details.forge.token();
59        use secrecy::ExposeSecret;
60        let token = api_token.expose_secret();
61        let url = format!(
62            "https://{hostname}/api/v1/repos/{repo_path}/commits/{commit}/status?token={token}"
63        );
64
65        let Ok(response) = self.net.get(url).send().await else {
66            return Ok(Status::Pending);
67        };
68        let combined_status = response.json::<CombinedStatus>().await.unwrap_or_default();
69        let status = match combined_status.state {
70            ForgejoState::Success => Status::Pass,
71            ForgejoState::Pending | ForgejoState::Blank => Status::Pending,
72            ForgejoState::Failure | ForgejoState::Error => Status::Fail,
73        };
74        Ok(status)
75    }
76
77    async fn list_webhooks(
78        &self,
79        repo_listen_url: &RepoListenUrl,
80    ) -> git::forge::webhook::Result<Vec<WebhookId>> {
81        webhook::list(&self.repo_details, repo_listen_url, &self.net).await
82    }
83
84    async fn unregister_webhook(&self, webhook_id: &WebhookId) -> git::forge::webhook::Result<()> {
85        webhook::unregister(webhook_id, &self.repo_details, &self.net).await
86    }
87
88    async fn register_webhook(
89        &self,
90        repo_listen_url: &RepoListenUrl,
91    ) -> git::forge::webhook::Result<RegisteredWebhook> {
92        webhook::register(&self.repo_details, repo_listen_url, &self.net).await
93    }
94}
95
96#[derive(Debug, Default, serde::Deserialize)]
97struct CombinedStatus {
98    pub state: ForgejoState,
99}
100
101#[derive(Debug, Default, serde::Deserialize)]
102enum ForgejoState {
103    #[serde(rename = "success")]
104    Success,
105    #[serde(rename = "pending")]
106    #[default]
107    Pending,
108    #[serde(rename = "failure")]
109    Failure,
110    #[serde(rename = "error")]
111    Error,
112    #[serde(rename = "")]
113    Blank,
114}