seidr/src/git.rs
Christina Sørensen 301c604f37
feat(git): added pull flag
Added the pull flag to the repo logic.

Now you can pull only some repos.
2023-07-01 05:45:03 +02:00

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();
}
}
}