diff options
Diffstat (limited to 'dotup_cli')
| -rw-r--r-- | dotup_cli/Cargo.toml | 29 | ||||
| -rw-r--r-- | dotup_cli/src/commands/init.rs | 22 | ||||
| -rw-r--r-- | dotup_cli/src/commands/install.rs | 25 | ||||
| -rw-r--r-- | dotup_cli/src/commands/link.rs | 134 | ||||
| -rw-r--r-- | dotup_cli/src/commands/mod.rs | 11 | ||||
| -rw-r--r-- | dotup_cli/src/commands/mv.rs | 53 | ||||
| -rw-r--r-- | dotup_cli/src/commands/status.rs | 175 | ||||
| -rw-r--r-- | dotup_cli/src/commands/uninstall.rs | 26 | ||||
| -rw-r--r-- | dotup_cli/src/commands/unlink.rs | 43 | ||||
| -rw-r--r-- | dotup_cli/src/config.rs | 10 | ||||
| -rw-r--r-- | dotup_cli/src/main.rs | 111 | ||||
| -rw-r--r-- | dotup_cli/src/utils.rs | 182 | ||||
| -rw-r--r-- | dotup_cli/tests/cli.rs | 145 |
13 files changed, 0 insertions, 966 deletions
diff --git a/dotup_cli/Cargo.toml b/dotup_cli/Cargo.toml deleted file mode 100644 index 89b8c27..0000000 --- a/dotup_cli/Cargo.toml +++ /dev/null | |||
| @@ -1,29 +0,0 @@ | |||
| 1 | [package] | ||
| 2 | edition = "2018" | ||
| 3 | name = "dotup_cli" | ||
| 4 | version = "0.1.0" | ||
| 5 | |||
| 6 | [[bin]] | ||
| 7 | name = "dotup" | ||
| 8 | path = "src/main.rs" | ||
| 9 | doc = false | ||
| 10 | |||
| 11 | [dependencies] | ||
| 12 | anyhow = "1.0" | ||
| 13 | log = "0.4" | ||
| 14 | ansi_term = "0.12.1" | ||
| 15 | |||
| 16 | [dependencies.clap] | ||
| 17 | features = ["derive"] | ||
| 18 | version = "3.0.0-rc.7" | ||
| 19 | |||
| 20 | [dependencies.dotup] | ||
| 21 | path = "../dotup" | ||
| 22 | |||
| 23 | [dependencies.flexi_logger] | ||
| 24 | features = ["colors"] | ||
| 25 | version = "0.22" | ||
| 26 | |||
| 27 | [dev-dependencies] | ||
| 28 | tempfile = "3.2" | ||
| 29 | assert_cmd = "2.0" | ||
diff --git a/dotup_cli/src/commands/init.rs b/dotup_cli/src/commands/init.rs deleted file mode 100644 index 45129bf..0000000 --- a/dotup_cli/src/commands/init.rs +++ /dev/null | |||
| @@ -1,22 +0,0 @@ | |||
| 1 | use super::prelude::*; | ||
| 2 | |||
| 3 | /// Creates an empty depot file if one doesnt already exist. | ||
| 4 | /// | ||
| 5 | /// By default this will create the file in the current directory | ||
| 6 | /// but the --depot flag can be used to change this path. | ||
| 7 | #[derive(Parser)] | ||
| 8 | pub struct Opts {} | ||
| 9 | |||
| 10 | pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { | ||
| 11 | if !dotup::utils::is_file(&config.archive_path)? { | ||
| 12 | let archive = Archive::default(); | ||
| 13 | log::info!("Creating archive at {}", &config.archive_path.display()); | ||
| 14 | utils::write_archive(&config.archive_path, &archive)?; | ||
| 15 | } else { | ||
| 16 | log::warn!( | ||
| 17 | "Archive file already exists : '{}'", | ||
| 18 | config.archive_path.display() | ||
| 19 | ); | ||
| 20 | } | ||
| 21 | Ok(()) | ||
| 22 | } | ||
diff --git a/dotup_cli/src/commands/install.rs b/dotup_cli/src/commands/install.rs deleted file mode 100644 index 6d9fbf7..0000000 --- a/dotup_cli/src/commands/install.rs +++ /dev/null | |||
| @@ -1,25 +0,0 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use super::prelude::*; | ||
| 4 | |||
| 5 | /// Install links. (Creates symlinks). | ||
| 6 | /// | ||
| 7 | /// Installing a link will create the necessary directories. | ||
| 8 | /// If a file or directory already exists at the location a link would be installed this command will fail. | ||
| 9 | #[derive(Parser)] | ||
| 10 | pub struct Opts { | ||
| 11 | /// The files/directories to install. | ||
| 12 | #[clap(min_values = 1, default_value = ".")] | ||
| 13 | paths: Vec<PathBuf>, | ||
| 14 | } | ||
| 15 | |||
| 16 | pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { | ||
| 17 | let depot = utils::read_depot(&config.archive_path)?; | ||
| 18 | |||
| 19 | for link in utils::collect_links_by_base_paths(&depot, &opts.paths) { | ||
| 20 | log::info!("Installing link {}", link); | ||
| 21 | depot.install_link(link, &config.install_path)?; | ||
| 22 | } | ||
| 23 | |||
| 24 | Ok(()) | ||
| 25 | } | ||
diff --git a/dotup_cli/src/commands/link.rs b/dotup_cli/src/commands/link.rs deleted file mode 100644 index d1f61ea..0000000 --- a/dotup_cli/src/commands/link.rs +++ /dev/null | |||
| @@ -1,134 +0,0 @@ | |||
| 1 | use std::{ | ||
| 2 | fs::{DirEntry, Metadata}, | ||
| 3 | path::{Path, PathBuf}, | ||
| 4 | }; | ||
| 5 | |||
| 6 | use super::prelude::*; | ||
| 7 | |||
| 8 | /// Creates links | ||
| 9 | /// | ||
| 10 | /// If a link is created for a file that already had a link then the old link will be overwritten. | ||
| 11 | /// By default creating a link to a directory will recursively link all files under that | ||
| 12 | /// directory, to actually link a directory use the --directory flag. | ||
| 13 | #[derive(Parser)] | ||
| 14 | pub struct Opts { | ||
| 15 | /// Treats the paths as directories. This will create links to the actual directories instead | ||
| 16 | /// of recursively linking all files under them. | ||
| 17 | #[clap(long)] | ||
| 18 | directory: bool, | ||
| 19 | |||
| 20 | /// The paths to link. The last path is the destination. | ||
| 21 | paths: Vec<PathBuf>, | ||
| 22 | } | ||
| 23 | |||
| 24 | // TODO: require destination | ||
| 25 | // remove else branch | ||
| 26 | pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { | ||
| 27 | let mut depot = utils::read_depot(&config.archive_path)?; | ||
| 28 | |||
| 29 | let (origins, destination) = match opts.paths.as_slice() { | ||
| 30 | p @ [] | p @ [_] => (p, None), | ||
| 31 | [o @ .., dest] => (o, Some(dest)), | ||
| 32 | _ => unreachable!(), | ||
| 33 | }; | ||
| 34 | |||
| 35 | if let Some(destination) = destination { | ||
| 36 | let params = if opts.directory { | ||
| 37 | origins | ||
| 38 | .iter() | ||
| 39 | .map(|p| LinkCreateParams { | ||
| 40 | origin: p.to_path_buf(), | ||
| 41 | destination: destination.clone(), | ||
| 42 | }) | ||
| 43 | .collect() | ||
| 44 | } else { | ||
| 45 | let mut params = Vec::new(); | ||
| 46 | for origin in origins { | ||
| 47 | generate_link_params(&depot, origin, destination, origin, &mut params)?; | ||
| 48 | } | ||
| 49 | params | ||
| 50 | }; | ||
| 51 | |||
| 52 | for link_params in params { | ||
| 53 | log::info!("Creating link : {}", link_params); | ||
| 54 | depot.create_link(link_params)?; | ||
| 55 | } | ||
| 56 | } else { | ||
| 57 | let base_path = match origins { | ||
| 58 | [] => std::env::current_dir()?, | ||
| 59 | [path] => path.clone(), | ||
| 60 | _ => unreachable!(), | ||
| 61 | }; | ||
| 62 | |||
| 63 | for link in utils::collect_links_by_base_paths(&depot, std::iter::once(base_path)) { | ||
| 64 | log::info!("{}", link); | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | utils::write_depot(&depot)?; | ||
| 69 | |||
| 70 | Ok(()) | ||
| 71 | } | ||
| 72 | |||
| 73 | fn generate_link_params( | ||
| 74 | depot: &Depot, | ||
| 75 | origin: &Path, | ||
| 76 | destination: &Path, | ||
| 77 | base: &Path, | ||
| 78 | params: &mut Vec<LinkCreateParams>, | ||
| 79 | ) -> anyhow::Result<()> { | ||
| 80 | let metadata = std::fs::metadata(origin)?; | ||
| 81 | if metadata.is_file() { | ||
| 82 | generate_file_link_params(depot, origin, destination, base, params)?; | ||
| 83 | } else if metadata.is_dir() { | ||
| 84 | generate_directory_link_params_recursive(depot, origin, destination, base, params)?; | ||
| 85 | } | ||
| 86 | Ok(()) | ||
| 87 | } | ||
| 88 | |||
| 89 | fn generate_file_link_params( | ||
| 90 | depot: &Depot, | ||
| 91 | origin: &Path, | ||
| 92 | destination: &Path, | ||
| 93 | base: &Path, | ||
| 94 | params: &mut Vec<LinkCreateParams>, | ||
| 95 | ) -> anyhow::Result<()> { | ||
| 96 | let origin_canonical = origin | ||
| 97 | .canonicalize() | ||
| 98 | .expect("Failed to canonicalize origin path"); | ||
| 99 | let base_canonical = base | ||
| 100 | .canonicalize() | ||
| 101 | .expect("Failed to canonicalize base path"); | ||
| 102 | |||
| 103 | log::debug!("Origin canonical : {}", origin_canonical.display()); | ||
| 104 | log::debug!("Base : {}", base.display()); | ||
| 105 | |||
| 106 | let partial = origin_canonical | ||
| 107 | .strip_prefix(base_canonical) | ||
| 108 | .expect("Failed to remove prefix from origin path"); | ||
| 109 | let destination = destination.join(partial); | ||
| 110 | let origin = origin_canonical | ||
| 111 | .strip_prefix(depot.base_path()) | ||
| 112 | .unwrap_or(&origin_canonical); | ||
| 113 | |||
| 114 | let link_params = LinkCreateParams { | ||
| 115 | origin: origin.to_path_buf(), | ||
| 116 | destination, | ||
| 117 | }; | ||
| 118 | params.push(link_params); | ||
| 119 | Ok(()) | ||
| 120 | } | ||
| 121 | |||
| 122 | fn generate_directory_link_params_recursive( | ||
| 123 | depot: &Depot, | ||
| 124 | dir_path: &Path, | ||
| 125 | destination: &Path, | ||
| 126 | base: &Path, | ||
| 127 | params: &mut Vec<LinkCreateParams>, | ||
| 128 | ) -> anyhow::Result<()> { | ||
| 129 | for origin in dir_path.read_dir()? { | ||
| 130 | let origin = origin?.path(); | ||
| 131 | generate_link_params(depot, &origin, destination, base, params)?; | ||
| 132 | } | ||
| 133 | Ok(()) | ||
| 134 | } | ||
diff --git a/dotup_cli/src/commands/mod.rs b/dotup_cli/src/commands/mod.rs deleted file mode 100644 index bd92599..0000000 --- a/dotup_cli/src/commands/mod.rs +++ /dev/null | |||
| @@ -1,11 +0,0 @@ | |||
| 1 | pub mod init; | ||
| 2 | pub mod install; | ||
| 3 | pub mod link; | ||
| 4 | pub mod mv; | ||
| 5 | pub mod status; | ||
| 6 | pub mod uninstall; | ||
| 7 | pub mod unlink; | ||
| 8 | |||
| 9 | mod prelude { | ||
| 10 | pub use crate::prelude::*; | ||
| 11 | } | ||
diff --git a/dotup_cli/src/commands/mv.rs b/dotup_cli/src/commands/mv.rs deleted file mode 100644 index aae2715..0000000 --- a/dotup_cli/src/commands/mv.rs +++ /dev/null | |||
| @@ -1,53 +0,0 @@ | |||
| 1 | use std::path::{Path, PathBuf}; | ||
| 2 | |||
| 3 | use super::prelude::*; | ||
| 4 | |||
| 5 | /// Install links. (Creates symlinks). | ||
| 6 | /// | ||
| 7 | /// Installing a link will create the necessary directories. | ||
| 8 | /// If a file or directory already exists at the location a link would be installed this command will fail. | ||
| 9 | #[derive(Parser)] | ||
| 10 | pub struct Opts { | ||
| 11 | /// The files/directories to move | ||
| 12 | #[clap(min_values = 2)] | ||
| 13 | paths: Vec<PathBuf>, | ||
| 14 | } | ||
| 15 | |||
| 16 | pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { | ||
| 17 | let mut depot = utils::read_depot(&config.archive_path)?; | ||
| 18 | |||
| 19 | let (sources, destination) = match opts.paths.as_slice() { | ||
| 20 | [source, destination] => {} | ||
| 21 | [sources @ .., destination] => { | ||
| 22 | let mut curr_destination = destination.to_owned(); | ||
| 23 | for source in sources { | ||
| 24 | let filename = match source.file_name() { | ||
| 25 | Some(filename) => filename, | ||
| 26 | None => { | ||
| 27 | log::warn!("Ignoring '{}', unknown file name", source.display()); | ||
| 28 | continue; | ||
| 29 | } | ||
| 30 | }; | ||
| 31 | curr_destination.push(filename); | ||
| 32 | std::fs::rename(source, &curr_destination)?; | ||
| 33 | if let Some(id) = depot.get_link_id_by_path(&source) { | ||
| 34 | depot.rename_link(id, &curr_destination); | ||
| 35 | } | ||
| 36 | curr_destination.pop(); | ||
| 37 | } | ||
| 38 | } | ||
| 39 | _ => unreachable!(), | ||
| 40 | }; | ||
| 41 | |||
| 42 | utils::write_depot(&depot)?; | ||
| 43 | |||
| 44 | Ok(()) | ||
| 45 | } | ||
| 46 | |||
| 47 | fn rename(depot: &mut Depot, source: &Path, destination: &Path) -> anyhow::Result<()> { | ||
| 48 | std::fs::rename(source, &destination)?; | ||
| 49 | if let Some(id) = depot.get_link_id_by_path(&source) { | ||
| 50 | depot.rename_link(id, &destination); | ||
| 51 | } | ||
| 52 | Ok(()) | ||
| 53 | } | ||
diff --git a/dotup_cli/src/commands/status.rs b/dotup_cli/src/commands/status.rs deleted file mode 100644 index b7221db..0000000 --- a/dotup_cli/src/commands/status.rs +++ /dev/null | |||
| @@ -1,175 +0,0 @@ | |||
| 1 | use std::path::{Path, PathBuf}; | ||
| 2 | |||
| 3 | use ansi_term::Colour; | ||
| 4 | use clap::Parser; | ||
| 5 | use dotup::{Depot, Link}; | ||
| 6 | |||
| 7 | use crate::{utils, Config}; | ||
| 8 | |||
| 9 | /// Shows information about links | ||
| 10 | /// | ||
| 11 | /// If a link is created for a file that already had a link then the old link will be overwritten. | ||
| 12 | /// By default creating a link to a directory will recursively link all files under that | ||
| 13 | /// directory, to actually link a directory use the --directory flag. | ||
| 14 | #[derive(Parser)] | ||
| 15 | pub struct Opts { | ||
| 16 | /// The location where links will be installed to. | ||
| 17 | /// Defaults to the home directory. | ||
| 18 | #[clap(long)] | ||
| 19 | install_base: Option<PathBuf>, | ||
| 20 | |||
| 21 | /// The paths to show the status of | ||
| 22 | paths: Vec<PathBuf>, | ||
| 23 | } | ||
| 24 | |||
| 25 | #[derive(Debug)] | ||
| 26 | struct State { | ||
| 27 | max_depth: u32, | ||
| 28 | install_path: PathBuf, | ||
| 29 | } | ||
| 30 | |||
| 31 | pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { | ||
| 32 | let mut depot = utils::read_depot(&config.archive_path)?; | ||
| 33 | |||
| 34 | // walk dir | ||
| 35 | // if node is file: | ||
| 36 | // if linked | ||
| 37 | // print name in green and destination blue | ||
| 38 | // if invalid | ||
| 39 | // print name and destination red | ||
| 40 | // if not linked | ||
| 41 | // print name in gray | ||
| 42 | // if node is directory: | ||
| 43 | // if linked | ||
| 44 | // print name in green and destination blue | ||
| 45 | // if invalid | ||
| 46 | // print name and destination red | ||
| 47 | // if not linked: | ||
| 48 | // print name in gray | ||
| 49 | // if contains files that are linked/invalid: | ||
| 50 | // recurse into directory | ||
| 51 | // | ||
| 52 | |||
| 53 | let depot_base = depot.base_path(); | ||
| 54 | let mut paths = Vec::new(); | ||
| 55 | for path in opts.paths { | ||
| 56 | let canonical = dotup::utils::weakly_canonical(&path); | ||
| 57 | if canonical.starts_with(depot_base) { | ||
| 58 | paths.push(canonical); | ||
| 59 | } else { | ||
| 60 | log::warn!("Path '{}' is outside the depot", path.display()); | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | if paths.is_empty() { | ||
| 65 | paths.push(PathBuf::from(".")); | ||
| 66 | } | ||
| 67 | |||
| 68 | let state = State { | ||
| 69 | max_depth: u32::MAX, | ||
| 70 | install_path: config.install_path, | ||
| 71 | }; | ||
| 72 | |||
| 73 | let (directories, files) = utils::collect_read_dir_split(".")?; | ||
| 74 | for path in directories.into_iter().chain(files.into_iter()) { | ||
| 75 | display_status_path(&depot, &state, &path, 0); | ||
| 76 | } | ||
| 77 | |||
| 78 | utils::write_depot(&depot)?; | ||
| 79 | |||
| 80 | Ok(()) | ||
| 81 | } | ||
| 82 | |||
| 83 | fn display_status_path(depot: &Depot, state: &State, path: &Path, depth: u32) { | ||
| 84 | if depth == state.max_depth { | ||
| 85 | return; | ||
| 86 | } | ||
| 87 | |||
| 88 | if path.is_dir() { | ||
| 89 | display_status_directory(depot, state, path, depth); | ||
| 90 | } else { | ||
| 91 | display_status_file(depot, state, path, depth); | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | fn display_status_directory(depot: &Depot, state: &State, path: &Path, depth: u32) { | ||
| 96 | assert!(path.is_dir()); | ||
| 97 | if depth == state.max_depth || !depot.subpath_has_links(path) { | ||
| 98 | return; | ||
| 99 | } | ||
| 100 | |||
| 101 | if let Some(link) = depot.get_link_by_path(path) { | ||
| 102 | print_link(depot, state, link, depth); | ||
| 103 | } else { | ||
| 104 | for entry in std::fs::read_dir(path).unwrap() { | ||
| 105 | let entry = match entry { | ||
| 106 | Ok(entry) => entry, | ||
| 107 | Err(_) => continue, | ||
| 108 | }; | ||
| 109 | let entry_path = entry.path().canonicalize().unwrap(); | ||
| 110 | let entry_path_stripped = entry_path | ||
| 111 | .strip_prefix(std::env::current_dir().unwrap()) | ||
| 112 | .unwrap(); | ||
| 113 | |||
| 114 | print_identation(depth); | ||
| 115 | println!( | ||
| 116 | "{}", | ||
| 117 | Colour::Yellow.paint(&format!("{}", path.file_name().unwrap().to_string_lossy())) | ||
| 118 | ); | ||
| 119 | |||
| 120 | display_status_path(depot, state, &entry_path_stripped, depth + 1); | ||
| 121 | } | ||
| 122 | } | ||
| 123 | } | ||
| 124 | |||
| 125 | fn display_status_file(depot: &Depot, state: &State, path: &Path, depth: u32) { | ||
| 126 | print_identation(depth); | ||
| 127 | if let Some(link) = depot.get_link_by_path(path) { | ||
| 128 | print_link(depot, state, link, depth); | ||
| 129 | } else { | ||
| 130 | print_unlinked(path, depth); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | fn print_link(depot: &Depot, state: &State, link: &Link, depth: u32) { | ||
| 135 | let origin = link.origin(); | ||
| 136 | let dest = link.destination(); | ||
| 137 | let filename = match origin.file_name() { | ||
| 138 | Some(filename) => filename, | ||
| 139 | None => return, | ||
| 140 | }; | ||
| 141 | |||
| 142 | print_identation(depth); | ||
| 143 | if depot.is_link_installed(link, &state.install_path) { | ||
| 144 | println!( | ||
| 145 | "{} -> {}", | ||
| 146 | Colour::Green.paint(&format!("{}", filename.to_string_lossy())), | ||
| 147 | Colour::Blue.paint(&format!("{}", dest.display())), | ||
| 148 | ); | ||
| 149 | } else { | ||
| 150 | println!( | ||
| 151 | "{}", | ||
| 152 | Colour::Red.paint(&format!( | ||
| 153 | "{} -> {}", | ||
| 154 | filename.to_string_lossy(), | ||
| 155 | dest.display() | ||
| 156 | )) | ||
| 157 | ); | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | fn print_unlinked(path: &Path, depth: u32) { | ||
| 162 | let filename = match path.file_name() { | ||
| 163 | Some(filename) => filename, | ||
| 164 | None => return, | ||
| 165 | }; | ||
| 166 | |||
| 167 | print_identation(depth); | ||
| 168 | println!("{}", filename.to_string_lossy()); | ||
| 169 | } | ||
| 170 | |||
| 171 | fn print_identation(depth: u32) { | ||
| 172 | for i in 0..depth { | ||
| 173 | print!(" "); | ||
| 174 | } | ||
| 175 | } | ||
diff --git a/dotup_cli/src/commands/uninstall.rs b/dotup_cli/src/commands/uninstall.rs deleted file mode 100644 index fe44bf0..0000000 --- a/dotup_cli/src/commands/uninstall.rs +++ /dev/null | |||
| @@ -1,26 +0,0 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use super::prelude::*; | ||
| 4 | |||
| 5 | /// Uninstalls links. (Removes symlinks). | ||
| 6 | /// | ||
| 7 | /// Uninstalling a link for a file that didnt have a link will do nothing. | ||
| 8 | /// Uninstalling a directory will recursively uninstall all files under it. | ||
| 9 | /// Symlinks are only deleted if they were pointing to the correct file. | ||
| 10 | #[derive(Parser)] | ||
| 11 | pub struct Opts { | ||
| 12 | /// The files/directories to uninstall. | ||
| 13 | #[clap(min_values = 1, default_value = ".")] | ||
| 14 | paths: Vec<PathBuf>, | ||
| 15 | } | ||
| 16 | |||
| 17 | pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { | ||
| 18 | let depot = utils::read_depot(&config.archive_path)?; | ||
| 19 | |||
| 20 | for link in utils::collect_links_by_base_paths(&depot, &opts.paths) { | ||
| 21 | log::info!("Uninstalling link : {}", link); | ||
| 22 | depot.uninstall_link(link, &config.install_path)?; | ||
| 23 | } | ||
| 24 | |||
| 25 | Ok(()) | ||
| 26 | } | ||
diff --git a/dotup_cli/src/commands/unlink.rs b/dotup_cli/src/commands/unlink.rs deleted file mode 100644 index abe23e3..0000000 --- a/dotup_cli/src/commands/unlink.rs +++ /dev/null | |||
| @@ -1,43 +0,0 @@ | |||
| 1 | use std::{ | ||
| 2 | fs::{DirEntry, Metadata}, | ||
| 3 | path::{Path, PathBuf}, | ||
| 4 | }; | ||
| 5 | |||
| 6 | use super::prelude::*; | ||
| 7 | |||
| 8 | /// Unlinks files/directories. | ||
| 9 | /// | ||
| 10 | /// This will recursively remove links. If a path is a directory then it will remove all links | ||
| 11 | /// recursively. | ||
| 12 | /// The links are not uninstall by default, see the --uninstall parameter. | ||
| 13 | #[derive(Parser)] | ||
| 14 | pub struct Opts { | ||
| 15 | /// Specify the install base if the links are also to be uninstalled. | ||
| 16 | #[clap(long)] | ||
| 17 | uninstall: Option<PathBuf>, | ||
| 18 | |||
| 19 | /// The paths to unlink. | ||
| 20 | #[clap(required = true, min_values = 1)] | ||
| 21 | paths: Vec<PathBuf>, | ||
| 22 | } | ||
| 23 | |||
| 24 | pub fn main(config: Config, opts: Opts) -> anyhow::Result<()> { | ||
| 25 | let mut depot = utils::read_depot(&config.archive_path)?; | ||
| 26 | |||
| 27 | for link_id in utils::collect_link_ids_by_base_paths(&depot, &opts.paths) { | ||
| 28 | let link = depot.get_link(link_id).unwrap(); | ||
| 29 | log::info!( | ||
| 30 | "Unlinking(uninstall = {}) : {}", | ||
| 31 | opts.uninstall.is_some(), | ||
| 32 | link | ||
| 33 | ); | ||
| 34 | if let Some(ref install_base) = opts.uninstall { | ||
| 35 | depot.uninstall_link(link, &install_base)?; | ||
| 36 | } | ||
| 37 | depot.remove_link(link_id); | ||
| 38 | } | ||
| 39 | |||
| 40 | utils::write_depot(&depot)?; | ||
| 41 | |||
| 42 | Ok(()) | ||
| 43 | } | ||
diff --git a/dotup_cli/src/config.rs b/dotup_cli/src/config.rs deleted file mode 100644 index dabaf74..0000000 --- a/dotup_cli/src/config.rs +++ /dev/null | |||
| @@ -1,10 +0,0 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | #[derive(Debug)] | ||
| 4 | pub struct Config { | ||
| 5 | pub archive_path: PathBuf, | ||
| 6 | pub install_path: PathBuf, | ||
| 7 | pub working_path: PathBuf, | ||
| 8 | } | ||
| 9 | |||
| 10 | impl Config {} | ||
diff --git a/dotup_cli/src/main.rs b/dotup_cli/src/main.rs deleted file mode 100644 index 0d730da..0000000 --- a/dotup_cli/src/main.rs +++ /dev/null | |||
| @@ -1,111 +0,0 @@ | |||
| 1 | #![allow(unused)] | ||
| 2 | |||
| 3 | pub mod commands; | ||
| 4 | pub mod config; | ||
| 5 | pub mod utils; | ||
| 6 | |||
| 7 | pub use config::Config; | ||
| 8 | |||
| 9 | pub mod prelude { | ||
| 10 | pub use super::{utils, Config}; | ||
| 11 | pub use clap::{AppSettings, Parser}; | ||
| 12 | pub use dotup::{Archive, Depot, DepotConfig, Link, LinkCreateParams, LinkID}; | ||
| 13 | } | ||
| 14 | |||
| 15 | use clap::{AppSettings, Parser}; | ||
| 16 | use flexi_logger::Logger; | ||
| 17 | use std::{ | ||
| 18 | collections::HashMap, | ||
| 19 | iter::FromIterator, | ||
| 20 | path::{Path, PathBuf}, | ||
| 21 | }; | ||
| 22 | |||
| 23 | use prelude::*; | ||
| 24 | |||
| 25 | #[derive(Parser)] | ||
| 26 | struct Opts { | ||
| 27 | /// Path to the depot file. | ||
| 28 | /// | ||
| 29 | /// By default it will try to find a file named "depot.toml" in the current directory or any of | ||
| 30 | /// the parent directories. | ||
| 31 | #[clap(long)] | ||
| 32 | depot: Option<PathBuf>, | ||
| 33 | |||
| 34 | /// Disable output to the console | ||
| 35 | #[clap(short, long)] | ||
| 36 | quiet: bool, | ||
| 37 | |||
| 38 | /// A level of verbosity, and can be used multiple times | ||
| 39 | /// | ||
| 40 | /// Level 1 - Info | ||
| 41 | /// | ||
| 42 | /// Level 2 - Debug | ||
| 43 | /// | ||
| 44 | /// Level 3 - Trace | ||
| 45 | #[clap(short, long, parse(from_occurrences))] | ||
| 46 | verbose: i32, | ||
| 47 | |||
| 48 | /// The location where links will be installed to. | ||
| 49 | /// Defaults to the home directory. | ||
| 50 | #[clap(short, long)] | ||
| 51 | install_path: Option<PathBuf>, | ||
| 52 | |||
| 53 | #[clap(subcommand)] | ||
| 54 | subcmd: SubCommand, | ||
| 55 | } | ||
| 56 | |||
| 57 | #[derive(Parser)] | ||
| 58 | enum SubCommand { | ||
| 59 | Init(commands::init::Opts), | ||
| 60 | Link(commands::link::Opts), | ||
| 61 | Mv(commands::mv::Opts), | ||
| 62 | Status(commands::status::Opts), | ||
| 63 | Unlink(commands::unlink::Opts), | ||
| 64 | Install(commands::install::Opts), | ||
| 65 | Uninstall(commands::uninstall::Opts), | ||
| 66 | } | ||
| 67 | |||
| 68 | fn main() -> anyhow::Result<()> { | ||
| 69 | let opts = Opts::parse(); | ||
| 70 | |||
| 71 | if !opts.quiet { | ||
| 72 | let log_level = match opts.verbose { | ||
| 73 | 0 => "warn", | ||
| 74 | 1 => "info", | ||
| 75 | 2 => "debug", | ||
| 76 | _ => "trace", | ||
| 77 | }; | ||
| 78 | |||
| 79 | Logger::try_with_env_or_str(log_level)? | ||
| 80 | .format(flexi_logger::colored_default_format) | ||
| 81 | .set_palette("196;208;32;198;15".to_string()) | ||
| 82 | .start()?; | ||
| 83 | } | ||
| 84 | |||
| 85 | let archive_path = match opts.depot { | ||
| 86 | Some(path) => path, | ||
| 87 | None => utils::find_archive_path()?, | ||
| 88 | }; | ||
| 89 | let install_path = match opts.install_path { | ||
| 90 | Some(path) => path, | ||
| 91 | None => utils::home_directory()?, | ||
| 92 | }; | ||
| 93 | let working_path = std::env::current_dir().expect("Failed to obtain current working directory"); | ||
| 94 | log::debug!("Archive path : {}", archive_path.display()); | ||
| 95 | |||
| 96 | let config = Config { | ||
| 97 | archive_path, | ||
| 98 | install_path, | ||
| 99 | working_path, | ||
| 100 | }; | ||
| 101 | |||
| 102 | match opts.subcmd { | ||
| 103 | SubCommand::Init(opts) => commands::init::main(config, opts), | ||
| 104 | SubCommand::Link(opts) => commands::link::main(config, opts), | ||
| 105 | SubCommand::Mv(opts) => commands::mv::main(config, opts), | ||
| 106 | SubCommand::Status(opts) => commands::status::main(config, opts), | ||
| 107 | SubCommand::Unlink(opts) => commands::unlink::main(config, opts), | ||
| 108 | SubCommand::Install(opts) => commands::install::main(config, opts), | ||
| 109 | SubCommand::Uninstall(opts) => commands::uninstall::main(config, opts), | ||
| 110 | } | ||
| 111 | } | ||
diff --git a/dotup_cli/src/utils.rs b/dotup_cli/src/utils.rs deleted file mode 100644 index b9a76a7..0000000 --- a/dotup_cli/src/utils.rs +++ /dev/null | |||
| @@ -1,182 +0,0 @@ | |||
| 1 | use std::{ | ||
| 2 | collections::VecDeque, | ||
| 3 | path::{Path, PathBuf}, | ||
| 4 | }; | ||
| 5 | |||
| 6 | use crate::prelude::*; | ||
| 7 | |||
| 8 | const DEFAULT_DEPOT_NAME: &str = "depot.toml"; | ||
| 9 | |||
| 10 | pub fn home_directory() -> anyhow::Result<PathBuf> { | ||
| 11 | match std::env::var("HOME") { | ||
| 12 | Ok(val) => Ok(PathBuf::from(val)), | ||
| 13 | Err(e) => { | ||
| 14 | log::error!("Failed to get home directory from enviornment variable"); | ||
| 15 | Err(e.into()) | ||
| 16 | } | ||
| 17 | } | ||
| 18 | } | ||
| 19 | |||
| 20 | pub fn find_archive_path() -> anyhow::Result<PathBuf> { | ||
| 21 | let cwd = std::env::current_dir()?; | ||
| 22 | let compn = cwd.components().count(); | ||
| 23 | let mut start = PathBuf::new(); | ||
| 24 | for _ in 0..=compn { | ||
| 25 | start.push(DEFAULT_DEPOT_NAME); | ||
| 26 | if dotup::archive_exists(&start) { | ||
| 27 | return Ok(start); | ||
| 28 | } | ||
| 29 | start.pop(); | ||
| 30 | start.push(".."); | ||
| 31 | } | ||
| 32 | Ok(PathBuf::from(DEFAULT_DEPOT_NAME)) | ||
| 33 | } | ||
| 34 | |||
| 35 | pub fn write_archive(path: impl AsRef<Path>, archive: &Archive) -> anyhow::Result<()> { | ||
| 36 | let path = path.as_ref(); | ||
| 37 | log::debug!("Writing archive to {}", path.display()); | ||
| 38 | match dotup::archive_write(path, archive) { | ||
| 39 | Ok(_) => Ok(()), | ||
| 40 | Err(e) => { | ||
| 41 | log::error!( | ||
| 42 | "Failed to write archive to : {}\nError : {}", | ||
| 43 | path.display(), | ||
| 44 | e | ||
| 45 | ); | ||
| 46 | Err(e.into()) | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | pub fn write_depot(depot: &Depot) -> anyhow::Result<()> { | ||
| 52 | let write_path = depot.archive_path(); | ||
| 53 | let archive = depot.archive(); | ||
| 54 | match dotup::archive_write(write_path, &archive) { | ||
| 55 | Ok(_) => Ok(()), | ||
| 56 | Err(e) => { | ||
| 57 | log::error!( | ||
| 58 | "Failed to write depot archive to : {}\nError : {}", | ||
| 59 | write_path.display(), | ||
| 60 | e | ||
| 61 | ); | ||
| 62 | Err(e.into()) | ||
| 63 | } | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | pub fn read_archive(path: impl AsRef<Path>) -> anyhow::Result<Archive> { | ||
| 68 | let path = path.as_ref(); | ||
| 69 | match dotup::archive_read(path) { | ||
| 70 | Ok(archive) => Ok(archive), | ||
| 71 | Err(e) => { | ||
| 72 | log::error!( | ||
| 73 | "Failed to read archive from : {}\nError : {}", | ||
| 74 | path.display(), | ||
| 75 | e | ||
| 76 | ); | ||
| 77 | Err(e.into()) | ||
| 78 | } | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | pub fn read_depot(archive_path: impl AsRef<Path>) -> anyhow::Result<Depot> { | ||
| 83 | let archive_path = archive_path.as_ref().to_path_buf(); | ||
| 84 | let archive = read_archive(&archive_path)?; | ||
| 85 | let depot_config = DepotConfig { | ||
| 86 | archive: Default::default(), | ||
| 87 | archive_path, | ||
| 88 | }; | ||
| 89 | let mut depot = Depot::new(depot_config)?; | ||
| 90 | |||
| 91 | for archive_link in archive.links { | ||
| 92 | let link_params = LinkCreateParams::from(archive_link); | ||
| 93 | if let Err(e) = depot.create_link(link_params) { | ||
| 94 | log::warn!("Error while adding link : {}", e); | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | Ok(depot) | ||
| 99 | } | ||
| 100 | |||
| 101 | pub fn collect_links_by_base_paths( | ||
| 102 | depot: &Depot, | ||
| 103 | paths: impl IntoIterator<Item = impl AsRef<Path>>, | ||
| 104 | ) -> Vec<&Link> { | ||
| 105 | let canonical_paths: Vec<_> = paths | ||
| 106 | .into_iter() | ||
| 107 | .map(|p| p.as_ref().canonicalize().unwrap()) | ||
| 108 | .collect(); | ||
| 109 | |||
| 110 | depot | ||
| 111 | .links() | ||
| 112 | .filter(|&l| { | ||
| 113 | canonical_paths | ||
| 114 | .iter() | ||
| 115 | .any(|p| l.origin_canonical().starts_with(p)) | ||
| 116 | }) | ||
| 117 | .collect() | ||
| 118 | } | ||
| 119 | |||
| 120 | pub fn collect_link_ids_by_base_paths( | ||
| 121 | depot: &Depot, | ||
| 122 | paths: impl IntoIterator<Item = impl AsRef<Path>>, | ||
| 123 | ) -> Vec<LinkID> { | ||
| 124 | collect_links_by_base_paths(depot, paths) | ||
| 125 | .into_iter() | ||
| 126 | .map(|l| l.id()) | ||
| 127 | .collect() | ||
| 128 | } | ||
| 129 | |||
| 130 | /// Returns a list of canonical paths to all the files in `dir`. This includes files in | ||
| 131 | /// subdirectories. | ||
| 132 | /// Fails if dir isnt a directory or if there is some other io error. | ||
| 133 | pub fn collect_files_in_dir(dir: impl Into<PathBuf>) -> anyhow::Result<Vec<PathBuf>> { | ||
| 134 | let mut paths = Vec::new(); | ||
| 135 | let mut dirs = VecDeque::new(); | ||
| 136 | dirs.push_back(dir.into()); | ||
| 137 | |||
| 138 | while let Some(dir) = dirs.pop_front() { | ||
| 139 | for entry in std::fs::read_dir(dir)? { | ||
| 140 | let entry = entry?; | ||
| 141 | let filetype = entry.file_type()?; | ||
| 142 | if filetype.is_dir() { | ||
| 143 | dirs.push_back(entry.path()); | ||
| 144 | } else { | ||
| 145 | paths.push(entry.path()); | ||
| 146 | } | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | Ok(paths) | ||
| 151 | } | ||
| 152 | |||
| 153 | /// Collects the result of std::fs::read_dir into two vecs | ||
| 154 | /// The first one contains all the directories and the second one all the files | ||
| 155 | pub fn collect_read_dir_split( | ||
| 156 | dir: impl AsRef<Path>, | ||
| 157 | ) -> anyhow::Result<(Vec<PathBuf>, Vec<PathBuf>)> { | ||
| 158 | Ok(std::fs::read_dir(dir)? | ||
| 159 | .filter_map(|e| e.ok()) | ||
| 160 | .map(|e| e.path()) | ||
| 161 | .partition(|p| p.is_dir())) | ||
| 162 | } | ||
| 163 | |||
| 164 | /// Checks if `path` is inside a git repository | ||
| 165 | pub fn path_is_in_git_repo(path: &Path) -> bool { | ||
| 166 | let mut path = if !path.is_absolute() { | ||
| 167 | dbg!(dotup::utils::weakly_canonical(path)) | ||
| 168 | } else { | ||
| 169 | path.to_owned() | ||
| 170 | }; | ||
| 171 | let recurse = path.pop(); | ||
| 172 | path.push(".git"); | ||
| 173 | if path.is_dir() { | ||
| 174 | return true; | ||
| 175 | } | ||
| 176 | if recurse { | ||
| 177 | path.pop(); | ||
| 178 | return path_is_in_git_repo(&path); | ||
| 179 | } else { | ||
| 180 | return false; | ||
| 181 | } | ||
| 182 | } | ||
diff --git a/dotup_cli/tests/cli.rs b/dotup_cli/tests/cli.rs deleted file mode 100644 index c836f63..0000000 --- a/dotup_cli/tests/cli.rs +++ /dev/null | |||
| @@ -1,145 +0,0 @@ | |||
| 1 | use assert_cmd::{assert::Assert, prelude::*}; | ||
| 2 | use dotup::ArchiveLink; | ||
| 3 | use std::{ | ||
| 4 | path::{Path, PathBuf}, | ||
| 5 | process::Command, | ||
| 6 | }; | ||
| 7 | use tempfile::TempDir; | ||
| 8 | |||
| 9 | const DEPOT_FILE_NAME: &str = "depot.toml"; | ||
| 10 | const BIN_NAME: &str = "dotup"; | ||
| 11 | |||
| 12 | fn create_empty_file(path: impl AsRef<Path>) { | ||
| 13 | let path = path.as_ref(); | ||
| 14 | if let Some(parent) = path.parent() { | ||
| 15 | std::fs::create_dir_all(parent).unwrap(); | ||
| 16 | } | ||
| 17 | std::fs::write(path, "").unwrap(); | ||
| 18 | } | ||
| 19 | |||
| 20 | fn prepare_command(dir: &TempDir) -> Command { | ||
| 21 | let mut cmd = Command::cargo_bin(BIN_NAME).unwrap(); | ||
| 22 | cmd.current_dir(dir.path()); | ||
| 23 | cmd | ||
| 24 | } | ||
| 25 | |||
| 26 | fn run_command(dir: &TempDir, cmd: &str) -> Assert { | ||
| 27 | let mut c = prepare_command(dir); | ||
| 28 | c.current_dir(dir.path()); | ||
| 29 | c.args(cmd.split_whitespace()); | ||
| 30 | c.assert() | ||
| 31 | } | ||
| 32 | |||
| 33 | fn prepare_dir() -> TempDir { | ||
| 34 | let dir = TempDir::new().unwrap(); | ||
| 35 | create_empty_file(dir.path().join("o1/file.txt")); | ||
| 36 | create_empty_file(dir.path().join("o1/dir/file.txt")); | ||
| 37 | create_empty_file(dir.path().join("o2/file1.txt")); | ||
| 38 | create_empty_file(dir.path().join("o2/file2.txt")); | ||
| 39 | dir | ||
| 40 | } | ||
| 41 | |||
| 42 | #[test] | ||
| 43 | fn test_cli_init() { | ||
| 44 | let dir = prepare_dir(); | ||
| 45 | let assert = run_command(&dir, "init"); | ||
| 46 | |||
| 47 | assert.success().code(0); | ||
| 48 | assert!(dir.path().join(DEPOT_FILE_NAME).is_file()); | ||
| 49 | } | ||
| 50 | |||
| 51 | #[test] | ||
| 52 | fn test_cli_link() { | ||
| 53 | let dir = prepare_dir(); | ||
| 54 | run_command(&dir, "init").success(); | ||
| 55 | |||
| 56 | let assert = run_command(&dir, "link o1 .config"); | ||
| 57 | assert.success().code(0); | ||
| 58 | |||
| 59 | let assert = run_command(&dir, "link --directory o2 .scripts"); | ||
| 60 | assert.success().code(0); | ||
| 61 | |||
| 62 | let archive = dotup::archive_read(dir.path().join(DEPOT_FILE_NAME)).unwrap(); | ||
| 63 | let link1 = ArchiveLink { | ||
| 64 | origin: PathBuf::from("o1/file.txt"), | ||
| 65 | destination: PathBuf::from(".config/file.txt"), | ||
| 66 | }; | ||
| 67 | let link2 = ArchiveLink { | ||
| 68 | origin: PathBuf::from("o1/dir/file.txt"), | ||
| 69 | destination: PathBuf::from(".config/dir/file.txt"), | ||
| 70 | }; | ||
| 71 | let link3 = ArchiveLink { | ||
| 72 | origin: PathBuf::from("o2"), | ||
| 73 | destination: PathBuf::from(".scripts"), | ||
| 74 | }; | ||
| 75 | |||
| 76 | assert!(archive.links.contains(&link1)); | ||
| 77 | assert!(archive.links.contains(&link2)); | ||
| 78 | assert!(archive.links.contains(&link3)); | ||
| 79 | } | ||
| 80 | |||
| 81 | #[test] | ||
| 82 | fn test_cli_install_uninstall_unlink() { | ||
| 83 | let dir = prepare_dir(); | ||
| 84 | run_command(&dir, "init").success(); | ||
| 85 | run_command(&dir, "link o1 .config").success(); | ||
| 86 | run_command(&dir, "link --directory o2 .scripts").success(); | ||
| 87 | |||
| 88 | let install_dir = TempDir::new().unwrap(); | ||
| 89 | let install_base = format!("{}", install_dir.path().display()); | ||
| 90 | run_command( | ||
| 91 | &dir, | ||
| 92 | &format!("--install-path {} install o1 o2", install_base), | ||
| 93 | ) | ||
| 94 | .success(); | ||
| 95 | |||
| 96 | assert_eq!( | ||
| 97 | std::fs::read_link(install_dir.path().join(".config/file.txt")).unwrap(), | ||
| 98 | dir.path().join("o1/file.txt") | ||
| 99 | ); | ||
| 100 | assert_eq!( | ||
| 101 | std::fs::read_link(install_dir.path().join(".config/dir/file.txt")).unwrap(), | ||
| 102 | dir.path().join("o1/dir/file.txt") | ||
| 103 | ); | ||
| 104 | assert_eq!( | ||
| 105 | std::fs::read_link(install_dir.path().join(".scripts")).unwrap(), | ||
| 106 | dir.path().join("o2") | ||
| 107 | ); | ||
| 108 | |||
| 109 | run_command( | ||
| 110 | &dir, | ||
| 111 | &format!("--install-path {} uninstall o1/file.txt", install_base), | ||
| 112 | ) | ||
| 113 | .success(); | ||
| 114 | assert!(!install_dir.path().join(".config/file.txt").exists()); | ||
| 115 | assert!(install_dir.path().join(".config/dir/file.txt").exists()); | ||
| 116 | assert!(install_dir.path().join(".scripts").exists()); | ||
| 117 | |||
| 118 | run_command( | ||
| 119 | &dir, | ||
| 120 | &format!("--install-path {} uninstall o1", install_base), | ||
| 121 | ) | ||
| 122 | .success(); | ||
| 123 | assert!(!install_dir.path().join(".config/file.txt").exists()); | ||
| 124 | assert!(!install_dir.path().join(".config/dir/file.txt").exists()); | ||
| 125 | assert!(install_dir.path().join(".scripts").exists()); | ||
| 126 | |||
| 127 | assert_eq!( | ||
| 128 | 3, | ||
| 129 | dotup::archive_read(dir.path().join(DEPOT_FILE_NAME)) | ||
| 130 | .unwrap() | ||
| 131 | .links | ||
| 132 | .len() | ||
| 133 | ); | ||
| 134 | |||
| 135 | run_command(&dir, &format!("unlink --uninstall {} o2", install_base)).success(); | ||
| 136 | assert!(!install_dir.path().join(".scripts").exists()); | ||
| 137 | |||
| 138 | assert_eq!( | ||
| 139 | 2, | ||
| 140 | dotup::archive_read(dir.path().join(DEPOT_FILE_NAME)) | ||
| 141 | .unwrap() | ||
| 142 | .links | ||
| 143 | .len() | ||
| 144 | ); | ||
| 145 | } | ||
