diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..c23bb91 --- /dev/null +++ b/.envrc @@ -0,0 +1,7 @@ +use_flake() { + watch_file flake.nix + watch_file flake.lock + eval "$(nix print-dev-env)" +} + +use flake diff --git a/.gitignore b/.gitignore index ea8c4bf..d787b70 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/result diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cda3975 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: +- repo: 'local' + hooks: + - id: 'nixpkgs-fmt' + name: 'nixpkgs-fmt' + description: 'Format nix code with nixpkgs-fmt' + entry: 'nixpkgs-fmt' + language: 'system' + files: '\.nix$' + always_run: true +- repo: 'https://github.com/pre-commit/pre-commit-hooks' + rev: 'v2.3.0' + hooks: + - id: 'trailing-whitespace' + - id: 'end-of-file-fixer' + - id: 'check-yaml' +- repo: 'https://github.com/jumanjihouse/pre-commit-hooks' + rev: '2.1.4' + hooks: + - id: 'forbid-binary' diff --git a/Cargo.lock b/Cargo.lock index ce08ffb..8fcd247 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,6 +231,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "filetime" version = "0.2.14" @@ -459,6 +465,12 @@ version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "log" version = "0.3.9" @@ -486,6 +498,7 @@ dependencies = [ "rocket", "rocket_contrib", "serde", + "serde_yaml", ] [[package]] @@ -851,6 +864,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + [[package]] name = "sha2" version = "0.9.3" @@ -1104,6 +1129,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 14d5b4d..6aabd22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ log = "0.4.14" rocket = "0.4.7" rocket_contrib = { version = "0.4.7", features = [ "json" ] } serde = { version = "1.0.125", features = [ "derive" ] } +serde_yaml = "0.8.17" diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..873ece4 --- /dev/null +++ b/default.nix @@ -0,0 +1,13 @@ +(import + ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { + src = ./.; + }).defaultNix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c0cf2ff --- /dev/null +++ b/flake.lock @@ -0,0 +1,99 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1606424373, + "narHash": "sha256-oq8d4//CJOrVj+EcOaSXvMebvuTkmBJuT5tzlfewUnQ=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "99f1c2157fba4bfe6211a321fd0ee43199025dbf", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1614513358, + "narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5466c5bbece17adaab2d82fae80b46e807611bf3", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "mozillapkgs": { + "flake": false, + "locked": { + "lastModified": 1603906276, + "narHash": "sha256-RsNPnEKd7BcogwkqhaV5kI/HuNC4flH/OQCC/4W5y/8=", + "owner": "mozilla", + "repo": "nixpkgs-mozilla", + "rev": "8c007b60731c07dd7a052cce508de3bb1ae849b4", + "type": "github" + }, + "original": { + "owner": "mozilla", + "repo": "nixpkgs-mozilla", + "type": "github" + } + }, + "naersk": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1614785451, + "narHash": "sha256-TPw8kQvr2UNCuvndtY+EjyXp6Q5GEW2l9UafXXh1XmI=", + "owner": "nmattia", + "repo": "naersk", + "rev": "e0fe990b478a66178a58c69cf53daec0478ca6f9", + "type": "github" + }, + "original": { + "owner": "nmattia", + "ref": "master", + "repo": "naersk", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1616670887, + "narHash": "sha256-wn+l9qJfR5sj5Gq4DheJHAcBDfOs9K2p9seW2f35xzs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c0e881852006b132236cbf0301bd1939bb50867e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "mozillapkgs": "mozillapkgs", + "naersk": "naersk", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..acd7f57 --- /dev/null +++ b/flake.nix @@ -0,0 +1,55 @@ +{ + inputs = { + naersk = { + url = "github:nmattia/naersk/master"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + mozillapkgs = { + url = "github:mozilla/nixpkgs-mozilla"; + flake = false; + }; + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; + }; + + outputs = { self, naersk, mozillapkgs, nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + + mozilla = pkgs.callPackage (mozillapkgs + "/package-set.nix") { }; + rustNightly = (mozilla.rustChannelOf { + date = "2021-03-29"; + channel = "nightly"; + sha256 = "sha256-Y94CnslybZgiZlNVV6Cg0TUPV2OeDXakPev1kqdt9Kk="; + }).rust; + + naersk-lib = pkgs.callPackage naersk { + cargo = rustNightly; + rustc = rustNightly; + }; + in + { + defaultPackage = naersk-lib.buildPackage ./.; + + defaultApp = flake-utils.lib.mkApp { + drv = self.defaultPackage."${system}"; + }; + + devShell = pkgs.mkShell { + buildInputs = with pkgs; [ + nixpkgs-fmt + pre-commit + rustPackages.clippy + rustNightly + rustfmt + ]; + + RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc; + }; + }); +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..9eb132a --- /dev/null +++ b/shell.nix @@ -0,0 +1,13 @@ +(import + ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { + src = ./.; + }).shellNix diff --git a/src/job.rs b/src/job.rs index 25697b5..1a15233 100644 --- a/src/job.rs +++ b/src/job.rs @@ -8,6 +8,7 @@ use anyhow::bail; use log::info; use crate::gitea::Repository; +use crate::settings::{GlobalSettings, RepoUrl}; pub(crate) struct Job { repo: Repository, @@ -91,7 +92,9 @@ impl Job { Ok(()) } - fn get_remotes(&self) -> anyhow::Result { + /// Can return Ok(None) if the .lohr file didn't exist, but no significant error occured + fn read_remotes_from_lohr_file(&self) -> anyhow::Result>> { + // try to read .lohr file from bare repo (hence the git show sorcery) let output = Command::new("git") .arg("-C") .arg(format!("{}", self.local_path.as_ref().unwrap().display())) @@ -101,25 +104,57 @@ impl Job { 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 read .lohr file from repo {}: exit code {}, stderr:\n{}", - self.repo.full_name, - code, - error - ); + // this error case is okay, .lohr just doesn't exist + if error.contains("does not exist in 'HEAD'") { + return Ok(None); + } else { + let code = output + .status + .code() + .unwrap_or_else(|| output.status.signal().unwrap()); + + bail!( + "couldn't read .lohr file from repo {}: exit code {}, stderr:\n{}", + self.repo.full_name, + code, + error + ); + } } - Ok(String::from_utf8(output.stdout)?) + let output = String::from_utf8(output.stdout)?; + + Ok(Some(output.lines().map(String::from).collect())) } - fn update_mirrors(&self) -> anyhow::Result<()> { - for remote in self.get_remotes()?.lines() { - info!("Updating mirror {}:{}...", remote, self.repo.full_name); + fn get_remotes(&self, config: &GlobalSettings) -> anyhow::Result> { + let local_path = self.local_path.as_ref().unwrap(); + + let stem_to_repo = |stem: &RepoUrl| -> RepoUrl { + let mut res = stem.clone(); + if !res.ends_with('/') { + res.push('/'); + }; + res.push_str(local_path.file_name().unwrap().to_str().unwrap()); + res + }; + + // use either .lohr file or default remotes from config + let mut remotes = match self.read_remotes_from_lohr_file()? { + Some(remotes) if !remotes.is_empty() => remotes, + _ => config.default_remotes.iter().map(stem_to_repo).collect(), + }; + + // additional remotes + remotes.append(&mut config.additional_remotes.iter().map(stem_to_repo).collect()); + + Ok(remotes) + } + + fn update_mirrors(&self, config: &GlobalSettings) -> anyhow::Result<()> { + for remote in &self.get_remotes(config)? { + info!("Updating mirror {}...", remote); let output = Command::new("git") .arg("-C") @@ -148,7 +183,7 @@ impl Job { Ok(()) } - pub(crate) fn run(&mut self, homedir: &Path) -> anyhow::Result<()> { + pub(crate) fn run(&mut self, homedir: &Path, config: &GlobalSettings) -> anyhow::Result<()> { let local_path = homedir.join(&self.repo.full_name); assert!(local_path.is_absolute()); self.local_path = Some(local_path); @@ -159,6 +194,10 @@ impl Job { self.update_repo()?; } - self.update_mirrors() + self.update_mirrors(config)?; + + info!("Done processing job {}!", self.repo.full_name); + + Ok(()) } } diff --git a/src/main.rs b/src/main.rs index 05e8f20..6bb3613 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![feature(proc_macro_hygiene, decl_macro)] use std::env; +use std::fs::File; use std::path::PathBuf; use std::sync::{ mpsc::{channel, Receiver, Sender}, @@ -19,10 +20,15 @@ use gitea::GiteaWebHook; mod job; use job::Job; +mod settings; +use settings::GlobalSettings; + struct JobSender(Mutex>); #[post("/", data = "")] fn gitea_webhook(payload: Json, sender: State) -> Status { + // TODO: validate Gitea signature + { let sender = sender.0.lock().unwrap(); let repo = &payload.repository; @@ -32,29 +38,44 @@ fn gitea_webhook(payload: Json, sender: State) -> Statu Status::Ok } -fn repo_updater(rx: Receiver, homedir: PathBuf) { +fn repo_updater(rx: Receiver, homedir: PathBuf, config: GlobalSettings) { loop { let mut job = rx.recv().unwrap(); - if let Err(err) = job.run(&homedir) { + if let Err(err) = job.run(&homedir, &config) { error!("couldn't process job: {}", err); } } } -fn main() { +fn parse_config(mut path: PathBuf) -> anyhow::Result { + path.push("lohr-config"); + path.set_extension("yaml"); + let config = if let Ok(file) = File::open(path.as_path()) { + serde_yaml::from_reader(file)? + } else { + Default::default() + }; + Ok(config) +} + +fn main() -> anyhow::Result<()> { 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!"); + let config = parse_config(homedir.clone())?; + thread::spawn(move || { - repo_updater(receiver, homedir); + repo_updater(receiver, homedir, config); }); rocket::ignite() .mount("/", routes![gitea_webhook]) .manage(JobSender(Mutex::new(sender))) .launch(); + + Ok(()) } diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..bfe7744 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,13 @@ +use serde::Deserialize; + +pub(crate) type RepoUrl = String; // FIXME: probably needs a better type than this + +#[derive(Default, Deserialize)] +pub(crate) struct GlobalSettings { + /// List of remote stems to use when no `.lohr` file is found + #[serde(default)] + pub default_remotes: Vec, + /// List of remote stems to use for every repository + #[serde(default)] + pub additional_remotes: Vec, +}