316 lines
11 KiB
Rust
316 lines
11 KiB
Rust
// A Rust GitOps/symlinkfarm orchestrator inspired by GNU Stow.
|
|
// Copyright (C) 2023 Christina Sørensen <christina@cafkafk.com>
|
|
//
|
|
// 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/gpl-3.0.html.
|
|
|
|
use log::{debug, error, info, trace, warn};
|
|
use serde::{Deserialize, Serialize};
|
|
use spinners::{Spinner, Spinners};
|
|
use std::collections::HashMap;
|
|
use std::fs::canonicalize;
|
|
use std::os::unix::fs::symlink;
|
|
use std::path::Path;
|
|
use std::{fs, process::Command};
|
|
|
|
// why not make it O(log n) instead of a vec that's /only/ O(n)
|
|
// ...because premature optimization is the root of all evil!
|
|
//
|
|
// it's time
|
|
|
|
#[derive(PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
|
pub enum RepoFlags {
|
|
Push,
|
|
Clone,
|
|
Pull,
|
|
}
|
|
|
|
/// Represents the config.toml file.
|
|
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
|
pub struct Config {
|
|
/// map of all categories
|
|
///
|
|
/// Key should conceptually be seen as the name of the category.
|
|
pub categories: HashMap<String, Category>,
|
|
pub links: Vec<Links>,
|
|
}
|
|
|
|
/// Represents a category of repositories
|
|
///
|
|
/// This allows you to organize your repositories into categories
|
|
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
|
pub struct Category {
|
|
pub flags: Vec<RepoFlags>, // FIXME: not implemented
|
|
/// map of all categories
|
|
///
|
|
/// Key should conceptually be seen as the name of the category.
|
|
pub repos: HashMap<String, GitRepo>,
|
|
}
|
|
|
|
/// Contain fields for a single link.
|
|
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
|
pub struct Links {
|
|
pub name: String,
|
|
pub rx: String,
|
|
pub tx: String,
|
|
}
|
|
|
|
/// Holds a single git repository and related fields.
|
|
#[derive(PartialEq, Debug, Serialize, Deserialize)]
|
|
pub struct GitRepo {
|
|
pub name: String,
|
|
pub path: String,
|
|
pub url: String,
|
|
pub flags: Vec<RepoFlags>,
|
|
}
|
|
|
|
fn handle_file_exists(selff: &Links, tx_path: &Path, rx_path: &Path) {
|
|
match rx_path.read_link() {
|
|
Ok(file) if file.canonicalize().unwrap() == tx_path.canonicalize().unwrap() => {
|
|
debug!(
|
|
"Linking {} -> {} failed: file already linked",
|
|
&selff.tx, &selff.rx
|
|
);
|
|
}
|
|
Ok(file) => {
|
|
error!(
|
|
"Linking {} -> {} failed: link to different file exists",
|
|
&selff.tx, &selff.rx
|
|
);
|
|
}
|
|
Err(error) => {
|
|
error!("Linking {} -> {} failed: file exists", &selff.tx, &selff.rx);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Links {
|
|
/// Creates a link from a file
|
|
fn link(&self) {
|
|
let tx_path: &Path = std::path::Path::new(&self.tx);
|
|
let rx_path: &Path = std::path::Path::new(&self.rx);
|
|
match rx_path.try_exists() {
|
|
Ok(true) => handle_file_exists(self, tx_path, rx_path),
|
|
Ok(false) if rx_path.is_symlink() => {
|
|
error!(
|
|
"Linking {} -> {} failed: broken symlink",
|
|
&self.tx, &self.rx
|
|
);
|
|
}
|
|
Ok(false) => {
|
|
symlink(&self.tx, &self.rx).expect("failed to create link");
|
|
}
|
|
Err(error) => {
|
|
error!("Linking {} -> {} failed: {}", &self.tx, &self.rx, error);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
impl GitRepo {
|
|
/// Clones the repository to its specified folder.
|
|
fn clone(&self) {
|
|
if self.flags.contains(&RepoFlags::Clone) {
|
|
// TODO: check if &self.name already exists in dir
|
|
let out = Command::new("git")
|
|
.current_dir(&self.path)
|
|
.arg("clone")
|
|
.arg(&self.url)
|
|
.arg(&self.name)
|
|
.output()
|
|
.unwrap_or_else(|_| panic!("git repo failed to clone: {:?}", &self,));
|
|
// info!("{out}");
|
|
} else {
|
|
info!("{} has clone set to false, not cloned", &self.name);
|
|
}
|
|
}
|
|
/// Pulls the repository if able.
|
|
fn pull(&self) {
|
|
if self.flags.contains(&RepoFlags::Pull) {
|
|
let out = Command::new("git")
|
|
.current_dir(format!("{}{}", &self.path, &self.name))
|
|
.arg("pull")
|
|
.output()
|
|
.unwrap_or_else(|_| panic!("git repo failed to pull: {:?}", &self,));
|
|
} else {
|
|
info!("{} has clone set to false, not pulled", &self.name);
|
|
}
|
|
// info!("{out}");
|
|
}
|
|
/// Adds all files in the repository.
|
|
fn add_all(&self) {
|
|
if self.flags.contains(&RepoFlags::Push) {
|
|
let out = Command::new("git")
|
|
.current_dir(format!("{}{}", &self.path, &self.name))
|
|
.arg("add")
|
|
.arg(".")
|
|
.output()
|
|
.unwrap_or_else(|_| panic!("git repo failed to add: {:?}", &self,));
|
|
// info!("{out}");
|
|
} else {
|
|
info!("{} has clone set to false, not cloned", &self.name);
|
|
}
|
|
}
|
|
/// Tries to commit changes in the repository.
|
|
#[allow(dead_code)]
|
|
fn commit(&self) {
|
|
if self.flags.contains(&RepoFlags::Push) {
|
|
let out = Command::new("git")
|
|
.current_dir(format!("{}{}", &self.path, &self.name))
|
|
.arg("commit")
|
|
.output()
|
|
.unwrap_or_else(|_| panic!("git repo failed to commit: {:?}", &self,));
|
|
// info!("{out}");
|
|
} else {
|
|
info!("{} has clone set to false, not cloned", &self.name);
|
|
}
|
|
}
|
|
/// Tries to commit changes with a message argument.
|
|
fn commit_with_msg(&self, msg: &String) {
|
|
if self.flags.contains(&RepoFlags::Push) {
|
|
let out = Command::new("git")
|
|
.current_dir(format!("{}{}", &self.path, &self.name))
|
|
.arg("commit")
|
|
.arg("-m")
|
|
.arg(msg)
|
|
.output()
|
|
.unwrap_or_else(|_| panic!("git repo failed to commit: {:?}", &self,));
|
|
// info!("{out}");
|
|
} else {
|
|
info!("{} has clone set to false, not cloned", &self.name);
|
|
}
|
|
}
|
|
/// Attempts to push the repository.
|
|
fn push(&self) {
|
|
if self.flags.contains(&RepoFlags::Push) {
|
|
let out = Command::new("git")
|
|
.current_dir(format!("{}{}", &self.path, &self.name))
|
|
.arg("push")
|
|
.output()
|
|
.unwrap_or_else(|_| panic!("git repo failed to push: {:?}", &self,));
|
|
// info!("{out}");
|
|
} else {
|
|
info!("{} has clone set to false, not cloned", &self.name);
|
|
}
|
|
}
|
|
/// Removes repository
|
|
fn remove(&self) -> Result<(), std::io::Error> {
|
|
// https://doc.rust-lang.org/std/fs/fn.remove_dir_all.html
|
|
unimplemented!("This seems to easy to missuse/exploit");
|
|
// fs::remove_dir_all(format!("{}{}", &self.path, &self.name))
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
/* GIT RELATED */
|
|
/// Reads the configuration toml from a path.
|
|
pub fn new(path: &String) -> Self {
|
|
debug!("initializing new Config struct");
|
|
let yaml = fs::read_to_string(path).unwrap_or_else(|_| {
|
|
panic!("Should have been able to read the file: path -> {:?}", path,)
|
|
});
|
|
debug!("deserialized yaml from config file");
|
|
serde_yaml::from_str(&yaml).unwrap_or_else(|_| {
|
|
panic!(
|
|
"Should have been able to deserialize yaml config: path -> {:?}",
|
|
path,
|
|
)
|
|
})
|
|
}
|
|
/// Runs associated function on all repos in config
|
|
///
|
|
/// TODO: need to be made over a generic repo type
|
|
fn on_all<F>(&self, f: F)
|
|
where
|
|
F: Fn(&GitRepo),
|
|
{
|
|
for (_, category) in self.categories.iter() {
|
|
for (_, repo) in category.repos.iter() {
|
|
f(repo);
|
|
}
|
|
}
|
|
}
|
|
fn on_all_spinner<F>(&self, op: &str, f: F)
|
|
where
|
|
F: Fn(&GitRepo),
|
|
{
|
|
for (_, category) in self.categories.iter() {
|
|
for (_, repo) in category.repos.iter() {
|
|
let mut sp =
|
|
Spinner::new(Spinners::Dots10, format!("{}: {}", repo.name, op).into());
|
|
f(repo);
|
|
sp.stop_and_persist("✔", format!("{}: {}", repo.name, op).into());
|
|
}
|
|
}
|
|
}
|
|
/// Tries to pull all repositories, skips if fail.
|
|
pub fn pull_all(&self) {
|
|
debug!("exectuting pull_all");
|
|
self.on_all_spinner("pull", |repo| {
|
|
repo.pull();
|
|
});
|
|
}
|
|
/// Tries to clone all repositories, skips if fail.
|
|
pub fn clone_all(&self) {
|
|
debug!("exectuting clone_all");
|
|
self.on_all_spinner("clone", |repo| {
|
|
repo.clone();
|
|
});
|
|
}
|
|
/// Tries to add all work in all repositories, skips if fail.
|
|
pub fn add_all(&self) {
|
|
debug!("exectuting clone_all");
|
|
self.on_all_spinner("add", |repo| {
|
|
repo.add_all();
|
|
});
|
|
}
|
|
/// Tries to commit all repositories one at a time, skips if fail.
|
|
pub fn commit_all(&self) {
|
|
debug!("exectuting clone_all");
|
|
self.on_all_spinner("commit", |repo| {
|
|
repo.commit();
|
|
});
|
|
}
|
|
/// Tries to commit all repositories with msg, skips if fail.
|
|
pub fn commit_all_msg(&self, msg: &String) {
|
|
debug!("exectuting clone_all");
|
|
self.on_all_spinner("commit", |repo| {
|
|
repo.commit_with_msg(msg);
|
|
});
|
|
}
|
|
/// Tries to pull, add all, commit with msg "quick commit", and push all
|
|
/// repositories, skips if fail.
|
|
pub fn quick(&self, msg: &String) {
|
|
debug!("exectuting quick");
|
|
self.on_all(|repo| {
|
|
let mut sp = Spinner::new(Spinners::Dots10, format!("{}: pull", repo.name).into());
|
|
repo.pull();
|
|
sp = Spinner::new(Spinners::Dots10, format!("{}: add_all", repo.name).into());
|
|
repo.add_all();
|
|
sp = Spinner::new(Spinners::Dots10, format!("{}: commit", repo.name).into());
|
|
repo.commit_with_msg(msg);
|
|
sp = Spinner::new(Spinners::Dots10, format!("{}: push", repo.name).into());
|
|
repo.push();
|
|
sp.stop_and_persist("✔", format!("{}: quick", repo.name).into());
|
|
});
|
|
}
|
|
|
|
/* LINK RELATED */
|
|
/// Tries to link all repositories, skips if fail.
|
|
pub fn link_all(&self) {
|
|
debug!("exectuting link_all");
|
|
for link in self.links.iter() {
|
|
link.link();
|
|
}
|
|
}
|
|
}
|