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: MIT
  4
  5use crate::fs::Fs;
  6use crate::imports;
  7use serde::{Deserialize, Serialize};
  8use serde_json::Value;
  9use std::clone::Clone;
 10use std::sync::{Mutex, OnceLock};
 11
 12#[derive(Serialize)]
 13#[serde(rename_all = "camelCase")]
 14pub struct ConfPlugin {
 15    pub default_run: Vec<PluginRun>,
 16    pub sdk_version: String,
 17    pub sdk_type: String,
 18}
 19
 20#[derive(Serialize, Deserialize)]
 21#[serde(rename_all = "camelCase")]
 22pub struct Exec {
 23    pub cmd: String,
 24    pub args: Vec<String>,
 25    pub env: Vec<String>,
 26}
 27
 28#[derive(Debug, Deserialize, Serialize)]
 29#[serde(rename_all = "camelCase")]
 30pub struct ForgeConf {
 31    domain: String,
 32    external_ssh_addr: String
 33	external_http_addr: String
 34}
 35
 36#[derive(Debug, Deserialize)]
 37#[serde(rename_all = "camelCase")]
 38pub struct Commit {
 39    pub branch: String,
 40    pub hash: String,
 41    pub message: String,
 42    pub date: String, //TODO should a DateTime
 43    pub committer: String,
 44    pub parent_hash: String,
 45}
 46
 47#[derive(Debug, Deserialize, Serialize, Clone)]
 48#[serde(rename_all = "lowercase")]
 49pub enum PluginRunWhen {
 50    ADD, // = "add",
 51    MOD, // = "mod",
 52    DEL, // = "del",
 53}
 54
 55#[derive(Debug, Deserialize, Serialize, Clone)]
 56#[serde(rename_all = "lowercase")]
 57pub enum PluginWriteRightCan {
 58    ADD,    // = "add",
 59    MOD,    // = "mod",
 60    DEL,    // = "del",
 61    APPEND, // = "append",
 62}
 63
 64impl PluginWriteRightCan {
 65    pub fn all() -> Vec<PluginWriteRightCan> {
 66        vec![
 67            PluginWriteRightCan::ADD,
 68            PluginWriteRightCan::MOD,
 69            PluginWriteRightCan::DEL,
 70            PluginWriteRightCan::APPEND,
 71        ]
 72    }
 73}
 74
 75#[derive(Debug, Serialize, Clone)]
 76#[serde(rename_all = "camelCase")]
 77pub struct PluginRun {
 78    pub path: String,
 79    pub branch: Vec<String>,
 80    pub when: Vec<PluginRunWhen>,
 81    pub write: PluginWrite,
 82    pub configuration: Value,
 83}
 84
 85#[derive(Debug, Serialize, Clone)]
 86#[serde(rename_all = "camelCase")]
 87pub struct PluginWrite {
 88    pub git: Vec<PluginWriteRight>,
 89    pub web: Vec<PluginWriteRight>,
 90    pub exec: Vec<PluginExecRight>,
 91}
 92
 93#[derive(Debug, Serialize, Clone)]
 94#[serde(rename_all = "camelCase")]
 95pub struct PluginWriteRight {
 96    pub path: String,
 97    pub can: Vec<PluginWriteRightCan>,
 98}
 99
100#[derive(Debug, Serialize, Clone)]
101#[serde(rename_all = "camelCase")]
102pub struct PluginExecRight {
103    pub command: String,
104}
105
106pub trait Plugin: Send + 'static {
107    fn init(
108        &self,
109        repo_name: String,
110        conf_has_changed: bool,
111        serialized_conf: String,
112    ) -> Result<(), String>;
113    fn start_commit(&self, commit: Commit) -> Result<(), String>;
114    fn add_file(&self, path: String) -> Result<(), String>;
115    fn mod_file(&self, from_path: String, to_path: String) -> Result<(), String>;
116    fn del_file(&self, path: String) -> Result<(), String>;
117    fn end_commit(&self, commit: Commit) -> Result<(), String>;
118    fn finish(&self) -> Result<(), String>;
119}
120
121pub struct Server {
122    at_least_one_change: bool,
123    plugin: Mutex<Option<Box<dyn Plugin>>>,
124    pub run: Vec<PluginRun>,
125}
126
127static INSTANCE: OnceLock<Server> = OnceLock::new();
128
129impl Server {
130    pub fn init<T, F>(run: Vec<PluginRun>, plugin_factory: F) -> &'static Self
131    where
132        T: Plugin,
133        F: FnOnce(&'static Self) -> T,
134    {
135        let server = Server {
136            at_least_one_change: false,
137            plugin: Mutex::new(None),
138            run: run,
139        };
140        let _ = INSTANCE.set(server);
141        let static_server = INSTANCE.get().unwrap();
142        let plugin = plugin_factory(static_server);
143        *static_server.plugin.lock().unwrap() = Some(Box::new(plugin));
144        static_server
145    }
146
147    pub fn get() -> &'static Self {
148        INSTANCE
149            .get()
150            .expect("Server has not been initialized. Call Server::init() first.")
151    }
152
153    pub fn with_plugin<F>(f: F)
154    where
155        F: FnOnce(&dyn Plugin) -> Result<(), String>,
156    {
157        let guard = Self::get().plugin.lock().unwrap();
158        if let Some(plugin) = guard.as_ref() {
159            let _ = f(&*plugin.as_ref())
160                .map_err(|err| imports::log_error(String::from("unknown error"), err));
161        } else {
162            imports::log_error(
163                String::from("Plugin not initialized after server was created"),
164                String::from("Fatal error"),
165            );
166        }
167    }
168
169    pub fn forge_conf(&self) -> Result<ForgeConf, String> {
170        imports::forge_conf()
171    }
172
173    pub fn worktree(&self) -> Fs {
174        Fs::new(String::from("/worktree/"))
175    }
176
177    pub fn webcontent(&self) -> Fs {
178        Fs::new(String::from("/webcontent/"))
179    }
180
181    pub fn cache(&self) -> Fs {
182        Fs::new(String::from("/cache"))
183    }
184
185    pub fn modify_content(&mut self, filepath: String, content: String) {
186        self.at_least_one_change = true;
187        imports::modify_content(filepath, content);
188    }
189
190    pub fn modify_web_content(&self, filepath: String, content: String) {
191        imports::modify_web_content(filepath, content);
192    }
193
194    pub fn replace_web_content(&self, filepath: String, old_content: String, content: String) {
195        imports::replace_web_content(filepath, old_content, content);
196    }
197
198    pub fn modify_cache_content(&self, filepath: String, content: String) {
199        imports::modify_cache_content(filepath, content);
200    }
201
202    pub fn commit_all_if_needed(&self, message: String) {
203        if self.at_least_one_change {
204            imports::commit_all(message);
205        }
206    }
207
208    pub fn diff_with_parent(
209        &self,
210        hash: String,
211        old_filepath: String,
212        new_filepath: String,
213    ) -> Result<String, String> {
214        imports::diff_with_parent(hash, old_filepath, new_filepath)
215    }
216
217    pub fn log(&self, message: String) {
218        imports::log(message);
219    }
220
221    pub fn log_error(&self, message: String, err: String) {
222        imports::log_error(message, err);
223    }
224
225    pub fn merge(&self, from: String, to: String) {
226        imports::merge(from, to);
227    }
228
229    pub fn commits(&self, from: String, to: String) -> Result<Vec<Commit>, String> {
230        imports::commits(from, to)
231    }
232
233    pub fn exec(&self, cmd: &[Exec]) -> Result<String, String> {
234        imports::exec(cmd)
235    }
236}