introduce workspace
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
use macros::Action;
|
||||
|
||||
#[derive(Debug, Action)]
|
||||
pub(crate) enum Actions {
|
||||
Clean,
|
||||
Edit,
|
||||
Groups,
|
||||
Import,
|
||||
New,
|
||||
Remove,
|
||||
Review,
|
||||
Search,
|
||||
Show,
|
||||
Sync,
|
||||
Unmanaged,
|
||||
Version,
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use path_absolutize::Absolutize;
|
||||
|
||||
use crate::action::*;
|
||||
use crate::core::get_version_string;
|
||||
|
||||
fn get_arg_parser() -> Command {
|
||||
Command::new("pacdef")
|
||||
.about("declarative package manager for Arch Linux")
|
||||
.version(get_version_string())
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(Command::new(CLEAN).about("remove unmanaged packages"))
|
||||
.subcommand(
|
||||
Command::new(EDIT)
|
||||
.about("edit one or more existing group files")
|
||||
.arg_required_else_help(true)
|
||||
.arg(Arg::new("group").num_args(1..)),
|
||||
)
|
||||
.subcommand(Command::new(GROUPS).about("show names of imported groups"))
|
||||
.subcommand(
|
||||
Command::new(IMPORT)
|
||||
.about("import one or more group files")
|
||||
.arg_required_else_help(true)
|
||||
.arg(Arg::new("files").num_args(1..)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(NEW)
|
||||
.about("create new group files")
|
||||
.arg_required_else_help(true)
|
||||
.arg(
|
||||
Arg::new("edit")
|
||||
.short('e')
|
||||
.long("edit")
|
||||
.help("edit the new group files after creation")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.arg(Arg::new("groups").num_args(1..)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(REMOVE)
|
||||
.about("remove one or more previously imported groups")
|
||||
.arg_required_else_help(true)
|
||||
.arg(Arg::new("groups").num_args(1..)),
|
||||
)
|
||||
.subcommand(Command::new(REVIEW).about("review unmanaged packages"))
|
||||
.subcommand(
|
||||
Command::new(SEARCH)
|
||||
.about("search for packages which match a provided string literal or regex")
|
||||
.arg_required_else_help(true)
|
||||
.arg(Arg::new("string")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(SHOW)
|
||||
.about("show packages under an imported group")
|
||||
.arg_required_else_help(true)
|
||||
.arg(Arg::new("group").num_args(1..)),
|
||||
)
|
||||
.subcommand(Command::new(SYNC).about("install packages from all imported groups"))
|
||||
.subcommand(
|
||||
Command::new(UNMANAGED)
|
||||
.about("show explicitly installed packages not managed by pacdef"),
|
||||
)
|
||||
.subcommand(Command::new(VERSION).about("show version info"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get() -> clap::ArgMatches {
|
||||
get_arg_parser().get_matches()
|
||||
}
|
||||
|
||||
pub(crate) fn get_absolutized_file_paths(arg_match: &ArgMatches) -> Vec<PathBuf> {
|
||||
arg_match
|
||||
.get_many::<String>("files")
|
||||
.unwrap()
|
||||
.cloned()
|
||||
.map(PathBuf::from)
|
||||
.map(|path| path.absolutize().unwrap().into_owned())
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod pacman;
|
||||
pub mod rust;
|
||||
@@ -0,0 +1,115 @@
|
||||
use std::collections::HashSet;
|
||||
use std::process::{Command, ExitStatus};
|
||||
|
||||
use alpm::Alpm;
|
||||
use alpm::PackageReason::Explicit;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::backend::backend_trait::*;
|
||||
use crate::{impl_backend_constants, Group, Package};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Pacman {
|
||||
pub(crate) binary: String,
|
||||
pub(crate) aur_rm_args: Option<Vec<String>>,
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
const BINARY: Text = "paru";
|
||||
const SECTION: Text = "pacman";
|
||||
|
||||
const SWITCHES_INFO: Switches = &["--query", "--info"];
|
||||
const SWITCHES_INSTALL: Switches = &["--sync"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &["--database", "--asdeps"];
|
||||
const SWITCHES_REMOVE: Switches = &["--remove", "--recursive"];
|
||||
|
||||
impl Backend for Pacman {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let alpm_packages = get_all_installed_packages_from_alpm()
|
||||
.context("getting all installed packages from alpm")?;
|
||||
|
||||
let result = convert_to_pacdef_packages(alpm_packages);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let alpm_packages = get_explicitly_installed_packages_from_alpm()
|
||||
.context("getting all installed packages from alpm")?;
|
||||
let result = convert_to_pacdef_packages(alpm_packages);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Install the specified packages.
|
||||
fn install_packages(&self, packages: &[Package]) -> Result<ExitStatus> {
|
||||
let mut cmd = Command::new(&self.binary);
|
||||
|
||||
cmd.args(self.get_switches_install());
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
cmd.status()
|
||||
.with_context(|| format!("running command {cmd:?}"))
|
||||
}
|
||||
|
||||
/// Remove the specified packages.
|
||||
fn remove_packages(&self, packages: &[Package]) -> Result<ExitStatus> {
|
||||
let mut cmd = Command::new(&self.binary);
|
||||
|
||||
cmd.args(self.get_switches_remove());
|
||||
if let Some(rm_args) = &self.aur_rm_args {
|
||||
cmd.args(rm_args);
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
|
||||
cmd.status()
|
||||
.with_context(|| format!("running command [{cmd:?}]"))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_all_installed_packages_from_alpm() -> Result<HashSet<String>> {
|
||||
let db = get_db_handle().context("getting DB handle")?;
|
||||
let result = db
|
||||
.localdb()
|
||||
.pkgs()
|
||||
.iter()
|
||||
.map(|p| p.name().to_string())
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages_from_alpm() -> Result<HashSet<String>> {
|
||||
let db = get_db_handle().context("getting DB handle")?;
|
||||
let result = db
|
||||
.localdb()
|
||||
.pkgs()
|
||||
.iter()
|
||||
.filter(|p| p.reason() == Explicit)
|
||||
.map(|p| p.name().to_string())
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn convert_to_pacdef_packages(packages: HashSet<String>) -> HashSet<Package> {
|
||||
packages.into_iter().map(Package::from).collect()
|
||||
}
|
||||
|
||||
fn get_db_handle() -> Result<Alpm> {
|
||||
Alpm::new("/", "/var/lib/pacman").context("connecting to DB using expected default values")
|
||||
}
|
||||
|
||||
impl Pacman {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
binary: BINARY.to_string(),
|
||||
aur_rm_args: None,
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitStatus;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::backend::backend_trait::*;
|
||||
use crate::{impl_backend_constants, Group, Package};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Rust {
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
const BINARY: Text = "cargo";
|
||||
const SECTION: Text = "rust";
|
||||
const SWITCHES_INSTALL: Switches = &["install"];
|
||||
const SWITCHES_INFO: Switches = &["search", "--limit", "1"];
|
||||
const SWITCHES_MAKE_DEPENDENCY: Switches = &[];
|
||||
const SWITCHES_REMOVE: Switches = &["uninstall"];
|
||||
|
||||
impl Backend for Rust {
|
||||
impl_backend_constants!();
|
||||
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
let file = get_crates_file().context("getting path to crates file")?;
|
||||
let content = read_to_string(file).context("reading crates file")?;
|
||||
let json: Value =
|
||||
serde_json::from_str(&content).context("parsing JSON from crates file")?;
|
||||
extract_packages(&json).context("extracing packages from crates file")
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
self.get_all_installed_packages()
|
||||
.context("getting all installed packages")
|
||||
}
|
||||
|
||||
fn make_dependency(&self, _: &[Package]) -> Result<ExitStatus> {
|
||||
unreachable!("cargo does not have unmanaged packages")
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_packages(json: &Value) -> Result<HashSet<Package>> {
|
||||
let result: HashSet<_> = json
|
||||
.get("installs")
|
||||
.context("get 'installs' field from json")?
|
||||
.as_object()
|
||||
.context("getting object")?
|
||||
.into_iter()
|
||||
.map(|(name, _)| name)
|
||||
.map(|name| {
|
||||
name.split_whitespace()
|
||||
.next()
|
||||
.expect("identifier is whitespace-delimited")
|
||||
})
|
||||
.map(|name| Package::try_from(name).expect("name is valid"))
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl Rust {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_crates_file() -> Result<PathBuf> {
|
||||
let mut result = crate::path::get_home_dir().context("getting home dir")?;
|
||||
result.push(".cargo");
|
||||
result.push(".crates2.json");
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::process::Command;
|
||||
use std::rc::Rc;
|
||||
use std::{collections::HashSet, process::ExitStatus};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::{Group, Package};
|
||||
|
||||
pub(in crate::backend) type Switches = &'static [&'static str];
|
||||
pub(in crate::backend) type Text = &'static str;
|
||||
|
||||
pub(crate) trait Backend: Debug {
|
||||
fn get_binary(&self) -> Text;
|
||||
fn get_section(&self) -> Text;
|
||||
|
||||
fn get_switches_info(&self) -> Switches;
|
||||
fn get_switches_install(&self) -> Switches;
|
||||
fn get_switches_remove(&self) -> Switches;
|
||||
fn get_switches_make_dependency(&self) -> Switches;
|
||||
|
||||
fn load(&mut self, groups: &HashSet<Group>);
|
||||
|
||||
fn get_managed_packages(&self) -> &HashSet<Package>;
|
||||
|
||||
/// Get all packages that are installed in the system.
|
||||
fn get_all_installed_packages(&self) -> Result<HashSet<Package>>;
|
||||
|
||||
/// Get all packages that were installed in the system explicitly.
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>>;
|
||||
|
||||
fn assign_group(&self, to_assign: Vec<(Package, Rc<Group>)>) {
|
||||
let group_package_map = get_group_packages_map(to_assign);
|
||||
let section_header = format!("[{}]", self.get_section());
|
||||
|
||||
for (group, packages) in group_package_map {
|
||||
group.save_packages(§ion_header, &packages);
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the specified packages.
|
||||
fn install_packages(&self, packages: &[Package]) -> Result<ExitStatus> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_install());
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
cmd.status()
|
||||
.with_context(|| format!("running command {cmd:?}"))
|
||||
}
|
||||
|
||||
fn make_dependency(&self, packages: &[Package]) -> Result<ExitStatus> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_make_dependency());
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
cmd.status()
|
||||
.with_context(|| format!("running command [{cmd:?}]"))
|
||||
}
|
||||
|
||||
/// Remove the specified packages.
|
||||
fn remove_packages(&self, packages: &[Package]) -> Result<ExitStatus> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_remove());
|
||||
for p in packages {
|
||||
cmd.arg(format!("{p}"));
|
||||
}
|
||||
cmd.status()
|
||||
.with_context(|| format!("running command [{cmd:?}]"))
|
||||
}
|
||||
|
||||
/// extract packages from its own section as read from group files
|
||||
fn extract_packages_from_group_file_content(&self, content: &str) -> HashSet<Package> {
|
||||
content
|
||||
.lines()
|
||||
.skip_while(|line| !line.starts_with(&format!("[{}]", self.get_section())))
|
||||
.skip(1)
|
||||
.filter(|line| !line.starts_with('['))
|
||||
.fuse()
|
||||
.filter_map(Package::try_from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_missing_packages_sorted(&self) -> Result<Vec<Package>> {
|
||||
let installed = self
|
||||
.get_all_installed_packages()
|
||||
.context("could not get installed packages")?;
|
||||
let managed = self.get_managed_packages();
|
||||
let mut diff: Vec<_> = managed.difference(&installed).cloned().collect();
|
||||
diff.sort_unstable();
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
fn add_packages(&mut self, packages: HashSet<Package>);
|
||||
|
||||
/// Show information from package manager for package.
|
||||
fn show_package_info(&self, package: &Package) -> Result<ExitStatus> {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(self.get_switches_info());
|
||||
cmd.arg(format!("{package}"));
|
||||
cmd.status()
|
||||
.with_context(|| format!("running command {cmd:?}"))
|
||||
}
|
||||
|
||||
fn get_unmanaged_packages_sorted(&self) -> Result<Vec<Package>> {
|
||||
let installed = self
|
||||
.get_explicitly_installed_packages()
|
||||
.context("could not get explicitly installed packages")?;
|
||||
let required = self.get_managed_packages();
|
||||
let mut diff: Vec<_> = installed.difference(required).cloned().collect();
|
||||
diff.sort_unstable();
|
||||
Ok(diff)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_group_packages_map(
|
||||
to_assign: Vec<(Package, Rc<Group>)>,
|
||||
) -> HashMap<Rc<Group>, Vec<Package>> {
|
||||
let mut group_package_map = HashMap::new();
|
||||
|
||||
for (p, group) in to_assign {
|
||||
if !group_package_map.contains_key(&group) {
|
||||
group_package_map.insert(group.clone(), vec![]);
|
||||
}
|
||||
|
||||
let inner = group_package_map.get_mut(&group).unwrap();
|
||||
inner.push(p);
|
||||
}
|
||||
|
||||
for vecs in group_package_map.values_mut() {
|
||||
vecs.sort();
|
||||
}
|
||||
group_package_map
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use super::{Backend, Backends};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BackendIter {
|
||||
pub(crate) next: Option<Backends>,
|
||||
}
|
||||
|
||||
impl Iterator for BackendIter {
|
||||
type Item = Box<dyn Backend>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match &self.next {
|
||||
None => None,
|
||||
Some(b) => {
|
||||
let result = b.get_backend();
|
||||
self.next = b.next();
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#[macro_export]
|
||||
macro_rules! impl_backend_constants {
|
||||
() => {
|
||||
fn get_binary(&self) -> Text {
|
||||
BINARY
|
||||
}
|
||||
|
||||
fn get_section(&self) -> Text {
|
||||
SECTION
|
||||
}
|
||||
|
||||
fn get_switches_info(&self) -> Switches {
|
||||
SWITCHES_INFO
|
||||
}
|
||||
|
||||
fn get_switches_install(&self) -> Switches {
|
||||
SWITCHES_INSTALL
|
||||
}
|
||||
|
||||
fn get_switches_remove(&self) -> Switches {
|
||||
SWITCHES_REMOVE
|
||||
}
|
||||
|
||||
fn get_switches_make_dependency(&self) -> Switches {
|
||||
SWITCHES_MAKE_DEPENDENCY
|
||||
}
|
||||
|
||||
fn get_managed_packages(&self) -> &HashSet<Package> {
|
||||
&self.packages
|
||||
}
|
||||
|
||||
fn load(&mut self, groups: &HashSet<Group>) {
|
||||
let own_section_name = self.get_section();
|
||||
|
||||
groups
|
||||
.iter()
|
||||
.flat_map(|g| &g.sections)
|
||||
.filter(|section| section.name == own_section_name)
|
||||
.flat_map(|section| §ion.packages)
|
||||
.for_each(|package| {
|
||||
self.packages.insert(package.clone());
|
||||
})
|
||||
}
|
||||
|
||||
fn add_packages(&mut self, packages: HashSet<Package>) {
|
||||
for p in packages {
|
||||
self.packages.insert(p);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
mod actual;
|
||||
mod backend_trait;
|
||||
mod iter;
|
||||
mod macros;
|
||||
mod todo_per_backend;
|
||||
|
||||
pub(crate) use backend_trait::Backend;
|
||||
pub(crate) use iter::BackendIter;
|
||||
pub(crate) use todo_per_backend::ToDoPerBackend;
|
||||
|
||||
use ::macros::Register;
|
||||
|
||||
#[derive(Debug, Register)]
|
||||
pub(crate) enum Backends {
|
||||
Pacman,
|
||||
Rust,
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use std::process::ExitStatus;
|
||||
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
|
||||
use super::Backend;
|
||||
use crate::Package;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ToDoPerBackend(Vec<(Box<dyn Backend>, Vec<Package>)>);
|
||||
|
||||
impl ToDoPerBackend {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn push(&mut self, item: (Box<dyn Backend>, Vec<Package>)) {
|
||||
self.0.push(item);
|
||||
}
|
||||
|
||||
pub(crate) fn into_iter(self) -> impl Iterator<Item = (Box<dyn Backend>, Vec<Package>)> {
|
||||
self.0.into_iter()
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &(Box<dyn Backend>, Vec<Package>)> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn nothing_to_do_for_all_backends(&self) -> bool {
|
||||
self.0.iter().all(|(_, diff)| diff.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn install_missing_packages(&self) -> Result<()> {
|
||||
self.handle_backend_command(Backend::install_packages, "install", "installing")
|
||||
.context("installing packages")
|
||||
}
|
||||
|
||||
pub(crate) fn remove_unmanaged_packages(&self) -> Result<()> {
|
||||
self.handle_backend_command(Backend::remove_packages, "remove", "removing")
|
||||
.context("removing packages")
|
||||
}
|
||||
|
||||
fn handle_backend_command<'a, F>(
|
||||
&'a self,
|
||||
func: F,
|
||||
verb: &'_ str,
|
||||
verb_continuous: &'_ str,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: Fn(&'a dyn Backend, &'a [Package]) -> Result<ExitStatus>,
|
||||
{
|
||||
for (backend, packages) in &self.0 {
|
||||
if packages.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let exit_status = func(&**backend, packages).with_context(|| {
|
||||
format!("{verb_continuous} packages for {}", backend.get_binary())
|
||||
})?;
|
||||
|
||||
match exit_status.code() {
|
||||
Some(val) => ensure!(val == 0, "command returned with exit code {val}"),
|
||||
None => bail!("could not {verb} packages for {}", backend.get_binary()),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn show(&self, indentend: bool) {
|
||||
for (backend, packages) in self.iter() {
|
||||
if packages.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if indentend {
|
||||
print!(" ");
|
||||
}
|
||||
println!("[{}]", backend.get_section());
|
||||
for package in packages {
|
||||
if indentend {
|
||||
print!(" ");
|
||||
}
|
||||
println!(" {package}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, ExitStatus};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use crate::env::get_editor;
|
||||
|
||||
pub fn run_edit_command(files: &[PathBuf]) -> Result<ExitStatus> {
|
||||
let mut cmd = Command::new(get_editor().context("getting suitable editor")?);
|
||||
cmd.current_dir(files[0].parent().unwrap());
|
||||
for f in files {
|
||||
cmd.arg(f.to_string_lossy().to_string());
|
||||
}
|
||||
cmd.status().map_err(|e| anyhow!(e))
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use std::fs::{read_to_string, File};
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::path::get_pacdef_base_dir;
|
||||
|
||||
const CONFIG_FILE_NAME: &str = "pacdef.yaml";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub aur_helper: String,
|
||||
pub aur_rm_args: Option<Vec<String>>,
|
||||
pub warn_not_symlinks: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self> {
|
||||
let file = get_config_file()?;
|
||||
let from_file = read_to_string(&file);
|
||||
|
||||
if let Err(e) = from_file {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
println!("creating default config under {file:?}");
|
||||
return Self::use_default_and_save_to(file);
|
||||
} else {
|
||||
bail!("unexpected error occured: {e:?}");
|
||||
};
|
||||
}
|
||||
|
||||
let content = from_file.unwrap();
|
||||
|
||||
serde_yaml::from_str(&content).context("parsing yaml config")
|
||||
}
|
||||
|
||||
fn use_default_and_save_to(file: PathBuf) -> Result<Self> {
|
||||
let result = Self::default();
|
||||
|
||||
let content = serde_yaml::to_string(&result).context("converting Config to yaml")?;
|
||||
let mut output = File::create(file).context("creating default config file")?;
|
||||
write!(output, "{content}").context("writing default config")?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config_file() -> Result<PathBuf> {
|
||||
let mut file = get_pacdef_base_dir().context("getting pacdef base dir for config file")?;
|
||||
file.push(CONFIG_FILE_NAME);
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
aur_helper: "paru".into(),
|
||||
aur_rm_args: None,
|
||||
warn_not_symlinks: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fs::{remove_file, File};
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, ensure, Context, Result};
|
||||
use clap::ArgMatches;
|
||||
|
||||
use crate::action::*;
|
||||
use crate::args;
|
||||
use crate::backend::{Backend, Backends, ToDoPerBackend};
|
||||
use crate::cmd::run_edit_command;
|
||||
use crate::env::get_single_var;
|
||||
use crate::path::get_pacdef_group_dir;
|
||||
use crate::review;
|
||||
use crate::search;
|
||||
use crate::ui::get_user_confirmation;
|
||||
use crate::Config;
|
||||
use crate::Group;
|
||||
|
||||
pub struct Pacdef {
|
||||
args: ArgMatches,
|
||||
config: Config,
|
||||
groups: HashSet<Group>,
|
||||
}
|
||||
|
||||
impl Pacdef {
|
||||
#[must_use]
|
||||
pub fn new(args: ArgMatches, config: Config, groups: HashSet<Group>) -> Self {
|
||||
Self {
|
||||
args,
|
||||
config,
|
||||
groups,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unit_arg)]
|
||||
pub fn run_action_from_arg(mut self) -> Result<()> {
|
||||
match self.args.subcommand() {
|
||||
Some((CLEAN, _)) => self.clean_packages(),
|
||||
Some((EDIT, args)) => self.edit_group_files(args).context("editing group files"),
|
||||
Some((GROUPS, _)) => Ok(self.show_groups()),
|
||||
Some((IMPORT, args)) => self.import_groups(args).context("importing groups"),
|
||||
Some((NEW, args)) => self.new_groups(args).context("creating new group files"),
|
||||
Some((REMOVE, args)) => self.remove_groups(args).context("removing groups"),
|
||||
Some((REVIEW, _)) => review::review(self.get_unmanaged_packages(), self.groups)
|
||||
.context("removing groups"),
|
||||
Some((SHOW, args)) => self.show_group_content(args).context("showing groups"),
|
||||
Some((SEARCH, args)) => {
|
||||
search::search_packages(args, &self.groups).context("searching packages")
|
||||
}
|
||||
Some((SYNC, _)) => self.install_packages(),
|
||||
Some((UNMANAGED, _)) => Ok(self.show_unmanaged_packages()),
|
||||
Some((VERSION, _)) => Ok(self.show_version()),
|
||||
Some((_, _)) => panic!(),
|
||||
None => {
|
||||
unreachable!("argument parser requires some subcommand to return an `ArgMatches`")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_missing_packages(&mut self) -> ToDoPerBackend {
|
||||
let mut to_install = ToDoPerBackend::new();
|
||||
|
||||
for backend in Backends::iter() {
|
||||
let mut backend = self.overwrite_values_from_config(backend);
|
||||
|
||||
backend.load(&self.groups);
|
||||
|
||||
match backend.get_missing_packages_sorted() {
|
||||
Ok(diff) => to_install.push((backend, diff)),
|
||||
Err(error) => show_error(&error, &*backend),
|
||||
};
|
||||
}
|
||||
|
||||
to_install
|
||||
}
|
||||
|
||||
fn overwrite_values_from_config(&mut self, backend: Box<dyn Backend>) -> Box<dyn Backend> {
|
||||
if backend.get_section() == "pacman" {
|
||||
Box::new(crate::backend::Pacman {
|
||||
binary: self.config.aur_helper.clone(),
|
||||
aur_rm_args: self.config.aur_rm_args.take(),
|
||||
packages: HashSet::new(),
|
||||
})
|
||||
} else {
|
||||
backend
|
||||
}
|
||||
}
|
||||
|
||||
fn install_packages(&mut self) -> Result<()> {
|
||||
let to_install = self.get_missing_packages();
|
||||
|
||||
if to_install.nothing_to_do_for_all_backends() {
|
||||
println!("nothing to do");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Would install the following packages:");
|
||||
to_install.show(true);
|
||||
|
||||
if !get_user_confirmation()? {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
to_install.install_missing_packages()
|
||||
}
|
||||
|
||||
fn edit_group_files(&self, groups: &ArgMatches) -> Result<()> {
|
||||
let group_dir = crate::path::get_pacdef_group_dir()?;
|
||||
|
||||
let files: Vec<_> = groups
|
||||
.get_many::<String>("group")
|
||||
.context("getting group from args")?
|
||||
.map(|file| {
|
||||
let mut buf = group_dir.clone();
|
||||
buf.push(file);
|
||||
buf
|
||||
})
|
||||
.collect();
|
||||
|
||||
for file in &files {
|
||||
ensure!(
|
||||
file.exists(),
|
||||
"group file {} not found",
|
||||
file.to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
let success = run_edit_command(&files)
|
||||
.context("running editor")?
|
||||
.success();
|
||||
|
||||
ensure!(success, "editor exited with error");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_version(self) {
|
||||
println!("{}", get_version_string());
|
||||
}
|
||||
|
||||
fn show_unmanaged_packages(mut self) {
|
||||
let unmanaged_per_backend = &self.get_unmanaged_packages();
|
||||
|
||||
unmanaged_per_backend.show(false);
|
||||
}
|
||||
|
||||
fn get_unmanaged_packages(&mut self) -> ToDoPerBackend {
|
||||
let mut result = ToDoPerBackend::new();
|
||||
|
||||
for backend in Backends::iter() {
|
||||
let mut backend = self.overwrite_values_from_config(backend);
|
||||
|
||||
backend.load(&self.groups);
|
||||
|
||||
match backend.get_unmanaged_packages_sorted() {
|
||||
Ok(unmanaged) => result.push((backend, unmanaged)),
|
||||
Err(error) => show_error(&error, &*backend),
|
||||
};
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn show_groups(self) {
|
||||
let mut vec: Vec<_> = self.groups.iter().collect();
|
||||
vec.sort_unstable();
|
||||
for g in vec {
|
||||
println!("{}", g.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_packages(mut self) -> Result<()> {
|
||||
let to_remove = self.get_unmanaged_packages();
|
||||
|
||||
if to_remove.nothing_to_do_for_all_backends() {
|
||||
println!("nothing to do");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Would remove the following packages");
|
||||
to_remove.show(true);
|
||||
|
||||
if !get_user_confirmation()? {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
to_remove.remove_unmanaged_packages()
|
||||
}
|
||||
|
||||
fn show_group_content(&self, groups: &ArgMatches) -> Result<()> {
|
||||
let mut iter = groups.get_many::<String>("group").unwrap().peekable();
|
||||
|
||||
let show_more_than_one_group = iter.size_hint().0 > 1;
|
||||
|
||||
while let Some(arg_group) = iter.next() {
|
||||
let group = self
|
||||
.groups
|
||||
.iter()
|
||||
.find(|g| g.name == *arg_group)
|
||||
.ok_or_else(|| anyhow!("group {} not found", *arg_group))?;
|
||||
|
||||
if show_more_than_one_group {
|
||||
let name = &group.name;
|
||||
println!("{name}");
|
||||
for _ in 0..name.len() {
|
||||
print!("-");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{group}");
|
||||
if iter.peek().is_some() {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn import_groups(&self, args: &ArgMatches) -> Result<()> {
|
||||
let files = args::get_absolutized_file_paths(args);
|
||||
let groups_dir = get_pacdef_group_dir()?;
|
||||
|
||||
for target in files {
|
||||
let target_name = target.file_name().unwrap().to_str().unwrap();
|
||||
|
||||
if !target.exists() {
|
||||
println!("file {target_name} does not exist, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut link = groups_dir.clone();
|
||||
link.push(target_name);
|
||||
|
||||
if link.exists() {
|
||||
println!("group {target_name} already exists, skipping");
|
||||
} else {
|
||||
symlink(target, link)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_groups(&self, arg_match: &ArgMatches) -> Result<()> {
|
||||
let paths = get_assumed_group_file_names(arg_match)?;
|
||||
|
||||
for file in &paths {
|
||||
ensure!(file.exists(), "did not find the group under {file:?}");
|
||||
}
|
||||
|
||||
for file in paths {
|
||||
remove_file(file)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_groups(&self, arg: &ArgMatches) -> Result<()> {
|
||||
let paths = get_assumed_group_file_names(arg)?;
|
||||
|
||||
for file in &paths {
|
||||
ensure!(!file.exists(), "group already exists under {file:?}");
|
||||
}
|
||||
|
||||
for file in &paths {
|
||||
File::create(file)?;
|
||||
}
|
||||
|
||||
if arg.get_flag("edit") {
|
||||
let success = run_edit_command(&paths)
|
||||
.context("running editor")?
|
||||
.success();
|
||||
|
||||
ensure!(success, "editor exited with error");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_assumed_group_file_names(arg_match: &ArgMatches) -> Result<Vec<PathBuf>> {
|
||||
let groups_dir = get_pacdef_group_dir()?;
|
||||
|
||||
let paths: Vec<_> = arg_match
|
||||
.get_many::<String>("groups")
|
||||
.unwrap()
|
||||
.map(|s| {
|
||||
let mut possible_group_file = groups_dir.clone();
|
||||
possible_group_file.push(s);
|
||||
possible_group_file
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
fn show_error(error: &anyhow::Error, backend: &dyn Backend) {
|
||||
let section = backend.get_section();
|
||||
match get_single_var("RUST_BACKTRACE") {
|
||||
Some(s) => {
|
||||
if s == "1" || s == "full" {
|
||||
println!("WARNING: skipping backend '{section}': {error:?}\n");
|
||||
}
|
||||
}
|
||||
None => println!("WARNING: skipping backend '{section}': {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn get_version_string() -> &'static str {
|
||||
concat!(
|
||||
"pacdef, version: ",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (",
|
||||
env!("GIT_HASH"),
|
||||
")",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use std::env::var;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
pub(crate) fn get_editor() -> Result<String> {
|
||||
check_vars_in_order(&["EDITOR", "VISUAL"]).ok_or_else(|| anyhow!("could not find editor"))
|
||||
}
|
||||
|
||||
fn check_vars_in_order(vars: &[&str]) -> Option<String> {
|
||||
vars.iter().find_map(|v| var(v).ok())
|
||||
}
|
||||
|
||||
pub(crate) fn get_single_var(variable: &str) -> Option<String> {
|
||||
var(variable).ok()
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
use std::fs::{read_to_string, File};
|
||||
use std::hash::Hash;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{collections::HashSet, fmt::Display};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use super::{Package, Section};
|
||||
|
||||
use crate::Config;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Group {
|
||||
pub(crate) name: String,
|
||||
pub(crate) sections: HashSet<Section>,
|
||||
pub(crate) path: PathBuf,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
pub fn load(config: &Config) -> Result<HashSet<Self>> {
|
||||
let mut result = HashSet::new();
|
||||
|
||||
let path = crate::path::get_pacdef_group_dir().context("getting pacdef group dir")?;
|
||||
for entry in path.read_dir().context("reading group dir")? {
|
||||
let file = entry.context("getting group file")?;
|
||||
let path = file.path();
|
||||
|
||||
if config.warn_not_symlinks && !path.is_symlink() {
|
||||
println!("WARNING: group file {path:?} is not a symlink");
|
||||
}
|
||||
|
||||
let group =
|
||||
Group::try_from(&path).with_context(|| format!("reading group file {path:?}"))?;
|
||||
|
||||
result.insert(group);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Group {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
match self.name.partial_cmp(&other.name) {
|
||||
Some(core::cmp::Ordering::Equal) => None,
|
||||
ord => ord,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Group {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.name.cmp(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Group {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Group {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Group {
|
||||
fn assert_receiver_is_total_eq(&self) {}
|
||||
}
|
||||
|
||||
impl Group {
|
||||
fn try_from<P>(p: P) -> Result<Self>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path = p.as_ref();
|
||||
let content = read_to_string(path).context("reading file content")?;
|
||||
let name = path
|
||||
.file_name()
|
||||
.context("getting file name")?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let mut lines = content.lines().peekable();
|
||||
let mut sections = HashSet::new();
|
||||
|
||||
while lines.peek().is_some() {
|
||||
match Section::try_from_lines(&mut lines).context("reading section") {
|
||||
Ok(section) => {
|
||||
sections.insert(section);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("WARNING: could not process a section under group '{name}': {e:?}\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sections.is_empty() {
|
||||
println!("WARNING: no sections found in group '{name}'");
|
||||
}
|
||||
|
||||
let path = path.into();
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
sections,
|
||||
path,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn save_packages(&self, section_header: &str, packages: &[Package]) {
|
||||
let mut content = read_to_string(&self.path).unwrap();
|
||||
|
||||
if content.contains(section_header) {
|
||||
write_packages_to_existing_section(&mut content, section_header, packages);
|
||||
} else {
|
||||
add_new_section_with_packages(&mut content, section_header, packages);
|
||||
}
|
||||
|
||||
let mut file = File::create(&self.path).unwrap();
|
||||
write!(file, "{content}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Group {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut sections: Vec<_> = self.sections.iter().collect();
|
||||
sections.sort_unstable();
|
||||
|
||||
let mut iter = sections.into_iter().peekable();
|
||||
|
||||
while let Some(section) = iter.next() {
|
||||
section.fmt(f)?;
|
||||
if iter.peek().is_some() {
|
||||
f.write_str("\n\n")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_packages_to_existing_section(
|
||||
group_file_content: &mut String,
|
||||
section_header: &str,
|
||||
packages: &[Package],
|
||||
) {
|
||||
let idx_of_first_package_line_in_section =
|
||||
find_first_package_line_in_section(group_file_content, section_header);
|
||||
|
||||
let after = group_file_content.split_off(idx_of_first_package_line_in_section);
|
||||
|
||||
for p in packages {
|
||||
group_file_content.push_str(&format!("{p}\n"));
|
||||
}
|
||||
|
||||
group_file_content.push_str(&after);
|
||||
}
|
||||
|
||||
fn find_first_package_line_in_section(group_file_content: &str, section_header: &str) -> usize {
|
||||
let section_start = group_file_content.find(section_header).unwrap();
|
||||
let distance_to_next_newline = group_file_content[section_start..].find('\n').unwrap();
|
||||
|
||||
section_start + distance_to_next_newline + 1 // + 1 to be after the newline
|
||||
}
|
||||
|
||||
fn add_new_section_with_packages(
|
||||
group_file_content: &mut String,
|
||||
section_header: &str,
|
||||
packages: &[Package],
|
||||
) {
|
||||
group_file_content.push('\n');
|
||||
group_file_content.push_str(section_header);
|
||||
group_file_content.push('\n');
|
||||
for p in packages {
|
||||
group_file_content.push_str(&format!("{p}\n"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod group;
|
||||
mod package;
|
||||
mod section;
|
||||
|
||||
pub use group::Group;
|
||||
pub(super) use package::Package;
|
||||
pub(super) use section::Section;
|
||||
@@ -0,0 +1,105 @@
|
||||
use std::fmt::{Display, Write};
|
||||
use std::hash::Hash;
|
||||
|
||||
#[derive(Debug, Eq, PartialOrd, Ord, Clone)]
|
||||
pub struct Package {
|
||||
pub(crate) name: String,
|
||||
repo: Option<String>,
|
||||
}
|
||||
|
||||
fn remove_all_but_package_name(s: &str) -> &str {
|
||||
s.split('#') // remove comment
|
||||
.next()
|
||||
.expect("line contains something")
|
||||
.trim() // remove whitespace
|
||||
}
|
||||
|
||||
impl From<String> for Package {
|
||||
fn from(value: String) -> Self {
|
||||
let trimmed = remove_all_but_package_name(&value);
|
||||
debug_assert!(!trimmed.is_empty(), "empty package names are not allowed");
|
||||
|
||||
let (name, repo) = Self::split_into_name_and_repo(trimmed);
|
||||
Self { name, repo }
|
||||
}
|
||||
}
|
||||
|
||||
impl Package {
|
||||
fn split_into_name_and_repo(s: &str) -> (String, Option<String>) {
|
||||
let mut iter = s.split('/').rev();
|
||||
let name = iter.next().unwrap().to_string();
|
||||
let repo = iter.next().map(|s| s.to_string());
|
||||
(name, repo)
|
||||
}
|
||||
|
||||
pub(crate) fn try_from<S>(s: S) -> Option<Self>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let trimmed = remove_all_but_package_name(s.as_ref());
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (name, repo) = Self::split_into_name_and_repo(trimmed);
|
||||
Some(Self { name, repo })
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Package {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
&& match &self.repo {
|
||||
None => true,
|
||||
Some(r) => match &other.repo {
|
||||
None => true,
|
||||
Some(r2) => r == r2,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Package {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Package {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.repo {
|
||||
None => (),
|
||||
Some(repo) => {
|
||||
f.write_str(repo)?;
|
||||
f.write_char('/')?;
|
||||
}
|
||||
}
|
||||
f.write_str(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Package;
|
||||
|
||||
#[test]
|
||||
fn split_into_name_and_repo() {
|
||||
let x = "repo/name".to_string();
|
||||
let (name, repo) = Package::split_into_name_and_repo(&x);
|
||||
assert_eq!(name, "name");
|
||||
assert_eq!(repo, Some("repo".to_string()));
|
||||
|
||||
let x = "something".to_string();
|
||||
let (name, repo) = super::Package::split_into_name_and_repo(&x);
|
||||
assert_eq!(name, "something");
|
||||
assert_eq!(repo, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from() {
|
||||
let x = "myrepo/somepackage # ".to_string();
|
||||
let p = Package::try_from(x).unwrap();
|
||||
assert_eq!(p.name, "somepackage");
|
||||
assert_eq!(p.repo, Some("myrepo".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::{Display, Write},
|
||||
hash::Hash,
|
||||
iter::Peekable,
|
||||
};
|
||||
|
||||
use anyhow::{ensure, Context, Result};
|
||||
|
||||
use super::Package;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Section {
|
||||
pub name: String,
|
||||
pub packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
impl Section {
|
||||
pub(crate) fn new(name: String, packages: HashSet<Package>) -> Self {
|
||||
Self { name, packages }
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_lines<'a>(
|
||||
iter: &mut Peekable<(impl Iterator<Item = &'a str> + std::fmt::Debug)>,
|
||||
) -> Result<Self> {
|
||||
let name = iter
|
||||
.find(|line| line.starts_with('['))
|
||||
.context("finding beginning of next section")?
|
||||
.trim()
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
|
||||
let mut packages = HashSet::new();
|
||||
// `while let` is unstable, unfortunately
|
||||
while iter.peek().is_some() && !iter.peek().unwrap().starts_with('[') {
|
||||
if let Some(package) = Package::try_from(iter.next().unwrap()) {
|
||||
packages.insert(package);
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(!packages.is_empty());
|
||||
|
||||
Ok(Self::new(name, packages))
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Section {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Section {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Section {
|
||||
fn assert_receiver_is_total_eq(&self) {}
|
||||
}
|
||||
|
||||
impl PartialOrd for Section {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.name.partial_cmp(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Section {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Section {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("[{}]\n", &self.name))?;
|
||||
|
||||
let mut packages: Vec<_> = self.packages.iter().collect();
|
||||
packages.sort_unstable();
|
||||
|
||||
let mut iter = packages.iter().peekable();
|
||||
|
||||
while let Some(package) = iter.next() {
|
||||
package.fmt(f)?;
|
||||
if iter.peek().is_some() {
|
||||
f.write_char('\n')?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
mod action;
|
||||
pub mod args;
|
||||
mod backend;
|
||||
mod cmd;
|
||||
mod config;
|
||||
mod core;
|
||||
mod env;
|
||||
mod grouping;
|
||||
mod path;
|
||||
mod review;
|
||||
mod search;
|
||||
mod ui;
|
||||
|
||||
pub use crate::config::Config;
|
||||
pub use crate::core::Pacdef;
|
||||
pub use crate::grouping::Group;
|
||||
pub(crate) use crate::grouping::Package;
|
||||
pub use crate::search::NO_PACKAGES_FOUND;
|
||||
|
||||
extern crate macros;
|
||||
@@ -0,0 +1,29 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
pub(crate) fn get_pacdef_group_dir() -> Result<PathBuf> {
|
||||
let mut result = get_pacdef_base_dir().context("getting pacdef base dir")?;
|
||||
result.push("groups");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) fn get_pacdef_base_dir() -> Result<PathBuf> {
|
||||
let mut dir = get_xdg_config_home().context("getting XDG_CONFIG_HOME")?;
|
||||
dir.push("pacdef");
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
fn get_xdg_config_home() -> Result<PathBuf> {
|
||||
if let Ok(config) = env::var("XDG_CONFIG_HOME") {
|
||||
Ok(config.into())
|
||||
} else {
|
||||
let mut config = get_home_dir().context("falling back to $HOME/.config")?;
|
||||
config.push(".config");
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_home_dir() -> Result<PathBuf> {
|
||||
Ok(env::var("HOME").context("getting $HOME variable")?.into())
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::{Group, Package};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(super) enum ReviewAction {
|
||||
AsDependency(Package),
|
||||
Delete(Package),
|
||||
AssignGroup(Package, Rc<Group>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum ReviewIntention {
|
||||
AsDependency,
|
||||
AssignGroup,
|
||||
Delete,
|
||||
Info,
|
||||
Invalid,
|
||||
Skip,
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ReviewsPerBackend {
|
||||
items: Vec<(Box<dyn Backend>, Vec<ReviewAction>)>,
|
||||
}
|
||||
|
||||
impl ReviewsPerBackend {
|
||||
pub(super) fn new() -> Self {
|
||||
Self { items: vec![] }
|
||||
}
|
||||
|
||||
pub(super) fn nothing_to_do(&self) -> bool {
|
||||
self.items.iter().all(|(_, vec)| vec.is_empty())
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, value: (Box<dyn Backend>, Vec<ReviewAction>)) {
|
||||
self.items.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for ReviewsPerBackend {
|
||||
type Item = (Box<dyn Backend>, Vec<ReviewAction>);
|
||||
|
||||
type IntoIter = std::vec::IntoIter<(Box<dyn Backend>, Vec<ReviewAction>)>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.items.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum ContinueWithReview {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
mod datastructures;
|
||||
mod strategy;
|
||||
|
||||
use std::io::{stdin, stdout, Write};
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::backend::{Backend, ToDoPerBackend};
|
||||
use crate::ui::{get_user_confirmation, read_single_char_from_terminal};
|
||||
use crate::{Group, Package};
|
||||
|
||||
use self::datastructures::{ContinueWithReview, ReviewAction, ReviewIntention, ReviewsPerBackend};
|
||||
use self::strategy::Strategy;
|
||||
|
||||
pub(crate) fn review(
|
||||
todo_per_backend: ToDoPerBackend,
|
||||
groups: impl IntoIterator<Item = Group>,
|
||||
) -> Result<()> {
|
||||
let mut reviews = ReviewsPerBackend::new();
|
||||
let mut groups: Vec<Rc<Group>> = groups.into_iter().map(Rc::new).collect();
|
||||
|
||||
groups.sort_unstable();
|
||||
|
||||
if todo_per_backend.nothing_to_do_for_all_backends() {
|
||||
println!("nothing to do");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (backend, packages) in todo_per_backend.into_iter() {
|
||||
let mut actions = vec![];
|
||||
for package in packages {
|
||||
println!("{}: {package}", backend.get_section());
|
||||
match get_action_for_package(package, &groups, &mut actions, &*backend)? {
|
||||
ContinueWithReview::Yes => continue,
|
||||
ContinueWithReview::No => return Ok(()),
|
||||
}
|
||||
}
|
||||
reviews.push((backend, actions));
|
||||
}
|
||||
|
||||
if reviews.nothing_to_do() {
|
||||
println!("nothing to do");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let strategies: Vec<Strategy> = reviews.into();
|
||||
|
||||
for strat in &strategies {
|
||||
strat.show();
|
||||
}
|
||||
|
||||
if !get_user_confirmation()? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for strat in strategies {
|
||||
strat.execute()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_action_for_package(
|
||||
package: Package,
|
||||
groups: &[Rc<Group>],
|
||||
reviews: &mut Vec<ReviewAction>,
|
||||
backend: &dyn Backend,
|
||||
) -> Result<ContinueWithReview> {
|
||||
loop {
|
||||
match ask_user_action_for_package()? {
|
||||
ReviewIntention::AsDependency => {
|
||||
reviews.push(ReviewAction::AsDependency(package));
|
||||
break;
|
||||
}
|
||||
ReviewIntention::AssignGroup => {
|
||||
if let Ok(Some(group)) = ask_group(groups) {
|
||||
reviews.push(ReviewAction::AssignGroup(package, group));
|
||||
break;
|
||||
};
|
||||
}
|
||||
ReviewIntention::Delete => {
|
||||
reviews.push(ReviewAction::Delete(package));
|
||||
break;
|
||||
}
|
||||
ReviewIntention::Info => {
|
||||
backend.show_package_info(&package)?;
|
||||
}
|
||||
ReviewIntention::Invalid => (),
|
||||
ReviewIntention::Skip => break,
|
||||
ReviewIntention::Quit => return Ok(ContinueWithReview::No),
|
||||
}
|
||||
}
|
||||
Ok(ContinueWithReview::Yes)
|
||||
}
|
||||
|
||||
fn ask_user_action_for_package() -> Result<ReviewIntention> {
|
||||
print!("assign to (g)roup, (d)elete, (s)kip, (i)nfo, (a)s dependency, (q)uit? ");
|
||||
stdout().lock().flush()?;
|
||||
match read_single_char_from_terminal()? {
|
||||
'a' => Ok(ReviewIntention::AsDependency),
|
||||
'd' => Ok(ReviewIntention::Delete),
|
||||
'g' => Ok(ReviewIntention::AssignGroup),
|
||||
'i' => Ok(ReviewIntention::Info),
|
||||
'q' => Ok(ReviewIntention::Quit),
|
||||
's' => Ok(ReviewIntention::Skip),
|
||||
_ => Ok(ReviewIntention::Invalid),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_enumerated_groups(groups: &[Rc<Group>]) {
|
||||
for (i, group) in groups.iter().enumerate() {
|
||||
println!("{i}: {}", group.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn ask_group(groups: &[Rc<Group>]) -> Result<Option<Rc<Group>>> {
|
||||
print_enumerated_groups(groups);
|
||||
let mut buf = String::new();
|
||||
stdin().read_line(&mut buf)?;
|
||||
let reply = buf.trim();
|
||||
|
||||
let idx: usize = if let Ok(idx) = reply.parse() {
|
||||
idx
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if idx < groups.len() {
|
||||
Ok(Some(groups[idx].clone()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::{Group, Package};
|
||||
|
||||
use super::datastructures::{ReviewAction, ReviewsPerBackend};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Strategy {
|
||||
backend: Box<dyn Backend>,
|
||||
delete: Vec<Package>,
|
||||
as_dependency: Vec<Package>,
|
||||
assign_group: Vec<(Package, Rc<Group>)>,
|
||||
}
|
||||
|
||||
impl Strategy {
|
||||
pub(super) fn new(
|
||||
backend: Box<dyn Backend>,
|
||||
delete: Vec<Package>,
|
||||
as_dependency: Vec<Package>,
|
||||
assign_group: Vec<(Package, Rc<Group>)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
backend,
|
||||
delete,
|
||||
as_dependency,
|
||||
assign_group,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn execute(self) -> Result<()> {
|
||||
if !self.delete.is_empty() {
|
||||
self.backend.remove_packages(&self.delete)?;
|
||||
}
|
||||
|
||||
if !self.as_dependency.is_empty() {
|
||||
self.backend.make_dependency(&self.as_dependency)?;
|
||||
}
|
||||
|
||||
if !self.assign_group.is_empty() {
|
||||
self.backend.assign_group(self.assign_group);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn show(&self) {
|
||||
if self.nothing_to_do() {
|
||||
return;
|
||||
}
|
||||
|
||||
println!("[{}]", self.backend.get_section());
|
||||
|
||||
if !self.delete.is_empty() {
|
||||
println!("delete:");
|
||||
for p in &self.delete {
|
||||
println!(" {p}");
|
||||
}
|
||||
}
|
||||
|
||||
if !self.as_dependency.is_empty() {
|
||||
println!("as depdendency:");
|
||||
for p in &self.as_dependency {
|
||||
println!(" {p}");
|
||||
}
|
||||
}
|
||||
|
||||
if !self.assign_group.is_empty() {
|
||||
println!("assign groups:");
|
||||
for (p, g) in &self.assign_group {
|
||||
println!(" {p} -> {}", g.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn nothing_to_do(&self) -> bool {
|
||||
self.delete.is_empty() && self.as_dependency.is_empty() && self.assign_group.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReviewsPerBackend> for Vec<Strategy> {
|
||||
fn from(reviews: ReviewsPerBackend) -> Self {
|
||||
let mut result = vec![];
|
||||
|
||||
for (backend, actions) in reviews {
|
||||
let mut to_delete = vec![];
|
||||
let mut assign_group = vec![];
|
||||
let mut as_dependency = vec![];
|
||||
|
||||
extract_actions(
|
||||
actions,
|
||||
&mut to_delete,
|
||||
&mut assign_group,
|
||||
&mut as_dependency,
|
||||
);
|
||||
|
||||
result.push(Strategy::new(
|
||||
backend,
|
||||
to_delete,
|
||||
as_dependency,
|
||||
assign_group,
|
||||
));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_actions(
|
||||
actions: Vec<ReviewAction>,
|
||||
to_delete: &mut Vec<Package>,
|
||||
assign_group: &mut Vec<(Package, Rc<Group>)>,
|
||||
as_dependency: &mut Vec<Package>,
|
||||
) {
|
||||
for action in actions {
|
||||
match action {
|
||||
ReviewAction::Delete(package) => to_delete.push(package),
|
||||
ReviewAction::AssignGroup(package, group) => assign_group.push((package, group)),
|
||||
ReviewAction::AsDependency(package) => as_dependency.push(package),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use std::collections::HashSet;
|
||||
use std::iter::Peekable;
|
||||
use std::vec::IntoIter;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::ArgMatches;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::grouping::{Group, Package, Section};
|
||||
|
||||
pub const NO_PACKAGES_FOUND: &str = "no packages matching query";
|
||||
|
||||
pub(crate) fn search_packages(args: &ArgMatches, groups: &HashSet<Group>) -> Result<()> {
|
||||
let search_string = args
|
||||
.get_one::<String>("string")
|
||||
.context("getting search string from arg")?;
|
||||
|
||||
let re = Regex::new(search_string)?;
|
||||
|
||||
let mut vec = vec![];
|
||||
|
||||
for group in groups {
|
||||
for section in &group.sections {
|
||||
for package in §ion.packages {
|
||||
if re.is_match(&package.name) {
|
||||
vec.push((group, section, package));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if vec.is_empty() {
|
||||
bail!(NO_PACKAGES_FOUND)
|
||||
}
|
||||
|
||||
print_triples(vec);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_triples(mut vec: Vec<(&Group, &Section, &Package)>) {
|
||||
vec.sort_unstable();
|
||||
|
||||
let mut g0 = String::new();
|
||||
let mut s0 = String::new();
|
||||
|
||||
let mut iter = vec.into_iter().peekable();
|
||||
|
||||
while let Some((g, s, p)) = iter.next() {
|
||||
print_group_if_changed(g, &g0, &mut s0);
|
||||
print_section_if_changed(s, &s0);
|
||||
println!("{p}");
|
||||
save_group_and_section_name(&mut g0, g, &mut s0, s);
|
||||
print_separator_unless_exhausted(&mut iter, &g0);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_group_and_section_name(g0: &mut String, g: &Group, s0: &mut String, s: &Section) {
|
||||
*g0 = g.name.clone();
|
||||
*s0 = s.name.clone();
|
||||
}
|
||||
|
||||
fn print_separator_unless_exhausted(
|
||||
iter: &mut Peekable<IntoIter<(&Group, &Section, &Package)>>,
|
||||
g0: &String,
|
||||
) {
|
||||
if let Some((g, _, _)) = iter.peek() {
|
||||
if g.name != *g0 {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_section_if_changed(s: &Section, s0: &String) {
|
||||
if s.name != *s0 {
|
||||
println!("[{}]", s.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_group_if_changed(g: &Group, g0: &String, s0: &mut String) {
|
||||
if g.name != *g0 {
|
||||
println!("{}", g.name);
|
||||
for _ in 0..g.name.len() {
|
||||
print!("-");
|
||||
}
|
||||
println!();
|
||||
s0.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use termios::*;
|
||||
|
||||
pub(crate) fn get_user_confirmation() -> Result<bool> {
|
||||
print!("Continue? [Y/n] ");
|
||||
std::io::stdout().flush().unwrap();
|
||||
|
||||
let mut reply = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut reply)
|
||||
.context("reading stdin")?;
|
||||
|
||||
Ok(reply.trim().is_empty() || reply.to_lowercase().contains('y'))
|
||||
}
|
||||
|
||||
pub(crate) fn read_single_char_from_terminal() -> Result<char> {
|
||||
// 0 is the file descriptor for stdin
|
||||
let fd = 0;
|
||||
let termios = Termios::from_fd(fd).context("getting stdin fd")?;
|
||||
let mut new_termios = termios;
|
||||
new_termios.c_lflag &= !(ICANON | ECHO);
|
||||
new_termios.c_cc[VMIN] = 1;
|
||||
new_termios.c_cc[VTIME] = 0;
|
||||
tcsetattr(fd, TCSANOW, &new_termios).context("setting terminal mode")?;
|
||||
|
||||
let mut input = [0u8; 1];
|
||||
io::stdin()
|
||||
.read_exact(&mut input[..])
|
||||
.context("reading one byte from stdin")?;
|
||||
let result = input[0] as char;
|
||||
// stdin is not echoed automatically in this terminal mode
|
||||
println!("{result}");
|
||||
|
||||
// restore previous settings
|
||||
tcsetattr(fd, TCSANOW, &termios).context("restoring terminal mode")?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
Reference in New Issue
Block a user