diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | Cargo.lock | 16 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/git.rs | 40 | ||||
-rw-r--r-- | src/lib.rs | 23 | ||||
-rw-r--r-- | src/parser.rs | 9 | ||||
-rw-r--r-- | tests/cli.rs | 64 |
7 files changed, 130 insertions, 24 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe5758..9de81c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 default, so that the CI environment can skip tests that require running docker. - New command "delete", for deleting an existing repo. +- New "--verbose" option to the list command, which also lists the repo size. ## [0.1.1] - 2023-05-10 @@ -269,6 +269,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] name = "idna" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -346,6 +355,12 @@ dependencies = [ ] [[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] name = "libz-sys" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -682,6 +697,7 @@ dependencies = [ "comfy-table", "get-port", "git2", + "humansize", "nix 0.26.2", "once_cell", "predicates", @@ -17,6 +17,7 @@ categories = ["command-line-utilities"] clap = { version = "4.1.8", features = ["derive"] } comfy-table = "6.1.4" git2 = { version = "0.16.1", default-features = false, features = ["vendored-libgit2"] } +humansize = "2.1.3" nix = { version = "0.26.2", default-features = false, features = ["user"] } rustyline = { version = "11.0.0", default-features = false } shlex = "1.1.0" @@ -109,6 +109,33 @@ pub struct RepoMetadata { pub description: String, } +pub struct VerboseRepoMetadata { + pub path: PathBuf, + pub description: String, + pub size: u64, +} + +fn get_size(path: impl AsRef<Path>) -> Result<u64, ShackleError> { + let path_metadata = path.as_ref().symlink_metadata()?; + + if path_metadata.is_dir() { + let mut size_in_bytes = path_metadata.len(); + for entry in path.as_ref().read_dir()? { + let entry = entry?; + let entry_metadata = entry.metadata()?; + + if entry_metadata.is_dir() { + size_in_bytes += get_size(entry.path())?; + } else { + size_in_bytes += entry_metadata.len(); + } + } + Ok(size_in_bytes) + } else { + Ok(path_metadata.len()) + } +} + pub fn list() -> Result<Vec<RepoMetadata>, ShackleError> { fn add_from_dir( collection_dir: &Path, @@ -163,6 +190,19 @@ pub fn list() -> Result<Vec<RepoMetadata>, ShackleError> { Ok(results) } +pub fn list_verbose() -> Result<Vec<VerboseRepoMetadata>, ShackleError> { + list()? + .into_iter() + .map(|meta| { + get_size(&meta.path).map(|size| VerboseRepoMetadata { + path: meta.path, + description: meta.description, + size, + }) + }) + .collect() +} + pub fn set_description(directory: &Path, description: &str) -> Result<(), ShackleError> { if !is_valid_git_repo_path(directory)? { return Err(ShackleError::InvalidDirectory); @@ -3,6 +3,7 @@ mod parser; pub mod user_info; use comfy_table::Table; +use humansize::{format_size, BINARY}; use parser::*; use rustyline::error::ReadlineError; use std::{io, ops::ControlFlow}; @@ -16,12 +17,24 @@ pub fn run_command(user_input: &str) -> Result<ControlFlow<(), ()>, ShackleError Ok(ShackleCommand::Exit) => { return Ok(ControlFlow::Break(())); } - Ok(ShackleCommand::List) => { + Ok(ShackleCommand::List(ListArgs { verbose })) => { let mut table = Table::new(); - table.set_header(vec!["path", "description"]); - let listing = git::list()?; - for meta in listing { - table.add_row(vec![meta.path.display().to_string(), meta.description]); + if !verbose { + table.set_header(vec!["path", "description"]); + let listing = git::list()?; + for meta in listing { + table.add_row(vec![meta.path.display().to_string(), meta.description]); + } + } else { + table.set_header(vec!["path", "description", "size"]); + let listing = git::list_verbose()?; + for meta in listing { + table.add_row(vec![ + meta.path.display().to_string(), + meta.description, + format_size(meta.size, BINARY), + ]); + } } println!("{table}"); diff --git a/src/parser.rs b/src/parser.rs index 30eb1e0..b429572 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -11,7 +11,7 @@ pub enum ShackleCommand { /// Create a new repository Init(InitArgs), /// List all repositories available - List, + List(ListArgs), /// Sets the description of a repository, as shown in the CLI listing and web interfaces SetDescription(SetDescriptionArgs), /// Sets the main branch of the repository @@ -42,6 +42,13 @@ pub struct InitArgs { } #[derive(Parser, Clone, Debug, PartialEq, Eq)] +pub struct ListArgs { + /// List extra metadata, like the repo's size on disk + #[arg(short, long)] + pub verbose: bool, +} + +#[derive(Parser, Clone, Debug, PartialEq, Eq)] pub struct SetDescriptionArgs { /// The full relative path of the repository, for example git/shuckie/repo.git #[arg(value_parser = RelativePathParser)] diff --git a/tests/cli.rs b/tests/cli.rs index 12e701f..dbe99b2 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -254,7 +254,7 @@ fn allows_single_quotes_and_spaces_inside_double_quotes() -> Result<()> { const DEFAULT_DESCRIPTION: &str = "Unnamed repository; edit this file 'description' to name the repository."; -fn expect_list_table(c: &mut TestContext, repos: &[(String, String)]) -> Result<()> { +fn expect_list_table(c: &mut TestContext, repos: &[(&str, &str)]) -> Result<()> { c.p.send_line("list")?; c.p.exp_regex(r"\+-+\+-+\+")?; c.p.exp_regex(r"\| path +\| description +\|")?; @@ -271,6 +271,25 @@ fn expect_list_table(c: &mut TestContext, repos: &[(String, String)]) -> Result< Ok(()) } +fn expect_list_table_verbose(c: &mut TestContext, repos: &[(&str, &str)]) -> Result<()> { + c.p.send_line("list --verbose")?; + c.p.exp_regex(r"\+-+\+-+\+-+\+")?; + c.p.exp_regex(r"\| path +\| description +\| size +\|")?; + c.p.exp_regex(r"\+=+\+")?; + for (path, description) in repos { + c.p.exp_string("| ")?; + c.p.exp_string(path)?; + c.p.exp_regex(r" +\| ")?; + c.p.exp_string(&description)?; + c.p.exp_regex(r" +\|")?; + c.p.exp_regex(r"\d+ (MiB|KiB|B)")?; + c.p.exp_regex(r" +\|")?; + } + c.p.exp_regex(r"\+-+\+-+\+-+\+")?; + c.expect_prompt()?; + Ok(()) +} + #[test] fn list_can_print_an_empty_list() -> Result<()> { let mut c = spawn_interactive_process()?; @@ -287,10 +306,7 @@ fn list_can_print_a_list_of_personal_repos_with_descriptions() -> Result<()> { expect_list_table( &mut c, - &[( - personal_repo_path(REPO_NAME), - DEFAULT_DESCRIPTION.to_owned(), - )], + &[(&personal_repo_path(REPO_NAME), DEFAULT_DESCRIPTION)], )?; Ok(()) @@ -309,14 +325,29 @@ fn list_can_print_a_list_of_all_repos_with_descriptions() -> Result<()> { expect_list_table( &mut c, &[ - ( - personal_repo_path(REPO_NAME), - DEFAULT_DESCRIPTION.to_owned(), - ), - ( - group_repo_path(&group, REPO_NAME_2), - DEFAULT_DESCRIPTION.to_owned(), - ), + (&personal_repo_path(REPO_NAME), DEFAULT_DESCRIPTION), + (&group_repo_path(&group, REPO_NAME_2), DEFAULT_DESCRIPTION), + ], + )?; + + Ok(()) +} + +#[test] +fn list_can_print_a_verbose_list_of_all_repos() -> Result<()> { + let mut c = spawn_interactive_process()?; + c.p.send_line(&format!("init {}", REPO_NAME))?; + c.expect_successful_init_message(&personal_repo_path(REPO_NAME))?; + + let group = arbitrary_user_group(); + c.p.send_line(&format!("init --group {} {}", group, REPO_NAME_2))?; + c.expect_successful_init_message(&group_repo_path(&group, REPO_NAME_2))?; + + expect_list_table_verbose( + &mut c, + &[ + (&personal_repo_path(REPO_NAME), DEFAULT_DESCRIPTION), + (&group_repo_path(&group, REPO_NAME_2), DEFAULT_DESCRIPTION), ], )?; @@ -330,10 +361,7 @@ fn can_set_the_description_on_a_repo_during_init() -> Result<()> { c.p.send_line(&format!("init --description \"{description}\" {REPO_NAME}"))?; c.expect_successful_init_message(&personal_repo_path(REPO_NAME))?; - expect_list_table( - &mut c, - &[(personal_repo_path(REPO_NAME), description.to_owned())], - )?; + expect_list_table(&mut c, &[(&personal_repo_path(REPO_NAME), description)])?; Ok(()) } @@ -350,7 +378,7 @@ fn can_change_the_description_on_a_repo() -> Result<()> { ))?; c.p.exp_string("Successfully updated description")?; c.expect_prompt()?; - expect_list_table(&mut c, &[(repo_path, description.to_owned())])?; + expect_list_table(&mut c, &[(&repo_path, description)])?; Ok(()) } |