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}