use anyhow::Context;
use colored::*;
use run_script::ScriptOptions;
use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, error::Error, fmt, slice::Iter, string::String};
#[derive(Debug, Serialize, Deserialize, Copy, Clone, EnumIter)]
pub enum PackageSource {
Cargo,
RustupComponent,
Apt,
Pacman,
Snap,
Chocolatey,
Scoop,
Pip,
PipUser,
Pip3,
Pip3User,
Npm,
}
impl PackageSource {
pub fn full_name(&self) -> &str {
match self {
PackageSource::Cargo => "Cargo Rust",
PackageSource::RustupComponent => "Rustup Component",
PackageSource::Apt => "Advanced Package Tool",
PackageSource::Pacman => "Pacman",
PackageSource::Snap => "Snap",
PackageSource::Chocolatey => "Chocolatey",
PackageSource::Scoop => "Scoop",
PackageSource::Pip => "Python Pip",
PackageSource::PipUser => "Python Pip --user",
PackageSource::Pip3 => "Python Pip 3",
PackageSource::Pip3User => "Python Pip 3 --user",
PackageSource::Npm => "Node Package Manager",
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn colour_full_name(&self) -> String {
format!("({})", self.full_name())
.cyan()
.italic()
.to_string()
}
#[cfg(not(target_os = "windows"))]
pub fn command(&self) -> &str {
match self {
PackageSource::Cargo => "cargo",
PackageSource::RustupComponent => "rustup",
PackageSource::Apt => "apt",
PackageSource::Pacman => "pacman",
PackageSource::Snap => "snap",
PackageSource::Pip => "pip",
PackageSource::PipUser => "pip",
PackageSource::Pip3 => "pip3",
PackageSource::Pip3User => "pip3",
PackageSource::Npm => "npm",
_ => "",
}
}
#[cfg(target_os = "windows")]
pub fn command(&self) -> &str {
match self {
PackageSource::Cargo => "cargo",
PackageSource::RustupComponent => "rustup",
PackageSource::Chocolatey => "choco",
PackageSource::Scoop => "scoop",
PackageSource::Pip => "pip",
PackageSource::PipUser => "pip",
PackageSource::Pip3 => "pip3",
PackageSource::Pip3User => "pip3",
PackageSource::Npm => "npm",
_ => "",
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
#[cfg(not(target_os = "windows"))]
pub fn install_command(&self) -> Vec<&str> {
match self {
PackageSource::Cargo => vec!["cargo", "install", "--quiet"],
PackageSource::RustupComponent => vec!["rustup", "component", "add"],
PackageSource::Apt => vec!["apt", "install"],
PackageSource::Pacman => vec!["pacman", "-Sy", "--noconfirm", "--quiet"],
PackageSource::Snap => vec!["snap", "install"],
PackageSource::Pip => vec!["pip", "install", "-q"],
PackageSource::PipUser => vec!["pip", "install", "-q", "--user"],
PackageSource::Pip3 => vec!["pip3", "install", "-q"],
PackageSource::Pip3User => vec!["pip3", "install", "-q", "--user"],
PackageSource::Npm => vec!["npm", "install", "-g"],
_ => vec![],
}
}
#[cfg(target_os = "windows")]
pub fn install_command(&self) -> Vec<&str> {
match self {
PackageSource::Cargo => vec!["cargo", "install", "--quiet"],
PackageSource::RustupComponent => vec!["rustup", "component", "add"],
PackageSource::Chocolatey => vec!["choco", "install", "-y"],
PackageSource::Scoop => vec!["scoop", "install"],
PackageSource::Pip => vec!["pip", "install", "-q"],
PackageSource::PipUser => vec!["pip", "install", "-q", "--user"],
PackageSource::Pip3 => vec!["pip3", "install", "-q"],
PackageSource::Pip3User => vec!["pip3", "install", "-q", "--user"],
PackageSource::Npm => vec!["npm", "install", "-g"],
_ => vec![],
}
}
#[cfg(not(target_os = "windows"))]
pub fn is_installed_script(&self) -> &str {
match self {
PackageSource::Cargo => "cargo install --list | grep 'v[0-9]' | grep -q",
PackageSource::RustupComponent => "rustup component list | grep -q",
PackageSource::Apt => "dpkg-query --show",
PackageSource::Pacman => "pacman -Q",
PackageSource::Snap => "snap | grep -Eo '^[^ ]+' | grep -q",
PackageSource::Pip => "pip show -q",
PackageSource::PipUser => "pip show -q",
PackageSource::Pip3 => "pip3 show -q",
PackageSource::Pip3User => "pip3 show -q",
PackageSource::Npm => "npm list --depth=0 -g | grep -q",
_ => "",
}
}
#[cfg(target_os = "windows")]
pub fn is_installed_script(&self) -> &str {
match self {
PackageSource::Cargo => "cargo install --list | findstr",
PackageSource::RustupComponent => "rustup component list | findstr",
PackageSource::Chocolatey => "choco feature enable --name=\"'useEnhancedExitCodes'\" && choco search -le --no-color",
PackageSource::Scoop => "scoop list | findstr",
PackageSource::Pip => "pip show -q",
PackageSource::PipUser => "pip show -q",
PackageSource::Pip3 => "pip3 show -q",
PackageSource::Pip3User => "pip3 show -q",
PackageSource::Npm => "npm list --depth=0 -g | findstr",
_ => ""
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn needs_root(&self) -> bool {
match self {
PackageSource::Cargo => false,
PackageSource::RustupComponent => false,
PackageSource::Apt => true,
PackageSource::Pacman => true,
PackageSource::Snap => true,
PackageSource::Chocolatey => true,
PackageSource::Scoop => true,
PackageSource::Pip => true,
PackageSource::PipUser => false,
PackageSource::Pip3 => true,
PackageSource::Pip3User => false,
PackageSource::Npm => false,
}
}
}
impl fmt::Display for PackageSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({})", self.full_name())
}
}
impl PartialEq for PackageSource {
fn eq(&self, other: &Self) -> bool {
self.full_name() == other.full_name()
}
}
impl Eq for PackageSource {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Package {
pub source: PackageSource,
pub name: String,
}
impl Package {
pub fn new(source: PackageSource, name: String) -> Self {
Self { source, name }
}
pub fn full_name(&self) -> String {
format!("{} {}", self.name, self.source.full_name())
}
pub fn colour_full_name(&self) -> String {
format!("{} {}", self.name.yellow(), self.source.colour_full_name())
}
pub fn command(&self) -> &str {
self.source.command()
}
pub fn install_command(&self) -> Vec<&str> {
let mut commands = self.source.install_command();
if self.source.needs_root() {
commands.insert(0, "sudo");
}
commands.push(&*self.name);
commands
}
pub fn is_installed(&self) -> Result<bool, Box<dyn Error>> {
let mut options = ScriptOptions::new();
options.exit_on_error = true;
options.print_commands = false;
let (code, _output, _error) =
run_script::run(&*self.is_installed_script(), &vec![], &options)
.context("could not run is installed script")?;
Ok(code == 0)
}
fn is_installed_script(&self) -> String {
format!("{} {}", self.source.is_installed_script(), self.name)
}
}
impl fmt::Display for Package {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.full_name())
}
}
impl Ord for Package {
fn cmp(&self, other: &Self) -> Ordering {
self.full_name().cmp(&other.full_name())
}
}
impl PartialOrd for Package {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Package {
fn eq(&self, other: &Self) -> bool {
self.full_name() == other.full_name() && self.source == other.source
}
}
impl Eq for Package {}
#[derive(Debug, Serialize, Deserialize)]
pub struct Packages(pub Vec<Package>);
impl Packages {
pub fn iter(&self) -> Iter<Package> {
self.0.iter()
}
pub fn merge(&mut self, other: &mut Packages) {
self.0.append(&mut other.0);
self.0.sort();
self.0.dedup();
}
pub fn filter_saved_packages(&mut self, old: &Packages) {
self.0 = self
.0
.iter()
.filter(|package| !old.iter().any(|old_package| *package == old_package))
.cloned()
.collect();
}
pub fn commit_message(&self) -> String {
match self.0.len() {
0 => panic!("Can't create a commit message for empty changes"),
1 => format!("Emplace - mirror package \"{}\"", self.0[0]),
n => format!("Emplace - mirror {} packages", n),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_packages_deduplication() {
let package = Package::new(PackageSource::Cargo, "test".to_string());
let duplicate_package = Package::new(PackageSource::Cargo, "test".to_string());
let packages_vec = vec![package];
let duplicate_packages_vec = vec![duplicate_package];
let mut packages = Packages(packages_vec);
let mut duplicate_packages = Packages(duplicate_packages_vec);
packages.merge(&mut duplicate_packages);
assert_eq!(1, packages.0.len());
}
}