diff --git a/.gitignore b/.gitignore index 9f11b75..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -.idea/ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ba9b3bf --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,205 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "env_logger" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "gitm-rs" +version = "0.1.0" +dependencies = [ + "env_logger", + "log", + "serde", + "serde_derive", + "toml", +] + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "libc" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..929116e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "gitm-rs" +version = "0.1.0" +authors = ["Arsen Musayelyan "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +toml = "0.5" +serde_derive = "1.0" +serde = "1.0" +log = "0.4" +env_logger = "0.8" \ No newline at end of file diff --git a/README.md b/README.md index f7f777a..44a8c7f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Gitm -Automatic git mirroring script. +Automatic git mirroring program. ### How it works -This is a simple script that intercepts commands like `git init` and `git push` and automatically configures and pushes to many different remotes. +This is a simple program that intercepts commands like `git init` and `git push` and automatically configures and pushes to many different remotes. ### Usage -To use this script, create a file called `.gitm.toml` and populate it with repositories like so: +To use this program, create a file called `.gitm.toml` and populate it with repositories like so: ```toml [repos] origin = "https://gitea.arsenm.dev/Arsen6331/gitm.git" diff --git a/gitm.rb b/gitm.rb deleted file mode 100644 index 76d3ffe..0000000 --- a/gitm.rb +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/ruby - -# Gitm. Automatic git mirroring script. -# -# Copyright (C) 2021 Arsen Musayelyan -# -# This program is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License along with -# this program. If not, see . - - -require 'toml' -require 'colorize' - -args = ARGV -cfgName = ".gitm.toml" - -def log(str) - txt = "[gitm]".cyan - puts "#{txt} #{str}" -end - -File.open(cfgName, "a") {} unless File.exists? cfgName - -cfgData = TOML::Parser.new(File.read(cfgName)).parsed -repos = cfgData["repos"] || {} -branch = cfgData["defaultBranch"] || "master" -if repos["origin"].nil? - log "Error: origin repo required" - exit 1 -end - -if repos.length < 1 - puts "Please add repos to the #{cfgName} file" - exit 1 -end - -case args[0] -when "push" - log "Intercepted push command" - repos.each { |name, _| system "git push #{name} #{branch}", *args[1...] } -when "init" - log "Intercepted init command" - system "git init", *args[1...] - repos.each { |name, repo| system "git remote add #{name} #{repo}" } - system "git fetch origin" - system "git checkout master" -else - system "git", *args -end diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..62e79a6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,149 @@ +use std::{collections::HashMap, env, process::{Command, Child, exit}}; +use serde_derive::Deserialize; +use std::fs; +use toml; + +#[macro_use] +extern crate log; + +// TOML config filename +const CFG_NAME: &str = ".gitm.toml"; + +// Config struct stores decoded TOML from config +#[derive(Deserialize, Default)] +struct Config { + repos: HashMap, + options: Option, +} + +// Options struct stores the options table in the TOML config +#[derive(Deserialize, Default)] +struct Options { + branch: Option, +} + +fn main() { + // Create new logger with level Info and no timestamp + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .format_timestamp(None) + .init(); + + // Get config contents + let cfg_contents = fs::read_to_string(CFG_NAME).log_err("Error reading file"); + + // Decode config contents + let config: Config = toml::from_str(&cfg_contents).unwrap_or_default(); + + // If no repos provided, error and exit + if config.repos.len() < 1 { + error!("Please add repos to the {} file", CFG_NAME); + exit(1) + } + + // If origin repo is not defined, error and exit + if config.repos.get("origin").is_none() { + error!("Origin repo required in {} file", CFG_NAME); + exit(1); + } + + // Collect arguments into vector + let args: Vec = env::args().collect(); + + // If no arguments provided + if args.len() < 2 { + // Run git --help + let mut proc = Command::new("git").arg("--help").spawn() + .log_err("Error running git command"); + exit_if_code_nonzero(&mut proc); + } + + // Ensure options exists in config, otherwise set to default + let options = config.options.unwrap_or_default(); + // Ensure branch exists in options, otherwise set to "master" + let branch = options.branch.unwrap_or("master".to_string()); + match args[1].as_str() { + "push" => { + info!("Intercepted push command"); + // For every repo in config + for (name, _) in config.repos { + // Run git push with applicable arguments + let mut proc = Command::new("git") + .args(&["push", &name, &branch]) + .args(&args[2..]) + .spawn() + .log_err("Error running git command"); + exit_if_code_nonzero(&mut proc); + } + }, + "init" => { + info!("Intercepted init command"); + // Run git init with any preceding arguments + let mut proc = Command::new("git").arg("init").args(&args[2..]).spawn() + .log_err("Error running git command"); + exit_if_code_nonzero(&mut proc); + // For every repo in config + for (name, repo) in config.repos { + // Run git remote add with name and repository URL + let mut proc = Command::new("git") + .args(&["remote", "add", &name, &repo]) + .spawn() + .log_err("Error running git command"); + exit_if_code_nonzero(&mut proc); + } + // Run git fetch origin + proc = Command::new("git").args(&["fetch", "origin"]).spawn() + .log_err("Error running git command"); + exit_if_code_nonzero(&mut proc); + // Run git checkout master + proc = Command::new("git").args(&["checkout", "master"]).spawn() + .log_err("Error running git command"); + exit_if_code_nonzero(&mut proc); + }, + // Default + _ => { + // Run git, passing through all arguments provided + let mut proc = Command::new("git").args(&args[1..]).spawn() + .log_err("Error running git command"); + exit_if_code_nonzero(&mut proc); + } + } +} + +fn exit_if_code_nonzero(proc: &mut Child) { + // Wait for process and get exit status + let status = proc.wait().log_err("Command was not running"); + // Get exit code, default 0 + let exit_code = status.code().unwrap_or(0); + // If nonzero exit code + if exit_code != 0 { + // Exit with the same exit code as process + exit(exit_code) + } +} + +// Trait LogErr allows user-friendly error messages +trait LogErr { + fn log_err(self, msg: &str) -> T; +} + +// Implement LogErr on Result of any type where E implements Debug +impl LogErr for Result where E: ::std::fmt::Debug { + // log_err unwraps Result, logging error and exiting if needed + fn log_err(self, msg: &str) -> T { + match self { + // If no error + Ok(res) => { + // Return value within Result + return res + } + // If error + Err(err) => { + // Log error in format "message: error" and exit + error!("{}: {:?}", msg, err); + exit(1) + }, + } + } +} +