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