declarative cli and core: refactor

This commit is contained in:
ripytide
2024-04-22 16:59:23 +01:00
parent 724f084acb
commit a5880f614e
30 changed files with 634 additions and 969 deletions
+2 -6
View File
@@ -12,7 +12,7 @@ categories.workspace = true
[dependencies]
anyhow = "1.0"
clap = "4.5"
clap = { version = "4.5", features = ["derive"] }
const_format = { version = "0.2", default-features = false }
path-absolutize = "3.1"
regex = { version = "1.10", default-features = false, features = ["std"] }
@@ -21,8 +21,7 @@ walkdir = "2.5"
libc = "0.2"
enum_dispatch = "0.3"
serde = "1.0"
serde_derive = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
@@ -30,9 +29,6 @@ serde_yaml = "0.9"
alpm = { version = "3.0", optional = true }
rust-apt = { version = "0.7", optional = true }
[dev-dependencies]
rstest = "0.19"
[features]
default = []
arch = ["dep:alpm"]
-159
View File
@@ -1,159 +0,0 @@
use crate::core::get_version_string;
use clap::{Arg, ArgAction, Command};
#[must_use]
pub(super) fn build_cli() -> Command {
let package_cmd = get_package_cmd();
let group_cmd = get_group_cmd();
let version_cmd = Command::new("version").about("show version info");
Command::new("pacdef")
.about("multi-backend declarative package manager for Linux")
.version(get_version_string())
.arg_required_else_help(true)
.subcommand_required(true)
.subcommands([group_cmd, package_cmd, version_cmd])
.subcommand_value_name("subcommand")
.disable_help_subcommand(true)
.disable_version_flag(true)
}
/// Build the `pacdef group` subcommand.
fn get_group_cmd() -> Command {
let edit = Command::new("edit")
.about("edit one or more existing group")
.arg_required_else_help(true)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("a previously imported group"),
)
.visible_alias("ed");
let export = Command::new("export")
.about("export one or more group files")
.arg_required_else_help(true)
.arg(
Arg::new("force")
.short('f')
.long("force")
.action(clap::ArgAction::SetTrue)
.help("overwrite output files if they exist"),
)
.arg(
Arg::new("output_dir")
.short('o')
.long("output")
.help("(optional) the directory under which to save the group"),
)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("the file to export as group"),
)
.visible_alias("ex");
let import = Command::new("import")
.about("import one or more group files")
.arg_required_else_help(true)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("the file to import as group"),
)
.visible_alias("i");
let list = Command::new("list")
.about("list names of imported groups")
.visible_alias("l");
let new = 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..).required(true))
.visible_alias("n");
let remove = Command::new("remove")
.about("remove one or more previously imported groups")
.arg_required_else_help(true)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("a previously imported group that will be removed"),
)
.visible_alias("r");
let show = Command::new("show")
.about("show packages under an imported group")
.arg_required_else_help(true)
.arg(
Arg::new("groups")
.num_args(1..)
.required(true)
.help("group file(s) to show"),
)
.visible_alias("s");
Command::new("group")
.arg_required_else_help(true)
.about("manage groups")
.visible_alias("g")
.subcommand_required(true)
.subcommands([edit, export, import, list, new, remove, show])
}
/// Build the `pacdef package` subcommand.
fn get_package_cmd() -> Command {
let sync = Command::new("sync")
.about("install packages from all imported groups")
.visible_alias("sy")
.arg(build_noconfirm_arg());
let clean = Command::new("clean")
.about("remove unmanaged packages")
.visible_alias("c")
.arg(build_noconfirm_arg());
let unmanaged = Command::new("unmanaged")
.about("show explicitly installed packages not managed by pacdef")
.visible_alias("u");
let review = Command::new("review")
.about("review unmanaged packages")
.visible_alias("r");
let search = Command::new("search")
.visible_alias("se")
.about("search for packages which match a provided regex")
.arg_required_else_help(true)
.arg(
Arg::new("regex")
.required(true)
.help("the regular expression the package must match"),
);
Command::new("package")
.arg_required_else_help(true)
.about("manage packages")
.visible_alias("p")
.subcommand_required(true)
.subcommands([clean, review, search, sync, unmanaged])
}
fn build_noconfirm_arg() -> Arg {
Arg::new("noconfirm")
.long("noconfirm")
.help("do not ask for any confirmation")
.action(ArgAction::SetTrue)
}
-47
View File
@@ -1,47 +0,0 @@
#[derive(Debug, PartialEq)]
pub enum Arguments {
Group(GroupAction),
Package(PackageAction),
Version,
}
#[derive(Debug, PartialEq)]
pub enum GroupAction {
Edit(Groups),
Export(Groups, OutputDir, Force),
Import(Groups),
List,
New(Groups, Edit),
Remove(Groups),
Show(Groups),
}
#[derive(Debug, PartialEq)]
pub struct Files(pub Vec<String>);
#[derive(Debug, PartialEq)]
pub struct Groups(pub Vec<String>);
#[derive(Debug, PartialEq)]
pub enum PackageAction {
Clean(Noconfirm),
Review,
Search(Regex),
Sync(Noconfirm),
Unmanaged,
}
#[derive(Debug, PartialEq)]
pub struct Regex(pub String);
#[derive(Debug, PartialEq)]
pub struct Edit(pub bool);
#[derive(Debug, PartialEq)]
pub struct Noconfirm(pub bool);
#[derive(Debug, PartialEq)]
pub struct Force(pub bool);
#[derive(Debug, PartialEq)]
pub struct OutputDir(pub Option<String>);
-14
View File
@@ -1,14 +0,0 @@
use self::datastructure::Arguments;
mod cli;
pub mod datastructure;
mod parsing;
#[cfg(test)]
mod tests;
/// Get and parse the CLI arguments.
#[must_use]
pub fn get() -> Arguments {
let args = cli::build_cli().get_matches();
parsing::parse(args)
}
-79
View File
@@ -1,79 +0,0 @@
use super::datastructure::{
Arguments, Edit, Force, GroupAction, Groups, Noconfirm, OutputDir, PackageAction, Regex,
};
const ARGS_CONSISTENT: &str = "argument declaration and parsing must be consistent";
pub(super) fn parse(args: clap::ArgMatches) -> Arguments {
match args.subcommand() {
Some(("group", args)) => Arguments::Group(parse_group_args(args)),
Some(("package", args)) => Arguments::Package(parse_package_args(args)),
Some(("version", _)) => Arguments::Version,
Some(value) => panic!("main subcommand was not matched: {value:?}"),
None => unreachable!("prevented by clap"),
}
}
fn parse_group_args(args: &clap::ArgMatches) -> GroupAction {
match args.subcommand() {
Some(("edit", args)) => GroupAction::Edit(get_groups(args)),
Some(("export", args)) => {
GroupAction::Export(get_groups(args), get_output_dir(args), get_force(args))
}
Some(("import", args)) => GroupAction::Import(get_groups(args)),
Some(("list", _)) => GroupAction::List,
Some(("new", args)) => GroupAction::New(get_groups(args), get_edit(args)),
Some(("remove", args)) => GroupAction::Remove(get_groups(args)),
Some(("show", args)) => GroupAction::Show(get_groups(args)),
Some(value) => panic!("group subcommand was not matched: {value:?}"),
None => unreachable!("prevented by clap"),
}
}
fn parse_package_args(args: &clap::ArgMatches) -> PackageAction {
match args.subcommand() {
Some(("clean", args)) => PackageAction::Clean(get_noconfirm(args)),
Some(("review", _)) => PackageAction::Review,
Some(("search", args)) => PackageAction::Search(get_regex(args)),
Some(("sync", args)) => PackageAction::Sync(get_noconfirm(args)),
Some(("unmanaged", _)) => PackageAction::Unmanaged,
Some(value) => panic!("package subcommand was not matched: {value:?}"),
None => unreachable!("prevented by clap"),
}
}
fn get_one_arg<T>(args: &clap::ArgMatches, id: &str) -> T
where
T: std::any::Any + Clone + Sync + Send + 'static,
{
args.get_one::<T>(id).expect(ARGS_CONSISTENT).to_owned()
}
fn get_regex(args: &clap::ArgMatches) -> Regex {
Regex(get_one_arg(args, "regex"))
}
fn get_noconfirm(args: &clap::ArgMatches) -> Noconfirm {
Noconfirm(get_one_arg(args, "noconfirm"))
}
fn get_edit(args: &clap::ArgMatches) -> Edit {
Edit(get_one_arg(args, "edit"))
}
fn get_groups(args: &clap::ArgMatches) -> Groups {
Groups(
args.get_many::<String>("groups")
.expect(ARGS_CONSISTENT)
.cloned()
.collect(),
)
}
fn get_output_dir(args: &clap::ArgMatches) -> OutputDir {
OutputDir(args.get_one::<String>("output_dir").cloned())
}
fn get_force(args: &clap::ArgMatches) -> Force {
Force(get_one_arg(args, "force"))
}
-38
View File
@@ -1,38 +0,0 @@
use super::cli::build_cli;
use super::datastructure::*;
use super::parsing::parse;
use rstest::rstest;
#[rstest]
#[case(vec!["pacdef", "group", "list"],
Arguments::Group(GroupAction::List))]
#[case(vec!["pacdef", "g", "l"],
Arguments::Group(GroupAction::List))]
#[case(vec!["pacdef", "version"],
Arguments::Version)]
#[case(vec!["pacdef", "group", "export", "-o", "/tmp", "--force", "base"],
Arguments::Group(GroupAction::Export(Groups(vec!["base".to_string()]), OutputDir(Some("/tmp".to_string())), Force(true))))]
#[case(vec!["pacdef", "package", "sync"],
Arguments::Package(PackageAction::Sync(Noconfirm(false))))]
#[case(vec!["pacdef", "package", "sync", "--noconfirm"],
Arguments::Package(PackageAction::Sync(Noconfirm(true))))]
#[case(vec!["pacdef", "p", "se", "myregex"],
Arguments::Package(PackageAction::Search(Regex("myregex".to_string()))))]
fn arg_parsing(#[case] input: Vec<&str>, #[case] expected: Arguments) {
let args = build_cli().get_matches_from(input);
let parsed = parse(args);
assert_eq!(parsed, expected);
}
#[rstest]
#[should_panic]
#[case(vec!["pacdef", "package", "search"], "regex missing")]
#[should_panic]
#[case(vec!["pacdef", "p", "s"], "can be either 'sync' or 'search'")]
#[should_panic]
#[case(vec!["pacdef", "group", "edit"], "group missing")]
fn arg_parsing_invalid(#[case] input: Vec<&str>, #[case] err_msg: &str) {
build_cli().try_get_matches_from(input).expect(err_msg);
}
+1 -1
View File
@@ -8,7 +8,7 @@ use anyhow::{Context, Result};
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Arch {
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::backend::root::build_base_command_with_privileges;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Debian {
+1 -1
View File
@@ -6,7 +6,7 @@ use anyhow::Result;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Fedora {
+1 -1
View File
@@ -6,7 +6,7 @@ use anyhow::Result;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Flatpak {
+1 -1
View File
@@ -7,7 +7,7 @@ use serde_json::Value;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::{Group, Package};
use crate::Package;
macro_rules! ERROR{
($bin:expr) => {
+1 -1
View File
@@ -8,7 +8,7 @@ use serde_json::Value;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Rust {
@@ -4,7 +4,7 @@ mod types;
use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
use anyhow::{bail, Context, Result};
use std::collections::HashSet;
use std::process::Command;
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::backend::backend_trait::{Backend, Switches, Text};
use crate::backend::macros::impl_backend_constants;
use crate::backend::root::build_base_command_with_privileges;
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::Package;
#[derive(Debug, Clone)]
pub struct Void {
+3 -4
View File
@@ -2,12 +2,11 @@ use std::cmp::{Eq, Ord};
use std::collections::{HashMap, HashSet};
use std::hash::Hash;
use std::process::Command;
use std::rc::Rc;
use anyhow::{Context, Result};
use crate::cmd::run_external_command;
use crate::{Group, Package};
use crate::{Group, Groups, Package};
pub type Switches = &'static [&'static str];
pub type Text = &'static str;
@@ -51,7 +50,7 @@ pub trait Backend {
/// Load all packages from a set of groups. The backend will visit all groups,
/// find its own section, and clone all packages into its own struct.
fn load(&mut self, groups: &HashSet<Group>);
fn load(&mut self, groups: &Groups);
/// Get all managed packages for this backend, i.e. all packages
/// under the corresponding section in all group files.
@@ -75,7 +74,7 @@ pub trait Backend {
/// Assign each of the packages to an individual group by editing the
/// group files.
fn assign_group(&self, to_assign: Vec<(Package, Rc<Group>)>) -> Result<()> {
fn assign_group(&self, to_assign: Vec<(Package, Group)>) -> Result<()> {
let group_package_map = to_hashmap(to_assign);
let section_header = format!("[{}]", self.get_section());
+1 -1
View File
@@ -34,7 +34,7 @@ macro_rules! impl_backend_constants {
&self.packages
}
fn load(&mut self, groups: &HashSet<Group>) {
fn load(&mut self, groups: &crate::Groups) {
let own_section_name = self.get_section();
groups
+1 -1
View File
@@ -7,11 +7,11 @@ pub mod todo_per_backend;
use crate::backend::backend_trait::Switches;
use crate::backend::backend_trait::Text;
use crate::Group;
use crate::Groups;
use crate::Package;
use anyhow::Result;
use backend_trait::Backend;
use std::collections::HashSet;
use std::rc::Rc;
use self::actual::{
fedora::Fedora, flatpak::Flatpak, python::Python, rust::Rust, rustup::Rustup, void::Void,
+182
View File
@@ -0,0 +1,182 @@
//! The clap declarative command line interface
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
#[derive(Parser)]
#[command(
version,
author,
arg_required_else_help(true),
subcommand_required(true),
disable_help_subcommand(true),
disable_version_flag(true)
)]
/// multi-backend declarative package manager for Linux
pub struct MainArguments {
#[command(subcommand)]
pub subcommand: MainSubcommand,
}
#[derive(Subcommand)]
pub enum MainSubcommand {
Group(GroupArguments),
Package(PackageArguments),
Version(VersionArguments),
}
#[derive(Args)]
#[command(
arg_required_else_help(true),
visible_alias("g"),
subcommand_required(true)
)]
/// manage groups
pub struct GroupArguments {
#[command(subcommand)]
pub group_action: GroupAction,
}
#[derive(Subcommand)]
pub enum GroupAction {
Edit(EditGroupAction),
Export(ExportGroupAction),
Import(ImportGroupAction),
List(ListGroupAction),
New(NewGroupAction),
Remove(RemoveGroupAction),
Show(ShowGroupAction),
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("ed"))]
/// edit one or more existing group
pub struct EditGroupAction {
#[arg(required(true), num_args(1..))]
/// a previously imported group
pub edit_groups: Vec<String>,
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("ex"))]
/// export one or more group files
pub struct ExportGroupAction {
#[arg(required(true), num_args(1..))]
/// the file to export as group
pub export_groups: Vec<String>,
#[arg(short, long)]
/// (optional) the directory under which to save the group
pub output_dir: Option<PathBuf>,
#[arg(short, long)]
/// overwrite output files if they exist
pub force: bool,
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("i"))]
/// import one or more group files
pub struct ImportGroupAction {
#[arg(required(true), num_args(1..))]
/// the file to import as group
pub import_groups: Vec<String>,
}
#[derive(Args)]
#[command(visible_alias("l"))]
/// list names of imported groups
pub struct ListGroupAction {}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("n"))]
/// create new group files
pub struct NewGroupAction {
#[arg(required(true), num_args(1..))]
/// the groups to create
pub new_groups: Vec<String>,
#[arg(short, long)]
/// edit the new group files after creation
pub edit: bool,
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("r"))]
/// remove one or more previously imported groups
pub struct RemoveGroupAction {
#[arg(required(true), num_args(1..))]
/// a previously imported group that will be removed
pub remove_groups: Vec<String>,
}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("s"))]
/// show packages under an imported group
pub struct ShowGroupAction {
#[arg(required(true), num_args(1..))]
/// group file(s) to show
pub show_groups: Vec<String>,
}
#[derive(Args)]
#[command(
arg_required_else_help(true),
subcommand_required(true),
visible_alias("p")
)]
/// manage packages
pub struct PackageArguments {
#[command(subcommand)]
pub package_action: PackageAction,
}
#[derive(Subcommand)]
pub enum PackageAction {
Clean(CleanPackageAction),
Review(ReviewPackageAction),
Search(SearchPackageAction),
Sync(SyncPackageAction),
Unmanaged(UnmanagedPackageAction),
}
#[derive(Args)]
#[command(visible_alias("c"))]
/// remove unmanaged packages
pub struct CleanPackageAction {
#[arg(long)]
/// do not ask for any confirmation
pub no_confirm: bool,
}
#[derive(Args)]
#[command(visible_alias("r"))]
/// review unmanaged packages
pub struct ReviewPackageAction {}
#[derive(Args)]
#[command(arg_required_else_help(true), visible_alias("se"))]
/// search for packages which match a provided regex
pub struct SearchPackageAction {
#[arg(required(true))]
/// the regular expression the package must match
pub regex: String,
}
#[derive(Args)]
#[command(visible_alias("sy"))]
/// install packages from all imported groups
pub struct SyncPackageAction {
#[arg(long)]
/// do not ask for any confirmation
pub no_confirm: bool,
}
#[derive(Args)]
#[command(visible_alias("u"))]
/// show explicitly installed packages not managed by pacdef
pub struct UnmanagedPackageAction {}
#[derive(Args)]
pub struct VersionArguments {}
+1 -1
View File
@@ -3,7 +3,7 @@ use std::io::{ErrorKind, Write};
use std::path::Path;
use anyhow::{bail, Context, Result};
use serde_derive::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
// Update the master README if fields change.
/// Config for the program, as listed in `$XDG_CONFIG_HOME/pacdef/pacdef.yaml`.
+357 -373
View File
@@ -1,49 +1,32 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::env::current_dir;
use std::fs::{copy, create_dir_all, remove_file, rename, File};
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{bail, ensure, Context, Result};
use const_format::formatcp;
use crate::args::datastructure::{
Arguments, Edit, Force, GroupAction, Groups, Noconfirm, OutputDir, PackageAction, Regex,
};
use crate::backend::backend_trait::Backend;
use crate::backend::todo_per_backend::ToDoPerBackend;
use crate::backend::AnyBackend;
use crate::cmd::run_edit_command;
use crate::env::should_print_debug_info;
use crate::cli::{
CleanPackageAction, EditGroupAction, ExportGroupAction, GroupAction, GroupArguments,
ImportGroupAction, ListGroupAction, MainArguments, MainSubcommand, NewGroupAction,
PackageAction, PackageArguments, RemoveGroupAction, ReviewPackageAction, SearchPackageAction,
ShowGroupAction, SyncPackageAction, UnmanagedPackageAction, VersionArguments,
};
use crate::cmd::{run_edit_command, run_external_command};
use crate::env::{get_editor, should_print_debug_info};
use crate::path::{binary_in_path, get_absolutized_file_paths, get_group_dir};
use crate::search;
use crate::review::review;
use crate::search::search_packages;
use crate::ui::get_user_confirmation;
use crate::Config;
use crate::Group;
use crate::{review, Error};
/// Most data that is required during runtime of the program.
pub struct Pacdef {
/// The command line arguments. Is an `Option` so that we can take ownership later without cloning.
args: Option<Arguments>,
/// The config of the program.
config: Config,
/// The hashset of all groups.
groups: HashSet<Group>,
}
impl Pacdef {
/// Creates a new [`Pacdef`]. `config` should be passed from [`Config::load`], and `args` from
/// [`args::get`].
#[must_use]
pub const fn new(args: Arguments, config: Config, groups: HashSet<Group>) -> Self {
Self {
args: Some(args),
config,
groups,
}
}
use crate::{Config, Error, Groups};
impl MainArguments {
/// Run the action that was provided by the user as first argument.
///
/// For convenience sake, all called functions take a `&self` argument, even if
@@ -52,305 +35,132 @@ impl Pacdef {
/// # Errors
///
/// This function propagates errors from the underlying functions.
///
/// # Panics
///
/// This function panics if the `args` field is `None`.
pub fn run_action_from_arg(mut self) -> Result<()> {
match self
.args
.take()
.expect("if there were no args we would not get to here")
{
Arguments::Group(group_args) => self.run_group_subcommand(&group_args),
Arguments::Package(package_args) => self.run_package_subcommand(&package_args),
Arguments::Version => {
self.show_version();
Ok(())
}
pub fn run(self, groups: &Groups, config: &Config) -> Result<()> {
match self.subcommand {
MainSubcommand::Group(group) => group.run(groups),
MainSubcommand::Package(package) => package.run(groups, config),
MainSubcommand::Version(version) => version.run(),
}
}
}
fn run_group_subcommand(self, args: &GroupAction) -> Result<()> {
match args {
GroupAction::Edit(Groups(groups)) => self.edit_groups(groups),
GroupAction::Export(Groups(groups), OutputDir(dir), Force(force)) => {
self.export_groups(groups, dir.as_ref(), *force)
}
GroupAction::Import(Groups(groups)) => self.import_groups(groups),
GroupAction::List => self.show_groups(),
GroupAction::New(Groups(groups), Edit(edit)) => self.new_groups(groups, *edit),
GroupAction::Remove(Groups(groups)) => self.remove_groups(groups),
GroupAction::Show(Groups(groups)) => self.show_group_content(groups),
impl VersionArguments {
/// If the crate was compiled from git, return `pacdef, <version> (<hash>)`.
/// Otherwise return `pacdef, <version>`.
fn run(self) -> Result<()> {
let backends = get_included_backends();
let mut result = format!("pacdef, version: {}\n", get_version_string());
result.push_str("supported backends:");
for b in backends {
result.push_str("\n ");
result.push_str(b);
}
println!("{}", result);
Ok(())
}
}
impl GroupArguments {
fn run(self, groups: &Groups) -> Result<()> {
match self.group_action {
GroupAction::Edit(edit) => edit.run(groups),
GroupAction::Export(export) => export.run(groups),
GroupAction::Import(import) => import.run(),
GroupAction::List(list) => list.run(groups),
GroupAction::New(new) => new.run(),
GroupAction::Remove(remove) => remove.run(groups),
GroupAction::Show(show) => show.run(groups),
}
}
}
fn run_package_subcommand(mut self, args: &PackageAction) -> Result<()> {
match args {
PackageAction::Clean(Noconfirm(noconfirm)) => self.clean_packages(*noconfirm),
PackageAction::Review => review::review(self.get_unmanaged_packages()?, self.groups),
PackageAction::Search(Regex(regex)) => {
self.warn_about_groups_that_arent_symlinks();
search::search_packages(regex, &self.groups)
}
PackageAction::Sync(Noconfirm(noconfirm)) => self.install_packages(*noconfirm),
PackageAction::Unmanaged => self.show_unmanaged_packages(),
}
}
fn get_missing_packages(&mut self) -> Result<ToDoPerBackend> {
let mut to_install = ToDoPerBackend::new();
if self.groups.is_empty() {
impl EditGroupAction {
fn run(self, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
for mut backend in AnyBackend::iter() {
if self
.config
.disabled_backends
.contains(&backend.get_section().to_string())
{
continue;
}
if !binary_in_path(backend.get_binary())? {
continue;
}
self.overwrite_values_from_config(&mut backend);
backend.load(&self.groups);
match backend.get_missing_packages_sorted() {
Ok(diff) => to_install.push((backend, diff)),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(to_install)
}
fn overwrite_values_from_config(&mut self, backend: &mut AnyBackend) {
#[cfg(feature = "arch")]
{
if let AnyBackend::Arch(arch) = backend {
arch.binary.clone_from(&self.config.aur_helper);
arch.aur_rm_args.clone_from(&self.config.aur_rm_args);
}
}
if let AnyBackend::Flatpak(flatpak) = backend {
flatpak.systemwide = self.config.flatpak_systemwide;
}
if let AnyBackend::Python(python) = backend {
python.binary.clone_from(&self.config.pip_binary);
}
}
fn install_packages(&mut self, noconfirm: bool) -> Result<()> {
self.warn_about_groups_that_arent_symlinks();
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:\n");
to_install.show().context("printing things to do")?;
println!();
if noconfirm {
println!("proceeding without confirmation");
} else if !get_user_confirmation()? {
return Ok(());
}
to_install.install_missing_packages(noconfirm)
}
fn warn_about_groups_that_arent_symlinks(&self) {
for group in &self.groups {
if group.warn_symlink {
eprintln!(
"WARNING: group file {} is not a symlink",
group.path.to_string_lossy()
);
}
}
}
fn edit_groups(&self, groups: &[String]) -> Result<()> {
self.warn_about_groups_that_arent_symlinks();
if self.groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let group_files: Vec<_> = find_groups_by_name(groups, &self.groups)
let group_files: Vec<_> = find_groups_by_name(&self.edit_groups, groups)
.context("getting group files for args")?
.into_iter()
.map(|g| g.path.as_path())
.collect();
run_edit_command(&group_files).context("running editor")?;
let mut cmd = Command::new(get_editor().context("getting suitable editor")?);
cmd.current_dir(
group_files[0]
.parent()
.context("getting parent dir of first file argument")?,
);
for group_file in group_files {
cmd.arg(group_file.to_string_lossy().to_string());
}
run_external_command(cmd)?;
Ok(())
}
}
fn show_version(self) {
println!("{}", get_name_and_version());
}
fn show_unmanaged_packages(mut self) -> Result<()> {
let unmanaged_per_backend = &self.get_unmanaged_packages()?;
if unmanaged_per_backend.nothing_to_do_for_all_backends() {
return Ok(());
}
unmanaged_per_backend
.show()
.context("printing things to do")
}
/// Get a list of unmanaged packages per backend.
impl ExportGroupAction {
/// Export pacdef groups by moving a group file to an output dir. The path of the
/// group file relative to the group base dir will be replicated under the output
/// directory.
///
/// This method loops through all enabled `Backend`s whose binary is in `PATH`.
/// By default, the output dir is the current working directory. `output_dir` may be
/// specified to the path of another directory, in which case `output_dir` must
/// exist.
///
/// If `force` is `true`, the output file will be overwritten if it exists.
///
/// # Errors
///
/// This function will propagate errors from the individual backends.
fn get_unmanaged_packages(&mut self) -> Result<ToDoPerBackend> {
self.warn_about_groups_that_arent_symlinks();
if self.groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut result = ToDoPerBackend::new();
for mut backend in AnyBackend::iter() {
if self
.config
.disabled_backends
.contains(&backend.get_section().to_string())
{
continue;
}
if !binary_in_path(backend.get_binary())? {
continue;
}
self.overwrite_values_from_config(&mut backend);
backend.load(&self.groups);
match backend.get_unmanaged_packages_sorted() {
Ok(unmanaged) => result.push((backend, unmanaged)),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(result)
}
/// Print the alphabetically sorted names of all groups to stdout.
/// This function will return an error if
/// - the group file is a symlink (in which case exporting makes no sense),
/// - the output file exists and `force` is not `true`, or
/// - the user does not have permission to write to the output dir.
///
/// This methods cannot return an error. It returns a `Result` to be consistent
/// with other methods.
fn show_groups(self) -> Result<()> {
self.warn_about_groups_that_arent_symlinks();
/// # Limitations
///
/// At the moment we cannot export nested group dirs. The user would have to
/// export every group file individually, or use a shell glob.
fn run(self, groups: &Groups) -> Result<()> {
let groups = find_groups_by_name(&self.export_groups, groups)?;
let output_dir = match self.output_dir {
Some(p) => p,
None => current_dir().context("no output dir specified, getting current directory")?,
};
if self.groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut vec: Vec<_> = self.groups.iter().collect();
vec.sort_unstable();
for g in vec {
println!("{}", g.name);
}
Ok(())
}
fn clean_packages(&mut self, noconfirm: bool) -> 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:\n");
to_remove.show().context("printing things to do")?;
println!();
if noconfirm {
println!("proceeding without confirmation");
} else if !get_user_confirmation()? {
return Ok(());
}
to_remove.remove_unmanaged_packages(noconfirm)
}
fn show_group_content(&self, args: &[String]) -> Result<()> {
self.warn_about_groups_that_arent_symlinks();
if self.groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut errors = vec![];
let mut groups = vec![];
// make sure all args exist before doing anything
for arg_group in args {
let possible_group = self.groups.iter().find(|g| g.name == **arg_group);
let Some(group) = possible_group else {
errors.push((*arg_group).clone());
continue;
};
groups.push(group);
}
// return an error if any arg was not found
ensure!(
errors.is_empty(),
crate::Error::MultipleGroupsNotFound(errors)
output_dir.exists() && output_dir.is_dir(),
"output must be a directory and exist"
);
let show_more_than_one_group = args.len() > 1;
for group in &groups {
ensure!(!&group.path.is_symlink(), "cannot export symlinks");
let mut iter = groups.into_iter().peekable();
let mut exported_path = output_dir.clone();
exported_path.push(PathBuf::from(&group.name));
while let Some(group) = iter.next() {
if show_more_than_one_group {
let name = &group.name;
println!("{name}");
for _ in 0..name.len() {
print!("-");
}
println!();
}
ensure!(
!self.force && !exported_path.exists(),
"{exported_path:?} already exists"
);
println!("{group}");
if iter.peek().is_some() {
println!();
}
create_parent(&exported_path)
.with_context(|| format!("creating parent dir of {exported_path:?}"))?;
move_file(&group.path, &exported_path).context("moving file")?;
symlink(&exported_path, &group.path).context("creating symlink to exported file")?;
}
Ok(())
}
}
fn import_groups(&self, file_names: &[String]) -> Result<()> {
let files = get_absolutized_file_paths(file_names)?;
impl ImportGroupAction {
fn run(self) -> Result<()> {
let files = get_absolutized_file_paths(&self.import_groups)?;
let groups_dir = get_group_dir()?;
for target in files {
@@ -377,21 +187,29 @@ impl Pacdef {
Ok(())
}
}
fn remove_groups(&self, groups: &[String]) -> Result<()> {
if self.groups.is_empty() {
impl ListGroupAction {
/// Print the alphabetically sorted names of all groups to stdout.
///
/// This methods cannot return an error. It returns a `Result` to be consistent
/// with other methods.
fn run(self, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let found = find_groups_by_name(groups, &self.groups)?;
for group in found {
remove_file(&group.path)?;
let mut vec: Vec<_> = groups.iter().collect();
vec.sort_unstable();
for g in vec {
println!("{}", g.name);
}
Ok(())
}
}
impl NewGroupAction {
/// Create empty group files.
///
/// If `edit` is `true`, the editor will be run to edit the files after they are
@@ -404,18 +222,19 @@ impl Pacdef {
/// - a group with the same name already exists,
/// - the editor cannot be run, or
/// - if we do not have permission to write to the group dir.
fn new_groups(&self, new_groups: &[String], edit: bool) -> Result<()> {
fn run(&self) -> Result<()> {
let group_path = get_group_dir()?;
// prevent group names that resolve to directories
for name in new_groups {
for new_group in &self.new_groups {
ensure!(
*name != "." && *name != "..",
crate::Error::InvalidGroupName(name.clone())
new_group != "." && new_group != "..",
crate::Error::InvalidGroupName(new_group.clone())
);
}
let paths: Vec<_> = new_groups
let paths: Vec<_> = self
.new_groups
.iter()
.map(|name| {
let mut base = group_path.clone();
@@ -435,72 +254,251 @@ impl Pacdef {
File::create(file)?;
}
if edit {
if self.edit {
run_edit_command(&paths).context("running editor")?;
}
Ok(())
}
}
/// Export pacdef groups by moving a group file to an output dir. The path of the
/// group file relative to the group base dir will be replicated under the output
/// directory.
///
/// By default, the output dir is the current working directory. `output_dir` may be
/// specified to the path of another directory, in which case `output_dir` must
/// exist.
///
/// If `force` is `true`, the output file will be overwritten if it exists.
///
/// # Errors
///
/// This function will return an error if
/// - the group file is a symlink (in which case exporting makes no sense),
/// - the output file exists and `force` is not `true`, or
/// - the user does not have permission to write to the output dir.
///
/// # Limitations
///
/// At the moment we cannot export nested group dirs. The user would have to
/// export every group file individually, or use a shell glob.
fn export_groups(
&self,
names: &[String],
output_dir: Option<&String>,
force: bool,
) -> Result<()> {
let groups = find_groups_by_name(names, &self.groups)?;
let output_dir = match output_dir.map(PathBuf::from) {
Some(p) => p,
None => current_dir().context("no output dir specified, getting current directory")?,
};
impl RemoveGroupAction {
fn run(self, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
ensure!(
output_dir.exists() && output_dir.is_dir(),
"output must be a directory and exist"
);
let found = find_groups_by_name(&self.remove_groups, groups)?;
for group in &groups {
ensure!(!&group.path.is_symlink(), "cannot export symlinks");
let mut exported_path = output_dir.clone();
exported_path.push(PathBuf::from(&group.name));
ensure!(
!force && !exported_path.exists(),
"{exported_path:?} already exists"
);
create_parent(&exported_path)
.with_context(|| format!("creating parent dir of {exported_path:?}"))?;
move_file(&group.path, &exported_path).context("moving file")?;
symlink(&exported_path, &group.path).context("creating symlink to exported file")?;
for group in found {
remove_file(&group.path)?;
}
Ok(())
}
}
impl ShowGroupAction {
fn run(self, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut errors = vec![];
let mut found_groups = vec![];
// make sure all args exist before doing anything
for show_group in &self.show_groups {
let possible_group = groups.iter().find(|group| group.name == *show_group);
let Some(group) = possible_group else {
errors.push(show_group.to_string());
continue;
};
found_groups.push(group);
}
// return an error if any arg was not found
ensure!(
errors.is_empty(),
crate::Error::MultipleGroupsNotFound(errors)
);
let show_more_than_one_group = self.show_groups.len() > 1;
let mut iter = found_groups.into_iter().peekable();
while let Some(group) = iter.next() {
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(())
}
}
impl PackageArguments {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
match self.package_action {
PackageAction::Clean(clean) => clean.run(groups, config),
PackageAction::Review(review) => review.run(groups, config),
PackageAction::Search(search) => search.run(groups),
PackageAction::Sync(sync) => sync.run(groups, config),
PackageAction::Unmanaged(unmanaged) => unmanaged.run(groups, config),
}
}
}
impl CleanPackageAction {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
let to_remove = get_unmanaged_packages(groups, config)?;
if to_remove.nothing_to_do_for_all_backends() {
println!("nothing to do");
return Ok(());
}
println!("Would remove the following packages:\n");
to_remove.show().context("printing things to do")?;
println!();
if self.no_confirm {
println!("proceeding without confirmation");
} else if !get_user_confirmation()? {
return Ok(());
}
to_remove.remove_unmanaged_packages(self.no_confirm)
}
}
impl ReviewPackageAction {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
review(get_unmanaged_packages(groups, config)?, groups)
}
}
impl SearchPackageAction {
fn run(self, groups: &Groups) -> Result<()> {
search_packages(&self.regex, groups)
}
}
impl SyncPackageAction {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
let to_install = get_missing_packages(groups, config)?;
if to_install.nothing_to_do_for_all_backends() {
println!("nothing to do");
return Ok(());
}
println!("Would install the following packages:\n");
to_install.show().context("printing things to do")?;
println!();
if self.no_confirm {
println!("proceeding without confirmation");
} else if !get_user_confirmation()? {
return Ok(());
}
to_install.install_missing_packages(self.no_confirm)
}
}
impl UnmanagedPackageAction {
fn run(self, groups: &Groups, config: &Config) -> Result<()> {
let unmanaged_per_backend = &get_unmanaged_packages(groups, config)?;
if unmanaged_per_backend.nothing_to_do_for_all_backends() {
return Ok(());
}
unmanaged_per_backend
.show()
.context("printing things to do")
}
}
fn get_missing_packages(groups: &Groups, config: &Config) -> Result<ToDoPerBackend> {
let mut to_install = ToDoPerBackend::new();
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
for mut backend in AnyBackend::iter() {
if config
.disabled_backends
.contains(&backend.get_section().to_string())
{
continue;
}
if !binary_in_path(backend.get_binary())? {
continue;
}
overwrite_values_from_config(&mut backend, config);
backend.load(groups);
match backend.get_missing_packages_sorted() {
Ok(diff) => to_install.push((backend, diff)),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(to_install)
}
fn overwrite_values_from_config(backend: &mut AnyBackend, config: &Config) {
#[cfg(feature = "arch")]
{
if let AnyBackend::Arch(arch) = backend {
arch.binary.clone_from(&config.aur_helper);
arch.aur_rm_args.clone_from(&config.aur_rm_args);
}
}
if let AnyBackend::Flatpak(flatpak) = backend {
flatpak.systemwide = config.flatpak_systemwide;
}
if let AnyBackend::Python(python) = backend {
python.binary.clone_from(&config.pip_binary);
}
}
/// Get a list of unmanaged packages per backend.
///
/// This method loops through all enabled `Backend`s whose binary is in `PATH`.
///
/// # Errors
///
/// This function will propagate errors from the individual backends.
fn get_unmanaged_packages(groups: &Groups, config: &Config) -> Result<ToDoPerBackend> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
}
let mut result = ToDoPerBackend::new();
for mut backend in AnyBackend::iter() {
if config
.disabled_backends
.contains(&backend.get_section().to_string())
{
continue;
}
if !binary_in_path(backend.get_binary())? {
continue;
}
overwrite_values_from_config(&mut backend, config);
backend.load(groups);
match backend.get_unmanaged_packages_sorted() {
Ok(unmanaged) => result.push((backend, unmanaged)),
Err(error) => show_backend_query_error(&error, &backend),
};
}
Ok(result)
}
/// Create the parent directory of the `path` if that directory does not exist.
///
/// Do nothing otherwise.
@@ -558,7 +556,7 @@ where
///
/// This function will return an error if any of the file names do not match one
/// of group names.
fn find_groups_by_name<'a>(names: &[String], groups: &'a HashSet<Group>) -> Result<Vec<&'a Group>> {
fn find_groups_by_name<'a>(names: &[String], groups: &'a Groups) -> Result<Vec<&'a Group>> {
let name_group_map: HashMap<&str, &Group> =
groups.iter().map(|g| (g.name.as_str(), g)).collect();
@@ -590,20 +588,6 @@ fn show_backend_query_error(error: &anyhow::Error, backend: &AnyBackend) {
}
}
/// If the crate was compiled from git, return `pacdef, <version> (<hash>)`.
/// Otherwise return `pacdef, <version>`.
fn get_name_and_version() -> String {
let backends = get_included_backends();
let mut result = format!("pacdef, version: {}\n", get_version_string());
result.push_str("supported backends:");
for b in backends {
result.push_str("\n ");
result.push_str(b);
}
result
}
/// If the crate was compiled from git, return `<version> (<hash>)`. Otherwise
/// return `<version>`.
pub const fn get_version_string() -> &'static str {
+7 -4
View File
@@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::collections::{BTreeSet, HashSet};
use std::fmt::Display;
use std::fs::{create_dir, read_to_string, File};
use std::hash::Hash;
@@ -13,8 +13,11 @@ use crate::path::get_relative_path;
use super::{Package, Section};
/// A set of groups
pub type Groups = BTreeSet<Group>;
/// Representation of a group file.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Group {
/// Name of the group (file name from which it was read, relative to the group
/// base dir).
@@ -37,8 +40,8 @@ impl Group {
///
/// This function will return an error if any of the files under `group_dir` cannot
/// be accessed.
pub fn load(group_dir: &Path, warn_not_symlinks: bool) -> Result<HashSet<Self>> {
let mut result = HashSet::new();
pub fn load(group_dir: &Path, warn_not_symlinks: bool) -> Result<Groups> {
let mut result = Groups::new();
if !group_dir.is_dir() {
// we only need to create the innermost dir. The rest was already created from when
+1
View File
@@ -14,5 +14,6 @@ mod package;
mod section;
pub use group::Group;
pub use group::Groups;
pub use package::Package;
pub use section::Section;
+1 -1
View File
@@ -7,7 +7,7 @@ use anyhow::{ensure, Context, Result};
use super::Package;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Section {
pub name: String,
pub packages: HashSet<Package>,
+7 -4
View File
@@ -20,22 +20,25 @@
missing_docs
)]
mod args;
pub(crate) mod backend;
#[allow(missing_docs)]
pub mod cli;
mod cmd;
mod config;
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
mod core;
mod env;
mod errors;
mod grouping;
pub mod path;
mod review;
mod search;
mod ui;
pub use crate::args::get as get_args;
pub mod path;
pub use crate::config::Config;
pub use crate::core::Pacdef;
pub use crate::errors::Error;
pub use crate::grouping::Group;
pub use crate::grouping::Groups;
pub use crate::grouping::Package;
+14 -4
View File
@@ -19,8 +19,10 @@ use std::process::{ExitCode, Termination};
use anyhow::{bail, Context, Result};
use clap::Parser;
use pacdef::cli::MainArguments;
use pacdef::path::{get_config_path, get_config_path_old_version, get_group_dir};
use pacdef::{get_args, Config, Error as PacdefError, Group, Pacdef};
use pacdef::{Config, Error as PacdefError, Group};
const MAJOR_UPDATE_MESSAGE: &str = "VERSION UPGRADE
You seem to have used version 0.x of pacdef before.
@@ -51,7 +53,7 @@ fn handle_final_result(result: Result<()>) -> ExitCode {
}
fn main_inner() -> Result<()> {
let args = get_args();
let main_arguments = MainArguments::parse();
let config_file = get_config_path().context("getting config file")?;
@@ -73,8 +75,16 @@ fn main_inner() -> Result<()> {
let groups = Group::load(&group_dir, config.warn_not_symlinks)
.with_context(|| format!("loading groups under {}", group_dir.to_string_lossy()))?;
let pacdef = Pacdef::new(args, config, groups);
pacdef.run_action_from_arg().context("running action")
for group in groups.iter() {
if group.warn_symlink {
eprintln!(
"WARNING: group file {} is not a symlink",
group.path.to_string_lossy()
);
}
}
main_arguments.run(&groups, &config)
}
fn load_default_config(config_file: &Path) -> Result<Config> {
+10 -12
View File
@@ -1,18 +1,16 @@
use std::rc::Rc;
use crate::{backend::AnyBackend, Group, Package};
use super::strategy::Strategy;
#[derive(Debug, PartialEq)]
pub(super) enum ReviewAction {
pub enum ReviewAction {
AsDependency(Package),
Delete(Package),
AssignGroup(Package, Rc<Group>),
AssignGroup(Package, Group),
}
#[derive(Debug)]
pub(super) enum ReviewIntention {
pub enum ReviewIntention {
AsDependency,
AssignGroup,
Delete,
@@ -24,20 +22,20 @@ pub(super) enum ReviewIntention {
}
#[derive(Debug)]
pub(super) struct ReviewsPerBackend {
pub struct ReviewsPerBackend {
items: Vec<(AnyBackend, Vec<ReviewAction>)>,
}
impl ReviewsPerBackend {
pub(super) fn new() -> Self {
pub fn new() -> Self {
Self { items: vec![] }
}
pub(super) fn nothing_to_do(&self) -> bool {
pub fn nothing_to_do(&self) -> bool {
self.items.iter().all(|(_, vec)| vec.is_empty())
}
pub(super) fn push(&mut self, value: (AnyBackend, Vec<ReviewAction>)) {
pub fn push(&mut self, value: (AnyBackend, Vec<ReviewAction>)) {
self.items.push(value);
}
@@ -46,7 +44,7 @@ impl ReviewsPerBackend {
///
/// If there are no actions for a `Backend`, then that `Backend` is removed from the return
/// value.
pub(super) fn into_strategies(self) -> Vec<Strategy> {
pub fn into_strategies(self) -> Vec<Strategy> {
let mut result = vec![];
for (backend, actions) in self {
@@ -85,7 +83,7 @@ impl IntoIterator for ReviewsPerBackend {
}
}
pub(super) enum ContinueWithReview {
pub enum ContinueWithReview {
Yes,
No,
NoAndApply,
@@ -94,7 +92,7 @@ pub(super) enum ContinueWithReview {
fn extract_actions(
actions: Vec<ReviewAction>,
to_delete: &mut Vec<Package>,
assign_group: &mut Vec<(Package, Rc<Group>)>,
assign_group: &mut Vec<(Package, Group)>,
as_dependency: &mut Vec<Package>,
) {
for action in actions {
+8 -15
View File
@@ -2,26 +2,19 @@ mod datastructures;
mod strategy;
use std::io::{stdin, stdout, Write};
use std::rc::Rc;
use anyhow::Result;
use crate::backend::backend_trait::Backend;
use crate::backend::todo_per_backend::ToDoPerBackend;
use crate::ui::{get_user_confirmation, read_single_char_from_terminal};
use crate::{Group, Package};
use crate::{Group, Groups, Package};
use self::datastructures::{ContinueWithReview, ReviewAction, ReviewIntention, ReviewsPerBackend};
use self::strategy::Strategy;
pub fn review(
todo_per_backend: ToDoPerBackend,
groups: impl IntoIterator<Item = Group>,
) -> Result<()> {
pub fn review(todo_per_backend: ToDoPerBackend, groups: &Groups) -> 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");
@@ -32,7 +25,7 @@ pub fn review(
let mut actions = vec![];
for package in packages {
println!("{}: {package}", backend.get_section());
match get_action_for_package(package, &groups, &mut actions, &backend)? {
match get_action_for_package(package, groups, &mut actions, &backend)? {
ContinueWithReview::Yes => continue,
ContinueWithReview::No => return Ok(()),
ContinueWithReview::NoAndApply => {
@@ -76,7 +69,7 @@ pub fn review(
fn get_action_for_package(
package: Package,
groups: &[Rc<Group>],
groups: &Groups,
reviews: &mut Vec<ReviewAction>,
backend: &dyn Backend,
) -> Result<ContinueWithReview> {
@@ -92,7 +85,7 @@ fn get_action_for_package(
}
ReviewIntention::AssignGroup => {
if let Ok(Some(group)) = ask_group(groups) {
reviews.push(ReviewAction::AssignGroup(package, group));
reviews.push(ReviewAction::AssignGroup(package, group.clone()));
break;
};
}
@@ -154,7 +147,7 @@ fn print_query(supports_as_dependency: bool) -> Result<()> {
Ok(())
}
fn print_enumerated_groups(groups: &[Rc<Group>]) {
fn print_enumerated_groups(groups: &Groups) {
let number_digits = get_amount_of_digits_for_number(groups.len());
for (i, group) in groups.iter().enumerate() {
@@ -166,7 +159,7 @@ fn get_amount_of_digits_for_number(number: usize) -> usize {
number.to_string().len()
}
fn ask_group(groups: &[Rc<Group>]) -> Result<Option<Rc<Group>>> {
fn ask_group(groups: &Groups) -> Result<Option<&Group>> {
print_enumerated_groups(groups);
let mut buf = String::new();
stdin().read_line(&mut buf)?;
@@ -179,7 +172,7 @@ fn ask_group(groups: &[Rc<Group>]) -> Result<Option<Rc<Group>>> {
};
if idx < groups.len() {
Ok(Some(groups[idx].clone()))
Ok(groups.iter().nth(idx))
} else {
Ok(None)
}
+7 -9
View File
@@ -1,5 +1,3 @@
use std::rc::Rc;
use anyhow::Result;
use crate::{
@@ -8,19 +6,19 @@ use crate::{
};
#[derive(Debug)]
pub(super) struct Strategy {
pub struct Strategy {
backend: AnyBackend,
delete: Vec<Package>,
as_dependency: Vec<Package>,
assign_group: Vec<(Package, Rc<Group>)>,
assign_group: Vec<(Package, Group)>,
}
impl Strategy {
pub(super) fn new(
pub fn new(
backend: AnyBackend,
delete: Vec<Package>,
as_dependency: Vec<Package>,
assign_group: Vec<(Package, Rc<Group>)>,
assign_group: Vec<(Package, Group)>,
) -> Self {
Self {
backend,
@@ -30,7 +28,7 @@ impl Strategy {
}
}
pub(super) fn execute(self) -> Result<()> {
pub fn execute(self) -> Result<()> {
if !self.delete.is_empty() {
self.backend.remove_packages(&self.delete, false)?;
}
@@ -46,7 +44,7 @@ impl Strategy {
Ok(())
}
pub(super) fn show(&self) {
pub fn show(&self) {
if self.nothing_to_do() {
return;
}
@@ -75,7 +73,7 @@ impl Strategy {
}
}
pub(super) fn nothing_to_do(&self) -> bool {
pub fn nothing_to_do(&self) -> bool {
self.delete.is_empty() && self.as_dependency.is_empty() && self.assign_group.is_empty()
}
}
+5 -3
View File
@@ -1,11 +1,13 @@
use std::collections::HashSet;
use std::iter::Peekable;
use std::vec::IntoIter;
use anyhow::{bail, Result};
use regex::Regex;
use crate::grouping::{Group, Package, Section};
use crate::{
grouping::{Group, Package, Section},
Groups,
};
/// Find all packages in all groups whose name match the regex from the
/// command-line arguments. Print the name of the packages per group and
@@ -16,7 +18,7 @@ use crate::grouping::{Group, Package, Section};
/// This function will return an error if
/// - an invalid regex was provided, or
/// - no matching packages could be found.
pub fn search_packages(regex_str: &str, groups: &HashSet<Group>) -> Result<()> {
pub fn search_packages(regex_str: &str, groups: &Groups) -> Result<()> {
if groups.is_empty() {
eprintln!("WARNING: no group files found");
bail!(crate::errors::Error::NoPackagesFound);