diff --git a/CITATION.cff b/CITATION.cff index e7f1a91..59c1423 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,7 +5,7 @@ authors: given-names: "Vince" orcid: "https://orcid.org/0000-0003-4510-1609" title: "SciDataFlow: A Tool for Improving the Flow of Data through Science" -version: 0.8.11 +version: 0.8.12 doi: http://dx.doi.org/10.1093/bioinformatics/btad754 date-released: 2024-01-05 url: "https://github.com/vsbuffalo/scidataflow/" diff --git a/Cargo.lock b/Cargo.lock index c23c08f..0a371a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2309,7 +2309,7 @@ dependencies = [ [[package]] name = "scidataflow" -version = "0.8.11" +version = "0.8.12" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 486dac7..4ed7ae1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scidataflow" -version = "0.8.11" +version = "0.8.12" edition = "2021" exclude = ["logo.png", "tests/test_data/**"] license = "MIT" diff --git a/src/lib.rs b/src/lib.rs index 70535bc..955f323 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod lib { pub mod progress; pub mod project; pub mod remote; + pub mod status; pub mod test_utilities; pub mod utils; } diff --git a/src/lib/project.rs b/src/lib/project.rs index 3402f94..6d316d9 100644 --- a/src/lib/project.rs +++ b/src/lib/project.rs @@ -21,6 +21,7 @@ use crate::lib::utils::{load_file, pluralize, print_status}; #[allow(unused_imports)] use crate::{print_info, print_warn}; +use super::status::StatusDisplayOptions; use super::utils::is_directory; const MANIFEST: &str = "data_manifest.yml"; @@ -309,13 +310,16 @@ impl Project { self.save() } - pub async fn status(&mut self, include_remotes: bool, all: bool) -> Result<()> { + pub async fn status(&mut self, display_options: &StatusDisplayOptions) -> Result<()> { // if include_remotes (e.g. --remotes) is set, we need to merge // in the remotes, so we authenticate first and then get them. let path_context = &canonicalize(self.path_context())?; - let status_rows = self.data.status(path_context, include_remotes).await?; - //let remotes: Option<_> = include_remotes.then(|| &self.data.remotes); - print_status(status_rows, Some(&self.data.remotes), all); + let status_rows = self + .data + .status(path_context, display_options.remotes) + .await?; + + print_status(status_rows, Some(&self.data.remotes), display_options); Ok(()) } diff --git a/src/lib/status.rs b/src/lib/status.rs new file mode 100644 index 0000000..cc4e368 --- /dev/null +++ b/src/lib/status.rs @@ -0,0 +1,47 @@ +use clap::Parser; + +/// Status display options +#[derive(Parser, Debug)] +pub struct StatusDisplayOptions { + /// Show remotes status (requires network). + #[arg(short = 'm', long)] + pub remotes: bool, + + /// Show statuses of all files, including those on remote(s) + /// but not in the manifest. + #[arg(short, long)] + pub all: bool, + + /// Don't print status with terminal colors. + #[arg(long)] + pub no_color: bool, + + /// A more terse summary, with --depth 2. + #[arg(short, long)] + pub short: bool, + + /// Depth to summarize over. + #[arg(short, long)] + depth: Option, + + /// Sort by time, showing the most recently modified files at + /// the top. + #[arg(short, long)] + pub time: bool, + + /// Reverse file order (if --time set, will show the files + /// with the oldest modification time at the top; otherwise + /// it will list files in reverse lexicographic order). + #[arg(short, long)] + pub reverse: bool, +} + +impl StatusDisplayOptions { + pub fn get_depth(&self) -> Option { + if self.short { + // --short includes + return Some(2); + } + self.depth + } +} diff --git a/src/lib/utils.rs b/src/lib/utils.rs index ebf9384..5ab7671 100644 --- a/src/lib/utils.rs +++ b/src/lib/utils.rs @@ -9,12 +9,17 @@ use std::collections::HashMap; use std::fs; use std::fs::File; use std::io::Read; +use std::ops::Add; use std::path::{Path, PathBuf}; use timeago::Formatter; use crate::lib::data::StatusEntry; use crate::lib::remote::Remote; +use super::data::LocalStatusCode; +use super::remote::RemoteStatusCode; +use super::status::StatusDisplayOptions; + pub const ISSUE_URL: &str = "https://github.com/vsbuffalo/scidataflow/issues"; pub fn load_file(path: &PathBuf) -> String { @@ -76,73 +81,120 @@ pub async fn compute_md5(file_path: &Path) -> Result> { let result = md5.compute(); Ok(Some(format!("{:x}", result))) } -/* - pub fn print_fixed_width(rows: HashMap>, nspaces: Option, indent: Option, color: bool) { - let indent = indent.unwrap_or(0); - let nspaces = nspaces.unwrap_or(6); - - let max_cols = rows.values() - .flat_map(|v| v.iter()) - .filter_map(|entry| { - match &entry.cols { - None => None, - Some(cols) => Some(cols.len()) - } - }) - .max() - .unwrap_or(0); - - let mut max_lengths = vec![0; max_cols]; - -// compute max lengths across all rows -for entry in rows.values().flat_map(|v| v.iter()) { -if let Some(cols) = &entry.cols { -for (i, col) in cols.iter().enumerate() { -max_lengths[i] = max_lengths[i].max(col.width()); -} -} -} -// print status table -let mut keys: Vec<&String> = rows.keys().collect(); -keys.sort(); -for (key, value) in &rows { -let pretty_key = if color { key.bold().to_string() } else { key.clone() }; -println!("[{}]", pretty_key); - -// Print the rows with the correct widths -for row in value { -let mut fixed_row = Vec::new(); -let tracked = &row.tracked; -let local_status = &row.local_status; -let remote_status = &row.remote_status; -if let Some(cols) = &row.cols { -for (i, col) in cols.iter().enumerate() { -// push a fixed-width column to vector -let fixed_col = format!("{:width$}", col, width = max_lengths[i]); -fixed_row.push(fixed_col); -} -} -let spacer = " ".repeat(nspaces); -let status_line = fixed_row.join(&spacer); -println!("{}{}", " ".repeat(indent), status_line); -} -println!(); + +/// Get the directory at the specified depth from a path string +fn get_dir_at_depth(dir: &str, filename: &str, depth: usize) -> String { + // Combine directory and filename into a full path + let full_path = if dir.is_empty() { + Path::new(filename).to_path_buf() + } else { + Path::new(dir).join(filename).to_path_buf() + }; + + // Get the parent directory of the full path + let parent_path = full_path.parent().unwrap_or(Path::new(".")); + + // Split the parent path into components + let components: Vec<_> = parent_path.components().collect(); + + if depth == 0 || components.is_empty() { + return ".".to_string(); + } + + // Take components up to the specified depth + let depth_path: PathBuf = components + .iter() + .take(depth.min(components.len())) + .collect(); + + if depth_path.as_os_str().is_empty() { + ".".to_string() + } else { + depth_path.to_string_lossy().to_string() + } } + +pub fn print_fixed_width_status_short( + rows: BTreeMap>, + options: &StatusDisplayOptions, +) { + let depth = options.get_depth(); + // If depth is provided, reorganize the data based on the specified depth + let grouped_rows: BTreeMap> = if let Some(depth) = depth { + let mut depth_grouped: BTreeMap> = BTreeMap::new(); + for (dir_entry, entries) in rows { + for entry in entries { + let base_dir = get_dir_at_depth(&dir_entry.path, &entry.name, depth); + depth_grouped + .entry(DirectoryEntry { + path: base_dir, + remote_name: dir_entry.remote_name.clone(), + }) + .or_default() + .push(entry); + } + } + depth_grouped + } else { + rows + }; + // dbg!(&grouped_rows); + + // Print status table + let mut dir_keys: Vec<&DirectoryEntry> = grouped_rows.keys().collect(); + dir_keys.sort(); + + for key in dir_keys { + let mut statuses = grouped_rows[key] + .iter() + .filter(|status| status.local_status.is_some() || options.all) + .cloned() + .collect::>(); + + if statuses.is_empty() { + continue; + } + + // TODO: we should consolidate code between this and + // print_fixed_width_status_short. + if !options.time { + // Sort the statuses by filename + statuses.sort_by(|a, b| a.name.cmp(&b.name)); + } else { + // Sort the statuses by timestamp + statuses.sort_by(|a, b| b.local_mod_time.cmp(&a.local_mod_time)); + } + + if options.reverse { + statuses.reverse(); + } + + let display_key = if key.path.is_empty() { + ".".to_string() + } else { + key.display().to_string() + }; + let prettier_key = if !options.no_color { + display_key.bold().to_string() + } else { + display_key.to_string() + }; + println!("[{}]", prettier_key); + let file_counts = + get_counts(&statuses, options.remotes).expect("Internal error: get_counts()."); + file_counts.pretty_print(options.short, !options.no_color); + println!(); + } } -*/ -// More specialized version of print_fixed_width() for statuses. -// Handles coloring, manual annotation, etc + pub fn print_fixed_width_status( - rows: BTreeMap>, + rows: BTreeMap>, nspaces: Option, indent: Option, - color: bool, - all: bool, + options: &StatusDisplayOptions, ) { - //debug!("rows: {:?}", rows); let indent = indent.unwrap_or(0); let nspaces = nspaces.unwrap_or(6); - let abbrev = Some(8); // get the max number of columns (in case ragged) @@ -159,26 +211,43 @@ pub fn print_fixed_width_status( for status in rows.values().flat_map(|v| v.iter()) { let cols = status.columns(abbrev); for (i, col) in cols.iter().enumerate() { - max_lengths[i] = max_lengths[i].max(col.len()); // Assuming col is a string + max_lengths[i] = max_lengths[i].max(col.len()); } } // print status table - let mut dir_keys: Vec<&String> = rows.keys().collect(); + let mut dir_keys: Vec<&DirectoryEntry> = rows.keys().collect(); dir_keys.sort(); + for key in dir_keys { - let statuses = &rows[key]; - let pretty_key = if key.is_empty() { "." } else { key }; - let prettier_key = if color { - pretty_key.bold().to_string() + let mut statuses = rows[key].clone(); + if !options.time { + // Sort the statuses by filename + statuses.sort_by(|a, b| a.name.cmp(&b.name)); + } else { + // Sort the statuses by timestamp + statuses.sort_by(|a, b| b.local_mod_time.cmp(&a.local_mod_time)); + } + + if options.reverse { + statuses.reverse(); + } + + let display_key = if key.path.is_empty() { + ".".to_string() + } else { + key.display().to_string() + }; + let prettier_key = if !options.no_color { + display_key.bold().to_string() } else { - pretty_key.to_string() + display_key.to_string() }; println!("[{}]", prettier_key); // Print the rows with the correct widths for status in statuses { - if status.local_status.is_none() && !all { + if status.local_status.is_none() && !options.all { // ignore things that aren't in the manifest, unless --all continue; } @@ -192,7 +261,7 @@ pub fn print_fixed_width_status( } let spacer = " ".repeat(nspaces); let line = fixed_row.join(&spacer); - let status_line = if color { + let status_line = if !options.no_color { status.color(line) } else { line.to_string() @@ -230,102 +299,341 @@ pub fn pluralize>(count: T, noun: &str) -> String { } } +#[derive(Debug, Default)] struct FileCounts { - local: u64, - remote: u64, - both: u64, - total: u64, - #[allow(dead_code)] - messy: u64, + local: u64, // Total local files + local_current: u64, // Files that match their manifest MD5 + local_modified: u64, // Files that differ from manifest MD5 + local_deleted: u64, // Files in manifest but not on disk + remote: u64, // Files only on remote + both: u64, // Files synced between local and remote + remote_different: u64, // Files where local matches manifest but differs from remote + local_messy: u64, // Files where local differs from both manifest and remote (MessyLocal) + total: u64, // Total number of files } -fn get_counts(rows: &BTreeMap>) -> Result { - let mut local = 0; - let mut remote = 0; - let mut both = 0; - let mut total = 0; - let mut messy = 0; - for files in rows.values() { - for file in files { - total += 1; - match (&file.local_status, &file.remote_status, &file.tracked) { - (None, None, _) => { - return Err(anyhow!("Internal Error: get_counts found a file with both local/remote set to None.")); - } - (None, Some(_), None) => { - remote += 1; - } - (Some(_), None, Some(false)) => { - local += 1; - } - (Some(_), None, None) => { - local += 1; +impl FileCounts { + pub fn pretty_print(&self, short: bool, color: bool) { + // Helper closure to conditionally apply color + let colorize = |text: String, color_fn: fn(String) -> ColoredString| -> String { + if color { + color_fn(text).to_string() + } else { + text + } + }; + + if short { + let mut parts = Vec::new(); + if self.local > 0 { + let mut local_str = format!("{} local", self.local); + local_str = colorize(local_str, |s| s.green()); + + let mut issues = Vec::new(); + if self.local_modified > 0 { + issues.push(format!( + "{} modified", + colorize(self.local_modified.to_string(), |s| s.red()) + )); } - (None, Some(_), Some(true)) => { - remote += 1; + if self.local_deleted > 0 { + issues.push(format!( + "{} deleted", + colorize(self.local_deleted.to_string(), |s| s.yellow()) + )); } - (None, Some(_), Some(false)) => { - local += 1; + if !issues.is_empty() { + local_str = format!("{} ({})", local_str, issues.join(", ")); } - (Some(_), Some(_), Some(true)) => { - both += 1; + parts.push(local_str); + } + if self.remote > 0 { + parts.push(format!( + "{} remote-only", + colorize(self.remote.to_string(), |s| s.yellow()) + )); + } + if self.both > 0 { + parts.push(format!( + "{} synced", + colorize(self.both.to_string(), |s| s.cyan()) + )); + } + if self.remote_different > 0 { + parts.push(format!( + "{} differ from remote", + colorize(self.remote_different.to_string(), |s| s.yellow()) + )); + } + if self.local_messy > 0 { + parts.push(format!( + "{} needs update", + colorize(self.local_messy.to_string(), |s| s.red()) + )); + } + if parts.is_empty() { + println!("no files"); + } else { + println!( + "{} ({})", + parts.join(", "), + colorize(format!("total: {}", self.total), |s| s.bold()) + ); + } + } else { + println!( + "{}", + colorize(format!(" {} files total", self.total), |s| s.bold()) + ); + if self.both > 0 { + println!( + " ✓ {} synced with remote", + colorize(self.both.to_string(), |s| s.cyan()) + ); + } + if self.local > 0 { + let mut status_parts = Vec::new(); + if self.local_current > 0 { + status_parts.push(format!( + "{} current", + colorize(self.local_current.to_string(), |s| s.green()) + )); } - (Some(_), Some(_), Some(false)) => { - messy += 1; + if self.local_modified > 0 { + status_parts.push(format!( + "{} modified", + colorize(self.local_modified.to_string(), |s| s.red()) + )); } - (Some(_), None, Some(true)) => { - remote += 1; + if self.local_deleted > 0 { + status_parts.push(format!( + "{} deleted", + colorize(self.local_deleted.to_string(), |s| s.yellow()) + )); } - (Some(_), Some(_), None) => { - messy += 1; + let status = if !status_parts.is_empty() { + format!(" ({})", status_parts.join(", ")) + } else { + String::from(" (all current)") + }; + println!( + " + {} local only{}", + colorize(self.local.to_string(), |s| s.green()), + status + ); + } + if self.remote > 0 { + println!( + " - {} remote only", + colorize(self.remote.to_string(), |s| s.yellow()) + ); + } + if self.remote_different > 0 { + println!( + " ! {} differ from remote", + colorize(self.remote_different.to_string(), |s| s.yellow()) + ); + } + if self.local_messy > 0 { + println!( + " ! {} need update", + colorize(self.local_messy.to_string(), |s| s.red()) + ); + } + } + } +} + +fn get_counts(files: &Vec, has_remote_info: bool) -> Result { + let mut counts = FileCounts::default(); + + for file in files { + counts.total += 1; + if !has_remote_info { + // When we don't have remote info, only track local status + if let Some(status) = &file.local_status { + match status { + LocalStatusCode::Current => { + counts.local += 1; + counts.local_current += 1; + } + LocalStatusCode::Modified => { + counts.local += 1; + counts.local_modified += 1; + } + LocalStatusCode::Deleted => { + counts.local_deleted += 1; + } + LocalStatusCode::Invalid => { + counts.local_messy += 1; + } } } + continue; + } + + match (&file.local_status, &file.remote_status, &file.tracked) { + (None, None, _) => { + return Err(anyhow!( + "Internal Error: get_counts found a file with both local/remote set to None." + )); + } + // Local files that match manifest but have no remote or aren't tracked + (Some(LocalStatusCode::Current), Some(RemoteStatusCode::NotExists), _) + | (Some(LocalStatusCode::Current), None, Some(false)) + | (Some(LocalStatusCode::Current), None, None) => { + counts.local += 1; + counts.local_current += 1; + } + // Modified local files that have no remote or aren't tracked + (Some(LocalStatusCode::Modified), Some(RemoteStatusCode::NotExists), _) + | (Some(LocalStatusCode::Modified), None, Some(false)) + | (Some(LocalStatusCode::Modified), None, None) => { + counts.local += 1; + counts.local_modified += 1; + } + // Deleted local files + (Some(LocalStatusCode::Deleted), _, _) => { + counts.local_deleted += 1; + } + // Files that are perfectly synced (local matches manifest matches remote) + (Some(LocalStatusCode::Current), Some(RemoteStatusCode::Current), Some(true)) => { + counts.both += 1; + } + // Local file matches manifest but differs from remote + (Some(LocalStatusCode::Current), Some(RemoteStatusCode::Different), Some(true)) => { + counts.remote_different += 1; + } + // Local file exists but doesn't match manifest or remote + (Some(_), Some(RemoteStatusCode::MessyLocal), _) => { + counts.local_messy += 1; + } + // Files that only exist on remote + (None, Some(RemoteStatusCode::Current), _) + | (None, Some(RemoteStatusCode::Exists), _) + | (None, Some(RemoteStatusCode::NoLocal), _) => { + counts.remote += 1; + } + // Remote file exists but we can't compare MD5s + (Some(LocalStatusCode::Current), Some(RemoteStatusCode::Exists), Some(true)) => { + counts.remote_different += 1; + } + // Everything else is counted as messy + _ => { + counts.local_messy += 1; + } + } + } + Ok(counts) +} + +impl Add for FileCounts { + type Output = FileCounts; + + fn add(self, other: FileCounts) -> FileCounts { + FileCounts { + local: self.local + other.local, + local_current: self.local_current + other.local_current, + local_modified: self.local_modified + other.local_modified, + local_deleted: self.local_deleted + other.local_deleted, + remote: self.remote + other.remote, + both: self.both + other.both, + remote_different: self.remote_different + other.remote_different, + local_messy: self.local_messy + other.local_messy, + total: self.total + other.total, + } + } +} + +fn get_counts_tree( + rows: &BTreeMap>, + has_remote_info: bool, +) -> Result { + let mut counts = FileCounts::default(); + for files in rows.values() { + counts = counts + get_counts(files, has_remote_info)?; + } + Ok(counts) +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct DirectoryEntry { + path: String, + remote_name: Option, +} + +impl DirectoryEntry { + fn display(&self) -> String { + if let Some(remote) = &self.remote_name { + format!("{} > {}", self.path, remote) + } else { + self.path.clone() } } - Ok(FileCounts { - local, - remote, - both, - total, - messy, - }) } pub fn print_status( rows: BTreeMap>, remote: Option<&HashMap>, - all: bool, + options: &StatusDisplayOptions, ) { println!("{}", "Project data status:".bold()); - let counts = get_counts(&rows).expect("Internal Error: get_counts() panicked."); - println!( - "{} local and tracked by a remote ({} only local, {} only remote), {} total.\n", - pluralize(counts.both, "file"), - pluralize(counts.local, "file"), - pluralize(counts.remote, "file"), - //pluralize(counts.messy as u64, "file"), - pluralize(counts.total, "file") - ); - - // this brings the remote name (if there is a corresponding remote) into - // the key, so the linked remote can be displayed in the status - let rows_by_dir: BTreeMap> = match remote { + + // Pass the remote info state to get_counts + let counts = + get_counts_tree(&rows, options.remotes).expect("Internal Error: get_counts() panicked."); + + // Adjust the status message based on whether we have remote info + if options.remotes { + println!( + "{} local and tracked by a remote ({} only local, {} only remote), {} total.\n", + pluralize(counts.both, "file"), + pluralize(counts.local, "file"), + pluralize(counts.remote, "file"), + pluralize(counts.total, "file") + ); + } else { + println!("{} local files total.\n", pluralize(counts.total, "file")); + } + + let rows_by_dir: BTreeMap> = match remote { Some(remote_map) => { let mut new_map = BTreeMap::new(); for (directory, statuses) in rows { - if let Some(remote) = remote_map.get(&directory) { - let new_key = format!("{} > {}", directory, remote.name()); - new_map.insert(new_key, statuses); + let entry = if let Some(remote) = remote_map.get(&directory) { + DirectoryEntry { + path: directory, + remote_name: Some(remote.name().to_string()), + } } else { - new_map.insert(directory, statuses); - } + DirectoryEntry { + path: directory, + remote_name: None, + } + }; + new_map.insert(entry, statuses); } new_map } - None => rows, + None => rows + .into_iter() + .map(|(dir, statuses)| { + ( + DirectoryEntry { + path: dir, + remote_name: None, + }, + statuses, + ) + }) + .collect(), }; - print_fixed_width_status(rows_by_dir, None, None, true, all); + if options.get_depth().is_some() { + print_fixed_width_status_short(rows_by_dir, options) + } else { + print_fixed_width_status(rows_by_dir, None, None, options); + } } pub fn format_bytes(size: u64) -> String { diff --git a/src/main.rs b/src/main.rs index 706ad93..02521f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use clap::{Parser, Subcommand}; use log::{debug, info, trace}; use scidataflow::lib::assets::GitHubRepo; use scidataflow::lib::download::Downloads; +use scidataflow::lib::status::StatusDisplayOptions; use tokio::runtime::Builder; use scidataflow::lib::project::Project; @@ -82,29 +83,29 @@ enum Commands { /// can be propagated to some APIs. Config { /// Your name. - #[arg(long)] + #[arg(short, long)] name: Option, // Your email. - #[arg(long)] + #[arg(short, long)] email: Option, // Your affiliation. - #[arg(long)] + #[arg(short, long)] affiliation: Option, }, /// Initialize a new project. Init { /// Project name (default: the name of the directory). - #[arg(long)] + #[arg(short, long)] name: Option, }, /// Download a file from a URL. Get { /// Download filename (default: based on URL). url: String, - #[arg(long)] + #[arg(short, long)] name: Option, /// Overwrite local files if they exit. - #[arg(long)] + #[arg(short, long)] overwrite: bool, }, /// Download a bunch of files from links stored in a file. @@ -112,24 +113,19 @@ enum Commands { /// A TSV or CSV file containing a column of URLs. Type inferred from suffix. filename: String, /// Which column contains links (default: first). - #[arg(long)] + #[arg(short, long)] column: Option, /// The TSV or CSV starts with a header (i.e. skip first line). - #[arg(long)] + #[arg(short, long)] header: bool, /// Overwrite local files if they exit. - #[arg(long)] + #[arg(short, long)] overwrite: bool, }, /// Show status of data. Status { - /// Show remotes status (requires network). - #[arg(long)] - remotes: bool, - - /// Show statuses of all files, including those on remote(s) but not in the manifest. - #[arg(long)] - all: bool, + #[clap(flatten)] + display_options: StatusDisplayOptions, }, /// Show file size statistics. Stats {}, @@ -139,7 +135,7 @@ enum Commands { #[arg(required = false)] filenames: Vec, /// Update all files presently registered in the manifest. - #[arg(long)] + #[arg(short, long)] all: bool, }, /// Remove a file from the manifest @@ -151,10 +147,10 @@ enum Commands { /// Retrieve a SciDataFlow Asset Asset { /// A GitHub link - #[arg(long)] + #[arg(short, long)] github: Option, /// A URL to a data_manifest.yml file - #[arg(long)] + #[arg(short, long)] url: Option, /// A SciDataFlow Asset name asset: Option, @@ -169,13 +165,13 @@ enum Commands { key: String, /// Project name for remote (default: the metadata title in the data /// manifest, or if that's not set, the directory name). - #[arg(long)] + #[arg(short, long)] name: Option, /// Don't initialize remote, only add to manifest. This will retrieve /// the remote information (i.e. the FigShare Article ID or Zenodo /// Depository ID) to add to the manifest. Requires network. - #[arg(long)] + #[arg(short, long)] link_only: bool, }, /// No longer keep track of this file on the remote. @@ -193,7 +189,7 @@ enum Commands { /// Push all tracked files to remote. Push { /// Overwrite remote files if they exit. - #[arg(long)] + #[arg(short, long)] overwrite: bool, }, /// Pull in all tracked files from the remote. If --urls is set, @@ -206,15 +202,15 @@ enum Commands { /// increase disk usage. Pull { /// Overwrite local files if they exit. - #[arg(long)] + #[arg(short, long)] overwrite: bool, /// Pull in files from the URLs, not remotes. - #[arg(long)] + #[arg(short, long)] urls: bool, /// Pull in files from remotes and URLs. - #[arg(long)] + #[arg(short, long)] all: bool, // multiple optional directories //directories: Vec, @@ -222,10 +218,10 @@ enum Commands { /// Change the project metadata. Metadata { /// The project name. - #[arg(long)] + #[arg(short, long)] title: Option, // A description of the project. - #[arg(long)] + #[arg(short, long)] description: Option, }, } @@ -289,9 +285,9 @@ async fn run() -> Result<()> { proj.bulk(filename, *column, *header, *overwrite).await } Some(Commands::Init { name }) => Project::init(name.clone()), - Some(Commands::Status { remotes, all }) => { + Some(Commands::Status { display_options }) => { let mut proj = Project::new()?; - proj.status(*remotes, *all).await + proj.status(display_options).await } Some(Commands::Stats {}) => { //let proj = Project::new()?;