diff --git a/src/gitea.rs b/src/gitea.rs index de0d593..e4826e5 100644 --- a/src/gitea.rs +++ b/src/gitea.rs @@ -1,13 +1,13 @@ -use serde::{Deserialize, Serialize}; +use serde::Deserialize; -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize)] pub(crate) struct Repository { pub(crate) name: String, pub(crate) full_name: String, pub(crate) clone_url: String, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize)] pub(crate) struct GiteaWebHook { pub(crate) repository: Repository, } diff --git a/src/job.rs b/src/job.rs index 25022b1..3bea40d 100644 --- a/src/job.rs +++ b/src/job.rs @@ -1,12 +1,140 @@ -pub struct Job { - repo: String, +use std::os::unix::process::ExitStatusExt; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::str; + +use anyhow::bail; + +use log::info; + +use crate::gitea::Repository; + +pub(crate) struct Job { + repo: Repository, + local_path: Option, } +// TODO: implement git operations with git2-rs where possible + impl Job { - pub fn new(repo: String) -> Self { - Self { repo } + const REMOTES: &'static [&'static str] = &["github", "gitlab"]; + + pub(crate) fn new(repo: Repository) -> Self { + Self { + repo, + local_path: None, + } } - pub fn run(&self) -> anyhow::Result<()> { - todo!() + + fn repo_exists(&self) -> bool { + self.local_path + .as_ref() + .map(|p| p.is_dir()) + .unwrap_or(false) + } + + fn mirror_repo(&self) -> anyhow::Result<()> { + info!("Cloning repo {}...", self.repo.full_name); + + let output = Command::new("git") + .arg("clone") + .arg("--mirror") + .arg(&self.repo.clone_url) + .arg(format!("{}", self.local_path.as_ref().unwrap().display())) + .output()?; + + if !output.status.success() { + let error = str::from_utf8(&output.stderr)?; + let code = output + .status + .code() + .unwrap_or_else(|| output.status.signal().unwrap()); + + bail!( + "couldn't mirror repo: exit code {}, stderr:\n{}", + code, + error + ); + } + + // TODO: handle git LFS mirroring: + // https://github.com/git-lfs/git-lfs/issues/2342#issuecomment-310323647 + + Ok(()) + } + + fn update_repo(&self) -> anyhow::Result<()> { + info!("Updating repo {}...", self.repo.full_name); + + let output = Command::new("git") + .arg("-C") + .arg(format!("{}", self.local_path.as_ref().unwrap().display())) + .arg("remote") + .arg("update") + .arg("origin") + // otherwise deleted tags and branches aren't updated on local copy + .arg("--prune") + .output()?; + + if !output.status.success() { + let error = str::from_utf8(&output.stderr)?; + let code = output + .status + .code() + .unwrap_or_else(|| output.status.signal().unwrap()); + + bail!( + "couldn't update origin remote: exit code {}, stderr:\n{}", + code, + error + ); + } + + Ok(()) + } + + fn update_mirrors(&self) -> anyhow::Result<()> { + for remote in Self::REMOTES.iter() { + info!("Updating mirror {}:{}...", remote, self.repo.full_name); + + let output = Command::new("git") + .arg("-C") + .arg(format!("{}", self.local_path.as_ref().unwrap().display())) + .arg("push") + .arg("--mirror") + .arg(format!("git@{}.com:{}", remote, self.repo.full_name)) + .output()?; + + if !output.status.success() { + let error = str::from_utf8(&output.stderr)?; + let code = output + .status + .code() + .unwrap_or_else(|| output.status.signal().unwrap()); + + bail!( + "couldn't update origin remote: exit code {}, stderr:\n{}", + code, + error + ); + } + } + + Ok(()) + } + + pub(crate) fn run(&mut self, homedir: &Path) -> anyhow::Result<()> { + let local_path = homedir.join(&self.repo.full_name); + println!("{}", local_path.display()); + assert!(local_path.is_absolute()); + self.local_path = Some(local_path); + + if !self.repo_exists() { + self.mirror_repo()?; + } else { + self.update_repo()?; + } + + self.update_mirrors() } } diff --git a/src/main.rs b/src/main.rs index ad6ecf1..05e8f20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ #![feature(proc_macro_hygiene, decl_macro)] -use std::path::{Path, PathBuf}; +use std::env; +use std::path::PathBuf; use std::sync::{ mpsc::{channel, Receiver, Sender}, Mutex, }; use std::thread; -use rocket::{fairing::AdHoc, http::Status, post, routes, State}; +use rocket::{http::Status, post, routes, State}; use rocket_contrib::json::Json; use log::error; @@ -18,26 +19,24 @@ use gitea::GiteaWebHook; mod job; use job::Job; -struct HomeDir(PathBuf); struct JobSender(Mutex>); #[post("/", data = "")] fn gitea_webhook(payload: Json, sender: State) -> Status { { let sender = sender.0.lock().unwrap(); - sender - .send(Job::new(payload.repository.full_name.clone())) - .unwrap(); + let repo = &payload.repository; + sender.send(Job::new(repo.clone())).unwrap(); } Status::Ok } -fn repo_updater(rx: Receiver) { +fn repo_updater(rx: Receiver, homedir: PathBuf) { loop { - let job = rx.recv().unwrap(); + let mut job = rx.recv().unwrap(); - if let Err(err) = job.run() { + if let Err(err) = job.run(&homedir) { error!("couldn't process job: {}", err); } } @@ -46,19 +45,16 @@ fn repo_updater(rx: Receiver) { fn main() { let (sender, receiver) = channel(); + let homedir = env::var("LOHR_HOME").unwrap_or_else(|_| "./".to_string()); + let homedir: PathBuf = homedir.into(); + let homedir = homedir.canonicalize().expect("LOHR_HOME isn't valid!"); + thread::spawn(move || { - repo_updater(receiver); + repo_updater(receiver, homedir); }); rocket::ignite() .mount("/", routes![gitea_webhook]) .manage(JobSender(Mutex::new(sender))) - .attach(AdHoc::on_attach("Assets Config", |rocket| { - let home_dir = rocket.config().get_str("home").unwrap(); - - let home_dir = Path::new(home_dir).into(); - - Ok(rocket.manage(HomeDir(home_dir))) - })) .launch(); }