summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock16
-rw-r--r--Cargo.toml1
-rw-r--r--src/git.rs40
-rw-r--r--src/lib.rs23
-rw-r--r--src/parser.rs9
-rw-r--r--tests/cli.rs64
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
diff --git a/Cargo.lock b/Cargo.lock
index 8d56231..7084642 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index 8bcad68..3e58893 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/src/git.rs b/src/git.rs
index c07615d..d92b5a0 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -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);
diff --git a/src/lib.rs b/src/lib.rs
index 32d5770..809c62d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(())
}