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, 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;
 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}
 25
 26impl Hop {
 27    fn new(server: Arc<Server>) -> Self {
 28        Self {
 29            server,
 30            conf: Arc::new(Mutex::new(None)),
 31            need_to_run: Arc::new(Mutex::new(false)),
 32        }
 33    }
 34}
 35
 36#[derive(Serialize, Deserialize)]
 37struct Conf {
 38    exec: Exec,
 39}
 40
 41impl Hop {
 42    fn format_exec_status(&self, status: &ExecStatus) -> Vec<String> {
 43        let mut output = Vec::new();
 44
 45        let count = status.cmds_exec.len();
 46        if count == 0 {
 47            output.push("❌ No commands executed.".to_string());
 48            return output;
 49        }
 50        if count != status.cmds_status.len()
 51            || count != status.cmds_logs.len()
 52            || count != status.cmds_stats.len()
 53        {
 54            output.push("⚠️ Data Error: Vectors do not have the same length.".to_string());
 55            return output;
 56        }
 57
 58        output.push(format!("Total of **{}** commands processed.", count));
 59        output.push("".to_string());
 60
 61        for i in 0..count {
 62            let cmd = &status.cmds_exec[i];
 63            let status_code = status.cmds_status[i];
 64            let logs = &status.cmds_logs[i];
 65            let stats = &status.cmds_stats[i];
 66
 67            //title
 68            let success_emoji = if status_code == 0 {
 69                "- ✅".to_string()
 70            } else {
 71                format!("- ❌ (status code {})", status_code)
 72            };
 73            output.push(format!("{} : `{}`", success_emoji, cmd));
 74
 75            //stats
 76            output.push("<details><summary>📊 Execution Statistics</summary>".to_string());
 77            output.push("".to_string());
 78
 79            output.push("| Metric | Value |".to_string());
 80            output.push("| :--- | :--- |".to_string());
 81
 82            output.push(format!(
 83                "| **Max Memory** | {} |",
 84                humanize_bytes_binary!(stats.max_memory_bytes)
 85            ));
 86            output.push(format!(
 87                "| **Total CPU Time** | {} |",
 88                Millisecond::from_millis(stats.total_cpu_time_ms)
 89            ));
 90            output.push(format!("| Max Threads | {} |", stats.max_threads));
 91            output.push(format!(
 92                "| I/O Read | {} |",
 93                humanize_bytes_binary!(stats.read_bytes_total)
 94            ));
 95            output.push(format!(
 96                "| I/O Write | {} |",
 97                humanize_bytes_binary!(stats.write_bytes_total)
 98            ));
 99            output.push("</details>".to_string());
100
101            //logs
102            output.push("<details><summary>📜 Logs</summary>".to_string());
103            output.push("".to_string());
104
105            if logs.is_empty() {
106                output.push("> _No standard or error output._".to_string());
107            } else {
108                output.push("`````text".to_string());
109                output.push(logs.trim().to_string());
110                output.push("`````".to_string());
111            }
112            output.push("</details>".to_string());
113        }
114
115        output
116    }
117}
118
119impl Plugin for Hop {
120    fn init(
121        &self,
122        _repo_name: String,
123        _conf_has_changed: bool,
124        serialized_conf: String,
125    ) -> Result<(), String> {
126        serde_json::from_str(serialized_conf.as_str())
127            .map(|conf: Conf| match self.conf.lock() {
128                Ok(mut data) => {
129                    *data = Some(conf);
130                }
131                Err(poison) => {
132                    eprintln!("Mutex empoisonné ! Récupération possible: {:?}", poison);
133                }
134            })
135            .map_err(|err| err.to_string())
136    }
137
138    fn start_commit(&self, _commit: Commit) -> Result<(), String> {
139        Ok(())
140    }
141
142    fn add_file(&self, _path: String) -> Result<(), String> {
143        match self.need_to_run.lock() {
144            Ok(mut data) => {
145                *data = true;
146                Ok(())
147            }
148            Err(poison) => Err(format!("Mutex poisoned add_file {:?}", poison)),
149        }
150    }
151
152    fn mod_file(&self, _from_path: String, _to_path: String) -> Result<(), String> {
153        match self.need_to_run.lock() {
154            Ok(mut data) => {
155                *data = true;
156                Ok(())
157            }
158            Err(poison) => Err(format!("Mutex poisoned mod_file {:?}", poison)),
159        }
160    }
161
162    fn del_file(&self, _path: String) -> Result<(), String> {
163        match self.need_to_run.lock() {
164            Ok(mut data) => {
165                *data = true;
166                Ok(())
167            }
168            Err(poison) => Err(format!("Mutex poisoned del_file {:?}", poison)),
169        }
170    }
171
172    fn end_commit(&self, _commit: Commit) -> Result<(), String> {
173        Ok(())
174    }
175
176    fn finish(&self) -> Result<(), String> {
177        match self.conf.lock() {
178            Ok(data) => match self.need_to_run.lock() {
179                Ok(need_to_run) => {
180                    if *need_to_run {
181                        let conf = data.as_ref().unwrap();
182                        let _ = self.server.exec(&conf.exec).map(|status| {
183                            let content = self.format_exec_status(&status);
184                            self.server.report(ReportLevel::INFO, content);
185                        });
186                    }
187                    Ok(())
188                }
189                Err(poison) => Err(format!("Mutex poisoned finish {:?}", poison)),
190            },
191            Err(poison) => Err(format!("Mutex poisoned finish {:?}", poison)),
192        }
193    }
194}
195
196#[cfg_attr(all(target_arch = "wasm32"), unsafe(export_name = "install"))]
197pub extern "C" fn install() {
198    let conf = PluginRun {
199        path: String::from("*"),
200        branch: vec![String::from("*")],
201        when: vec![PluginRunWhen::ADD, PluginRunWhen::MOD],
202        write: PluginWrite {
203            git: vec![],
204            web: vec![],
205            exec: vec![PluginExecRight {
206                command: String::from("cat"),
207            }],
208        },
209        configuration: serde_json::to_value(Conf {
210            exec: Exec {
211                build: String::from(""),
212                cmds: vec![Cmd {
213                    cmd: String::from("cat"),
214                    args: vec![String::from("README.md")],
215                }],
216                env: vec![],
217            },
218        })
219        .unwrap(),
220    };
221    register(vec![conf], Hop::new);
222}