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}