This repository has been archived on 2021-06-07. You can view files and clone it, but cannot push or open issues or pull requests.
gitm/src/main.rs

201 lines
6.2 KiB
Rust

/* Gitm. Automatic git mirroring script.
*
* Copyright (C) 2021 Arsen Musayelyan <arsen@arsenm.dev>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use serde_derive::Deserialize;
use std::fs;
use std::{
collections::HashMap,
env,
path::Path,
process::{exit, Child, Command},
};
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<String, String>,
options: Option<Options>,
}
// Options struct stores the options table in the TOML config
#[derive(Deserialize, Default)]
struct Options {
branch: Option<String>,
}
fn main() {
// Create new logger with level Info and no timestamp
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.format_timestamp(None)
.init();
// Collect arguments into vector
let args: Vec<String> = env::args().collect();
// If config does not exist
if !Path::new(CFG_NAME).exists() {
// Pass arguments to git
default_git(&args[1..]);
exit(0);
}
// 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 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);
exit(0);
}
// 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");
// Ensure that required repos exist
ensure_repos(&config.repos);
// 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");
// Ensure that required repos exist
ensure_repos(&config.repos);
// 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
_ => default_git(&args[1..]),
}
}
fn default_git(args: &[String]) {
// Run git, passing through all arguments provided
let mut proc = Command::new("git")
.args(&args[..])
.spawn()
.log_err("Error running git command");
exit_if_code_nonzero(&mut proc);
}
fn ensure_repos(repos: &HashMap<String, String>) {
// If no repos provided, error and exit
if repos.len() < 1 {
error!("Please add repos to the {} file", CFG_NAME);
exit(1);
}
// If origin repo is not defined, error and exit
if repos.get("origin").is_none() {
error!("Origin repo required in {} file", CFG_NAME);
exit(1);
}
}
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<T> {
fn log_err(self, msg: &str) -> T;
}
// Implement LogErr on Result of any type where E implements Debug
impl<T, E> LogErr<T> for Result<T, E>
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)
}
}
}
}