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}