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