aboutsummaryrefslogtreecommitdiff
path: root/dotup_cli
diff options
context:
space:
mode:
Diffstat (limited to 'dotup_cli')
-rw-r--r--dotup_cli/Cargo.toml29
-rw-r--r--dotup_cli/src/commands/init.rs22
-rw-r--r--dotup_cli/src/commands/install.rs25
-rw-r--r--dotup_cli/src/commands/link.rs134
-rw-r--r--dotup_cli/src/commands/mod.rs11
-rw-r--r--dotup_cli/src/commands/mv.rs53
-rw-r--r--dotup_cli/src/commands/status.rs175
-rw-r--r--dotup_cli/src/commands/uninstall.rs26
-rw-r--r--dotup_cli/src/commands/unlink.rs43
-rw-r--r--dotup_cli/src/config.rs10
-rw-r--r--dotup_cli/src/main.rs111
-rw-r--r--dotup_cli/src/utils.rs182
-rw-r--r--dotup_cli/tests/cli.rs145
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]
2edition = "2018"
3name = "dotup_cli"
4version = "0.1.0"
5
6[[bin]]
7name = "dotup"
8path = "src/main.rs"
9doc = false
10
11[dependencies]
12anyhow = "1.0"
13log = "0.4"
14ansi_term = "0.12.1"
15
16[dependencies.clap]
17features = ["derive"]
18version = "3.0.0-rc.7"
19
20[dependencies.dotup]
21path = "../dotup"
22
23[dependencies.flexi_logger]
24features = ["colors"]
25version = "0.22"
26
27[dev-dependencies]
28tempfile = "3.2"
29assert_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 @@
1use 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)]
8pub struct Opts {}
9
10pub 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 @@
1use std::path::PathBuf;
2
3use 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)]
10pub struct Opts {
11 /// The files/directories to install.
12 #[clap(min_values = 1, default_value = ".")]
13 paths: Vec<PathBuf>,
14}
15
16pub 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 @@
1use std::{
2 fs::{DirEntry, Metadata},
3 path::{Path, PathBuf},
4};
5
6use 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)]
14pub 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
26pub 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
73fn 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
89fn 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
122fn 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 @@
1pub mod init;
2pub mod install;
3pub mod link;
4pub mod mv;
5pub mod status;
6pub mod uninstall;
7pub mod unlink;
8
9mod 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 @@
1use std::path::{Path, PathBuf};
2
3use 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)]
10pub struct Opts {
11 /// The files/directories to move
12 #[clap(min_values = 2)]
13 paths: Vec<PathBuf>,
14}
15
16pub 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
47fn 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 @@
1use std::path::{Path, PathBuf};
2
3use ansi_term::Colour;
4use clap::Parser;
5use dotup::{Depot, Link};
6
7use 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)]
15pub 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)]
26struct State {
27 max_depth: u32,
28 install_path: PathBuf,
29}
30
31pub 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
83fn 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
95fn 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
125fn 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
134fn 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
161fn 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
171fn 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 @@
1use std::path::PathBuf;
2
3use 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)]
11pub struct Opts {
12 /// The files/directories to uninstall.
13 #[clap(min_values = 1, default_value = ".")]
14 paths: Vec<PathBuf>,
15}
16
17pub 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 @@
1use std::{
2 fs::{DirEntry, Metadata},
3 path::{Path, PathBuf},
4};
5
6use 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)]
14pub 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
24pub 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 @@
1use std::path::PathBuf;
2
3#[derive(Debug)]
4pub struct Config {
5 pub archive_path: PathBuf,
6 pub install_path: PathBuf,
7 pub working_path: PathBuf,
8}
9
10impl 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
3pub mod commands;
4pub mod config;
5pub mod utils;
6
7pub use config::Config;
8
9pub 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
15use clap::{AppSettings, Parser};
16use flexi_logger::Logger;
17use std::{
18 collections::HashMap,
19 iter::FromIterator,
20 path::{Path, PathBuf},
21};
22
23use prelude::*;
24
25#[derive(Parser)]
26struct 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)]
58enum 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
68fn 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 @@
1use std::{
2 collections::VecDeque,
3 path::{Path, PathBuf},
4};
5
6use crate::prelude::*;
7
8const DEFAULT_DEPOT_NAME: &str = "depot.toml";
9
10pub 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
20pub 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
35pub 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
51pub 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
67pub 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
82pub 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
101pub 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
120pub 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.
133pub 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
155pub 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
165pub 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 @@
1use assert_cmd::{assert::Assert, prelude::*};
2use dotup::ArchiveLink;
3use std::{
4 path::{Path, PathBuf},
5 process::Command,
6};
7use tempfile::TempDir;
8
9const DEPOT_FILE_NAME: &str = "depot.toml";
10const BIN_NAME: &str = "dotup";
11
12fn 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
20fn 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
26fn 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
33fn 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]
43fn 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]
52fn 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]
82fn 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}