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}