feat: diff of states
Added first subcommand - diff. It allows to check how diffrent current state of machine is in compareson to world file.
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
pub type Error = std::boxed::Box<dyn core::error::Error>;
|
||||||
|
|
||||||
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
+121
-2
@@ -13,6 +13,125 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
fn main() {
|
use std::path::PathBuf;
|
||||||
println!("Hello, world!");
|
|
||||||
|
use clap::Parser;
|
||||||
|
use clap_derive::{Parser, Subcommand};
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod package_manager;
|
||||||
|
mod world;
|
||||||
|
|
||||||
|
use error::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
package_manager::get_system_state,
|
||||||
|
world::{World, get_world_location},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "yay", feature = "paru"))]
|
||||||
|
compile_error!("'yay' and 'paru' are mutually exclusive and cannot be enabled together.");
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
compile_error!("Only (Arch) linux is supported!");
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Args {
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
/// Custom path to the world file
|
||||||
|
world: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
/// Don't run any commands that mutates state, only write them to stdout - for debug purposes
|
||||||
|
dry_run: bool,
|
||||||
|
|
||||||
|
#[arg(short, long, global = true)]
|
||||||
|
yes: bool,
|
||||||
|
|
||||||
|
#[arg(short, long, global = true)]
|
||||||
|
quiet: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Synchronizes state of machine with the world file and update everythink
|
||||||
|
Sync,
|
||||||
|
/// Export currently installed packages to the world file
|
||||||
|
Export {
|
||||||
|
#[arg(short, long)]
|
||||||
|
/// Do you want to overwrite the world file
|
||||||
|
overwrite: bool,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
/// When specified, the new world file content will be writen to stdout
|
||||||
|
stdout: bool,
|
||||||
|
},
|
||||||
|
/// Writes diffrents between the world file and the system
|
||||||
|
Diff,
|
||||||
|
/// Delete packages that aren't specified in the world file and abandond ones
|
||||||
|
Pure {
|
||||||
|
#[arg(short, long)]
|
||||||
|
/// Deletes packages with there's configs - same as -Rns instead of -Rs
|
||||||
|
all: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let world_path = match args.world {
|
||||||
|
Some(x) => x,
|
||||||
|
None => get_world_location()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Commands::Sync => todo!(),
|
||||||
|
Commands::Export { overwrite, stdout } => todo!(),
|
||||||
|
Commands::Diff => {
|
||||||
|
let world = World::load_from(world_path).unwrap_or(World::new_empty());
|
||||||
|
let world_state = world.get_packages();
|
||||||
|
let state = get_system_state()?;
|
||||||
|
|
||||||
|
let mut added = state.exclude(world_state);
|
||||||
|
let mut removed = world_state.exclude(&state);
|
||||||
|
|
||||||
|
added.ignore = world_state.ignore.clone();
|
||||||
|
removed.ignore = world_state.ignore.clone();
|
||||||
|
|
||||||
|
added.exclude_ignored();
|
||||||
|
removed.exclude_ignored();
|
||||||
|
|
||||||
|
let mut s = "Official\n".to_string();
|
||||||
|
|
||||||
|
// official added
|
||||||
|
added
|
||||||
|
.official
|
||||||
|
.iter()
|
||||||
|
.for_each(|x| s += &format!("[+] {}\n", x));
|
||||||
|
// official removed
|
||||||
|
removed
|
||||||
|
.official
|
||||||
|
.iter()
|
||||||
|
.for_each(|x| s += &format!("[-] {}\n", x));
|
||||||
|
|
||||||
|
s += "\nForeign\n";
|
||||||
|
// foreign added
|
||||||
|
added
|
||||||
|
.foreign
|
||||||
|
.iter()
|
||||||
|
.for_each(|x| s += &format!("[+] {}\n", x));
|
||||||
|
// foreign removed
|
||||||
|
removed
|
||||||
|
.foreign
|
||||||
|
.iter()
|
||||||
|
.for_each(|x| s += &format!("[-] {}\n", x));
|
||||||
|
|
||||||
|
println!("{}", s);
|
||||||
|
}
|
||||||
|
Commands::Pure { all } => todo!(),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use crate::{error::Result, world::Packages};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
pub fn get_system_state() -> Result<Packages> {
|
||||||
|
let command = Command::new("pacman")
|
||||||
|
.arg("-Qqe")
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
let official: Vec<String> = String::from_utf8(command.stdout)?
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|x| x.to_owned())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let command = Command::new("pacman")
|
||||||
|
.arg("-Qqm")
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
let foreign: Vec<String> = String::from_utf8(command.stdout)?
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|x| x.to_owned())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Packages {
|
||||||
|
official,
|
||||||
|
foreign,
|
||||||
|
ignore: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::{fs::File, path::PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
pub fn get_world_location() -> Result<PathBuf> {
|
||||||
|
match env::var("TOPAZ_WORLD") {
|
||||||
|
Ok(x) => Ok(x.into()),
|
||||||
|
Err(env::VarError::NotPresent) => Ok("/etc/topaz/world.toml".into()),
|
||||||
|
Err(env::VarError::NotUnicode(_)) => Err("World location in env is invalid!".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Packages {
|
||||||
|
pub official: Vec<String>,
|
||||||
|
pub foreign: Vec<String>,
|
||||||
|
pub ignore: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct World {
|
||||||
|
packages: Packages,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Packages {
|
||||||
|
pub fn new() -> Packages {
|
||||||
|
Packages {
|
||||||
|
official: Vec::new(),
|
||||||
|
foreign: Vec::new(),
|
||||||
|
ignore: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Packages {
|
||||||
|
pub fn exclude(&self, other: &Self) -> Self {
|
||||||
|
let official = self
|
||||||
|
.official
|
||||||
|
.iter()
|
||||||
|
.filter(|x| !other.official.contains(x))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
let foreign = self
|
||||||
|
.foreign
|
||||||
|
.iter()
|
||||||
|
.filter(|x| !other.foreign.contains(x))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Packages {
|
||||||
|
official,
|
||||||
|
foreign,
|
||||||
|
ignore: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exclude_ignored(&mut self) {
|
||||||
|
self.official.retain(|x| !self.ignore.contains(x));
|
||||||
|
self.foreign.retain(|x| !self.ignore.contains(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World {
|
||||||
|
pub fn new_empty() -> World {
|
||||||
|
World {
|
||||||
|
packages: Packages::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_packages(&self) -> &Packages {
|
||||||
|
&self.packages
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut_packages(&mut self) -> &mut Packages {
|
||||||
|
&mut self.packages
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_from(world_path: PathBuf) -> Result<World> {
|
||||||
|
let mut file = File::open(world_path)?;
|
||||||
|
let mut text = String::new();
|
||||||
|
file.read_to_string(&mut text)?;
|
||||||
|
|
||||||
|
let world: World = toml::from_str(&text)?;
|
||||||
|
|
||||||
|
Ok(world)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, world_path: Option<PathBuf>, do_print: bool) -> Result<()> {
|
||||||
|
let text = toml::to_string_pretty(self)?;
|
||||||
|
|
||||||
|
if let Some(world_path) = world_path {
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(world_path)?;
|
||||||
|
|
||||||
|
file.write_all(text.as_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if do_print {
|
||||||
|
println!("{}", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user