GitRoot

Craft your forge, Build your project, Grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5#![no_main]
  6
  7use gitroot_plugin_sdk::{
  8    Plugin, Server,
  9    model::{
 10        Cmd, Commit, Exec, ExecStatus, File, PluginExecRight, PluginRun, PluginRunWhen,
 11        PluginWrite, ReportLevel,
 12    },
 13    register,
 14};
 15
 16pub use gitroot_plugin_sdk::exports::*;
 17pub use gitroot_plugin_sdk::imports::*;
 18use humanize_bytes::humanize_bytes_binary;
 19use millisecond::{Millisecond, MillisecondFormatter};
 20use serde::{Deserialize, Serialize};
 21use std::sync::{Arc, Mutex};
 22
 23struct Hop {
 24    server: Arc<Server>,
 25    conf: Arc<Mutex<Option<Conf>>>,
 26    need_to_run: Arc<Mutex<bool>>,
 27    repo_name: Arc<Mutex<Option<String>>>,
 28}
 29
 30impl Hop {
 31    fn new(server: Arc<Server>) -> Self {
 32        Self {
 33            server,
 34            conf: Arc::new(Mutex::new(None)),
 35            need_to_run: Arc::new(Mutex::new(false)),
 36            repo_name: Arc::new(Mutex::new(None)),
 37        }
 38    }
 39}
 40
 41#[derive(Serialize, Deserialize)]
 42#[serde(rename_all = "camelCase")]
 43struct Conf {
 44    exec: Exec,
 45    pipelines_to_web_dir: String,
 46}
 47
 48impl Hop {
 49    fn format_exec_status(
 50        &self,
 51        status: &ExecStatus,
 52        pipelines_to_web_dir: &String,
 53    ) -> Vec<String> {
 54        let mut output = Vec::new();
 55
 56        let count = status.cmds_exec.len();
 57        if count == 0 {
 58            output.push("❌ No commands executed.".to_string());
 59            return output;
 60        }
 61        if count != status.cmds_status.len()
 62            || count != status.cmds_logs.len()
 63            || count != status.cmds_stats.len()
 64        {
 65            output.push("⚠️ Data Error: Vectors do not have the same length.".to_string());
 66            return output;
 67        }
 68
 69        output.push(format!("Total of **{}** commands processed.", count));
 70        output.push("".to_string());
 71        output.push("| Status | Command | CPU/Mem | Logs |".to_string());
 72        output.push("|:------:|:--------|:-------:|:----:|".to_string());
 73
 74        for i in 0..count {
 75            let cmd: &String = &status.cmds_exec[i];
 76            let status_code = status.cmds_status[i];
 77            let logs = &status.cmds_logs[i];
 78            let stats = &status.cmds_stats[i];
 79
 80            //title
 81            let success_emoji = if status_code == 0 {
 82                "✅".to_string()
 83            } else {
 84                format!("❌ ({})", status_code)
 85            };
 86            let cpu_time = {
 87                let s = Millisecond::from_millis(stats.total_cpu_time_ms).pretty();
 88                if s.is_empty() { "0ms".to_string() } else { s }
 89            };
 90            output.push(format!(
 91                "| {} | {} | {}/{} | [view]({}/{}) |",
 92                success_emoji,
 93                cmd.split(" ").next().unwrap(),
 94                cpu_time,
 95                humanize_bytes_binary!(stats.max_memory_bytes),
 96                pipelines_to_web_dir,
 97                logs
 98            ));
 99
100            //stats
101            // output.push("<details><summary>📊 Execution Statistics</summary>".to_string());
102            // output.push("".to_string());
103
104            // output.push("| Metric | Value |".to_string());
105            // output.push("| :--- | :--- |".to_string());
106
107            // output.push(format!(
108            //     "| **Max Memory** | {} |",
109            //     humanize_bytes_binary!(stats.max_memory_bytes)
110            // ));
111            // output.push(format!(
112            //     "| **Total CPU Time** | {} |",
113            //     Millisecond::from_millis(stats.total_cpu_time_ms)
114            // ));
115            // output.push(format!("| Max Threads | {} |", stats.max_threads));
116            // output.push(format!(
117            //     "| I/O Read | {} |",
118            //     humanize_bytes_binary!(stats.read_bytes_total)
119            // ));
120            // output.push(format!(
121            //     "| I/O Write | {} |",
122            //     humanize_bytes_binary!(stats.write_bytes_total)
123            // ));
124            // output.push("</details>".to_string());
125
126            // //logs
127            // output.push("<details><summary>📜 Logs</summary>".to_string());
128            // output.push("".to_string());
129
130            // if logs.is_empty() {
131            //     output.push("> _No standard or error output._".to_string());
132            // } else {
133            //     output.push("`````text".to_string());
134            //     output.push(logs.trim().to_string());
135            //     output.push("`````".to_string());
136            // }
137            // output.push("</details>".to_string());
138        }
139
140        output
141    }
142
143    fn get_project_url(&self) -> Option<String> {
144        let forge_conf = self.server.forge_conf().ok()?;
145        let repo_lock = self.repo_name.lock().ok()?;
146        let repo_name = repo_lock.as_ref()?;
147
148        let url = if *repo_name == forge_conf.root_repository_name {
149            forge_conf.external_http_addr.clone()
150        } else {
151            format!("{}{}/", forge_conf.external_http_addr, repo_name)
152        };
153
154        Some(url)
155    }
156}
157
158impl Plugin for Hop {
159    fn init(
160        &self,
161        repo_name: String,
162        _conf_has_changed: bool,
163        serialized_conf: String,
164    ) -> Result<(), String> {
165        match self.repo_name.lock() {
166            Ok(mut data) => {
167                *data = Some(repo_name);
168            }
169            Err(poison) => {
170                eprintln!("Mutex empoisonné ! Récupération possible: {:?}", poison);
171            }
172        }
173        serde_json::from_str(serialized_conf.as_str())
174            .map(|conf: Conf| match self.conf.lock() {
175                Ok(mut data) => {
176                    *data = Some(conf);
177                }
178                Err(poison) => {
179                    eprintln!("Mutex empoisonné ! Récupération possible: {:?}", poison);
180                }
181            })
182            .map_err(|err| err.to_string())
183    }
184
185    fn start_commit(&self, _commit: Commit) -> Result<(), String> {
186        Ok(())
187    }
188
189    fn add_file(&self, _file: File) -> Result<(), String> {
190        match self.need_to_run.lock() {
191            Ok(mut data) => {
192                *data = true;
193                Ok(())
194            }
195            Err(poison) => Err(format!("Mutex poisoned add_file {:?}", poison)),
196        }
197    }
198
199    fn mod_file(&self, _file: File) -> Result<(), String> {
200        match self.need_to_run.lock() {
201            Ok(mut data) => {
202                *data = true;
203                Ok(())
204            }
205            Err(poison) => Err(format!("Mutex poisoned mod_file {:?}", poison)),
206        }
207    }
208
209    fn del_file(&self, _file: File) -> Result<(), String> {
210        match self.need_to_run.lock() {
211            Ok(mut data) => {
212                *data = true;
213                Ok(())
214            }
215            Err(poison) => Err(format!("Mutex poisoned del_file {:?}", poison)),
216        }
217    }
218
219    fn end_commit(&self, _commit: Commit) -> Result<(), String> {
220        Ok(())
221    }
222
223    fn finish(&self) -> Result<(), String> {
224        match self.conf.lock() {
225            Ok(data) => match self.need_to_run.lock() {
226                Ok(need_to_run) => {
227                    if *need_to_run {
228                        self.server.log("start exec".to_string());
229                        let conf = data.as_ref().unwrap();
230                        let _ = self.server.exec(&conf.exec).map(|status| {
231                            let repo_url = self.get_project_url().unwrap_or("".to_string());
232                            let content = self.format_exec_status(
233                                &status,
234                                &format!("{}{}", repo_url, conf.pipelines_to_web_dir),
235                            );
236                            let mut artifact_report = Vec::new();
237                            if !conf.pipelines_to_web_dir.is_empty() {
238                                artifact_report.push("".to_string());
239                                status.artifacts.iter().for_each(|artifact| {
240                                    if let Err(e) = self.server.cache().move_file_to_fs(
241                                        artifact,
242                                        self.server.webcontent(),
243                                        format!("{}/{artifact}", conf.pipelines_to_web_dir),
244                                    ) {
245                                        self.server.log_error("can't move artifact to web", e);
246                                    } else {
247                                        artifact_report.push(format!(
248                                            "Artifact accessible at [{artifact}]({}{}/{artifact})",
249                                            repo_url, conf.pipelines_to_web_dir
250                                        ));
251                                    }
252                                });
253                                status.cmds_logs.iter().for_each(|log| {
254                                    if let Err(e) = self.server.cache().move_file_to_fs(
255                                        log,
256                                        self.server.webcontent(),
257                                        format!("{}/{log}", conf.pipelines_to_web_dir),
258                                    ) {
259                                        self.server.log_error("can't move log to web", e);
260                                    }
261                                })
262                            }
263
264                            self.server.report(
265                                ReportLevel::Info,
266                                content.into_iter().chain(artifact_report).collect(),
267                            );
268                        });
269                    } else {
270                        self.server.log("don't start because no need".to_string());
271                    }
272                    Ok(())
273                }
274                Err(poison) => Err(format!("Mutex poisoned finish {:?}", poison)),
275            },
276            Err(poison) => Err(format!("Mutex poisoned finish {:?}", poison)),
277        }
278    }
279}
280
281#[cfg_attr(all(target_arch = "wasm32"), unsafe(export_name = "install"))]
282pub extern "C" fn install() {
283    let conf = PluginRun {
284        path: String::from("*"),
285        branch: vec![String::from("*")],
286        when: vec![PluginRunWhen::Add, PluginRunWhen::Mod],
287        func: vec![],
288        write: PluginWrite {
289            git: vec![],
290            web: vec![],
291            exec: vec![PluginExecRight {
292                command: String::from("cat .*"),
293            }],
294            call_func: vec![],
295        },
296        configuration: serde_json::to_value(Conf {
297            exec: Exec {
298                build: String::from(""),
299                report_stats: true,
300                cmds: vec![Cmd {
301                    cmd: String::from("cat"),
302                    args: vec![String::from("README.md")],
303                }],
304                env: vec![],
305                artifacts: vec![],
306                cache: vec![],
307            },
308            pipelines_to_web_dir: "pipelines".to_string(),
309        })
310        .unwrap(),
311    };
312    register(vec![conf], Hop::new);
313}