diff options
author | Pascal H <hpwxf@haveneer.com> | 2023-04-30 18:26:15 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-01 00:26:15 +0800 |
commit | 2fe3fcdd3564836962eab8ba6b1444996fe24e1e (patch) | |
tree | fad504cec71bd00aa6b1a5e6c34fe042e4a1cfec /src | |
parent | 6840c01905a7601b0d7f0d4dc2e08c5a5a581321 (diff) |
Git integration (#822)
Diffstat (limited to 'src')
-rw-r--r-- | src/app.rs | 49 | ||||
-rw-r--r-- | src/color.rs | 7 | ||||
-rw-r--r-- | src/config_file.rs | 4 | ||||
-rw-r--r-- | src/core.rs | 32 | ||||
-rw-r--r-- | src/display.rs | 180 | ||||
-rw-r--r-- | src/flags.rs | 1 | ||||
-rw-r--r-- | src/flags/blocks.rs | 59 | ||||
-rw-r--r-- | src/flags/sorting.rs | 29 | ||||
-rw-r--r-- | src/git.rs | 460 | ||||
-rw-r--r-- | src/git_theme.rs | 31 | ||||
-rw-r--r-- | src/icon.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/meta/git_file_status.rs | 67 | ||||
-rw-r--r-- | src/meta/mod.rs | 14 | ||||
-rw-r--r-- | src/meta/name.rs | 2 | ||||
-rw-r--r-- | src/sort.rs | 5 | ||||
-rw-r--r-- | src/theme.rs | 3 | ||||
-rw-r--r-- | src/theme/color.rs | 19 | ||||
-rw-r--r-- | src/theme/git.rs | 35 |
19 files changed, 968 insertions, 33 deletions
@@ -96,6 +96,10 @@ pub struct Cli { #[arg(short = 'X', long)] pub extensionsort: bool, + /// Sort by git status + #[arg(short = 'G', long)] + pub gitsort: bool, + /// Natural sort of (version) numbers within text #[arg(short = 'v', long)] pub versionsort: bool, @@ -104,13 +108,13 @@ pub struct Cli { #[arg( long, value_name = "TYPE", - value_parser = ["size", "time", "version", "extension", "none"], - overrides_with_all = ["timesort", "sizesort", "extensionsort", "versionsort", "no_sort"] + value_parser = ["size", "time", "version", "extension", "git", "none"], + overrides_with_all = ["timesort", "sizesort", "extensionsort", "versionsort", "gitsort", "no_sort"] )] pub sort: Option<String>, /// Do not sort. List entries in directory order - #[arg(short = 'U', long, overrides_with_all = ["timesort", "sizesort", "extensionsort", "versionsort", "sort"])] + #[arg(short = 'U', long, overrides_with_all = ["timesort", "sizesort", "extensionsort", "versionsort", "gitsort", "sort"])] pub no_sort: bool, /// Reverse the order of the sort @@ -127,9 +131,9 @@ pub struct Cli { /// Specify the blocks that will be displayed and in what order #[arg( - long, - value_delimiter = ',', - value_parser = ["permission", "user", "group", "context", "size", "date", "name", "inode", "links"], + long, + value_delimiter = ',', + value_parser = ["permission", "user", "group", "context", "size", "date", "name", "inode", "links", "git"], )] pub blocks: Vec<String>, @@ -150,6 +154,11 @@ pub struct Cli { #[arg(short, long)] pub inode: bool, + /// Show git status on file and directory" + /// Only when used with --long option + #[arg(short, long)] + pub git: bool, + /// When showing file information for a symbolic link, /// show information for the file the link references rather than for the link itself #[arg(short = 'L', long)] @@ -196,15 +205,15 @@ pub fn validate_time_format(formatter: &str) -> Result<String, String> { Some('f') => (), Some(n @ ('3' | '6' | '9')) => match chars.next() { Some('f') => (), - Some(c) => return Err(format!("invalid format specifier: %.{}{}", n, c)), + Some(c) => return Err(format!("invalid format specifier: %.{n}{c}")), None => return Err("missing format specifier".to_owned()), }, - Some(c) => return Err(format!("invalid format specifier: %.{}", c)), + Some(c) => return Err(format!("invalid format specifier: %.{c}")), None => return Err("missing format specifier".to_owned()), }, Some(n @ (':' | '#')) => match chars.next() { Some('z') => (), - Some(c) => return Err(format!("invalid format specifier: %{}{}", n, c)), + Some(c) => return Err(format!("invalid format specifier: %{n}{c}")), None => return Err("missing format specifier".to_owned()), }, Some(n @ ('-' | '_' | '0')) => match chars.next() { @@ -212,7 +221,7 @@ pub fn validate_time_format(formatter: &str) -> Result<String, String> { 'C' | 'd' | 'e' | 'f' | 'G' | 'g' | 'H' | 'I' | 'j' | 'k' | 'l' | 'M' | 'm' | 'S' | 's' | 'U' | 'u' | 'V' | 'W' | 'w' | 'Y' | 'y', ) => (), - Some(c) => return Err(format!("invalid format specifier: %{}{}", n, c)), + Some(c) => return Err(format!("invalid format specifier: %{n}{c}")), None => return Err("missing format specifier".to_owned()), }, Some( @@ -223,10 +232,10 @@ pub fn validate_time_format(formatter: &str) -> Result<String, String> { ) => (), Some(n @ ('3' | '6' | '9')) => match chars.next() { Some('f') => (), - Some(c) => return Err(format!("invalid format specifier: %{}{}", n, c)), + Some(c) => return Err(format!("invalid format specifier: %{n}{c}")), None => return Err("missing format specifier".to_owned()), }, - Some(c) => return Err(format!("invalid format specifier: %{}", c)), + Some(c) => return Err(format!("invalid format specifier: %{c}")), None => return Err("missing format specifier".to_owned()), }, None => break, @@ -235,3 +244,19 @@ pub fn validate_time_format(formatter: &str) -> Result<String, String> { } Ok(formatter.to_owned()) } + +// Wrapper for value_parser to simply remove non supported option (mainly git flag) +// required since value_parser requires impl Into<ValueParser> that Vec do not support +// should be located here, since this file is included by build.rs +struct LabelFilter<Filter: Fn(&'static str) -> bool, const C: usize>([&'static str; C], Filter); + +impl<Filter: Fn(&'static str) -> bool, const C: usize> From<LabelFilter<Filter, C>> + for clap::builder::ValueParser +{ + fn from(label_filter: LabelFilter<Filter, C>) -> Self { + let filter = label_filter.1; + let values = label_filter.0.into_iter().filter(|x| filter(x)); + let inner = clap::builder::PossibleValuesParser::from(values); + Self::from(inner) + } +} diff --git a/src/color.rs b/src/color.rs index c054d5a..ed69849 100644 --- a/src/color.rs +++ b/src/color.rs @@ -4,6 +4,7 @@ use lscolors::{Indicator, LsColors}; use std::path::Path; pub use crate::flags::color::ThemeOption; +use crate::git::GitStatus; use crate::theme::{color::ColorTheme, Theme}; #[allow(dead_code)] @@ -61,6 +62,10 @@ pub enum Elem { }, TreeEdge, + + GitStatus { + status: GitStatus, + }, } impl Elem { @@ -121,6 +126,7 @@ impl Elem { Elem::TreeEdge => theme.tree_edge, Elem::Links { valid: false } => theme.links.invalid, Elem::Links { valid: true } => theme.links.valid, + Elem::GitStatus { .. } => theme.git_status.default, } } } @@ -389,6 +395,7 @@ mod elem { invalid: Color::AnsiValue(245), // Grey }, tree_edge: Color::AnsiValue(245), // Grey + git_status: Default::default(), } } diff --git a/src/config_file.rs b/src/config_file.rs index d2c6d7c..c9392c8 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -203,7 +203,7 @@ classic: false # == Blocks == # This specifies the columns and their order when using the long and the tree # layout. -# Possible values: permission, user, group, context, size, date, name, inode +# Possible values: permission, user, group, context, size, date, name, inode, git blocks: - permission - user @@ -388,7 +388,7 @@ mod tests { total_size: Some(false), symlink_arrow: Some("⇒".into()), hyperlink: Some(HyperlinkOption::Never), - header: None + header: None, }, c ); diff --git a/src/core.rs b/src/core.rs index 9377c07..b0d0efc 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,7 +1,9 @@ use crate::color::Colors; use crate::display; use crate::flags::{ColorOption, Display, Flags, HyperlinkOption, Layout, SortOrder, ThemeOption}; +use crate::git::GitCache; use crate::icon::Icons; + use crate::meta::Meta; use crate::{print_error, print_output, sort, ExitCode}; use std::path::PathBuf; @@ -11,6 +13,8 @@ use std::io; #[cfg(not(target_os = "windows"))] use std::os::unix::io::AsRawFd; +use crate::flags::blocks::Block; +use crate::git_theme::GitTheme; #[cfg(target_os = "windows")] use terminal_size::terminal_size; @@ -18,6 +22,7 @@ pub struct Core { flags: Flags, icons: Icons, colors: Colors, + git_theme: GitTheme, sorters: Vec<(SortOrder, sort::SortFn)>, } @@ -75,6 +80,7 @@ impl Core { flags, colors: Colors::new(color_theme), icons: Icons::new(tty_available, icon_when, icon_theme, icon_separator), + git_theme: GitTheme::new(), sorters, } } @@ -106,12 +112,19 @@ impl Core { } }; + let cache = if self.flags.blocks.0.contains(&Block::GitStatus) { + Some(GitCache::new(&path)) + } else { + None + }; + let recurse = self.flags.layout == Layout::Tree || self.flags.display != Display::DirectoryOnly; if recurse { - match meta.recurse_into(depth, &self.flags) { + match meta.recurse_into(depth, &self.flags, cache.as_ref()) { Ok((content, path_exit_code)) => { meta.content = content; + meta.git_status = cache.and_then(|cache| cache.get(&meta.path, true)); meta_list.push(meta); exit_code.set_if_greater(path_exit_code); } @@ -122,6 +135,7 @@ impl Core { } }; } else { + meta.git_status = cache.and_then(|cache| cache.get(&meta.path, true)); meta_list.push(meta); }; } @@ -147,9 +161,21 @@ impl Core { fn display(&self, metas: &[Meta]) { let output = if self.flags.layout == Layout::Tree { - display::tree(metas, &self.flags, &self.colors, &self.icons) + display::tree( + metas, + &self.flags, + &self.colors, + &self.icons, + &self.git_theme, + ) } else { - display::grid(metas, &self.flags, &self.colors, &self.icons) + display::grid( + metas, + &self.flags, + &self.colors, + &self.icons, + &self.git_theme, + ) }; print_output!("{}", output); diff --git a/src/display.rs b/src/display.rs index 8e7c251..6fea6e9 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,5 +1,7 @@ use crate::color::{Colors, Elem}; -use crate::flags::{Block, Display, Flags, HyperlinkOption, Layout}; +use crate::flags::blocks::Block; +use crate::flags::{Display, Flags, HyperlinkOption, Layout}; +use crate::git_theme::GitTheme; use crate::icon::Icons; use crate::meta::name::DisplayOption; use crate::meta::{FileType, Meta}; @@ -13,7 +15,13 @@ const LINE: &str = "\u{2502} "; // "│ " const CORNER: &str = "\u{2514}\u{2500}\u{2500}"; // "└──" const BLANK: &str = " "; -pub fn grid(metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons) -> String { +pub fn grid( + metas: &[Meta], + flags: &Flags, + colors: &Colors, + icons: &Icons, + git_theme: &GitTheme, +) -> String { let term_width = terminal_size().map(|(w, _)| w.0 as usize); inner_display_grid( @@ -22,12 +30,19 @@ pub fn grid(metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons) -> St flags, colors, icons, + git_theme, 0, term_width, ) } -pub fn tree(metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons) -> String { +pub fn tree( + metas: &[Meta], + flags: &Flags, + colors: &Colors, + icons: &Icons, + git_theme: &GitTheme, +) -> String { let mut grid = Grid::new(GridOptions { filling: Filling::Spaces(1), direction: Direction::LeftToRight, @@ -42,19 +57,30 @@ pub fn tree(metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons) -> St } } - for cell in inner_display_tree(metas, flags, colors, icons, (0, ""), &padding_rules, index) { + for cell in inner_display_tree( + metas, + flags, + colors, + icons, + git_theme, + (0, ""), + &padding_rules, + index, + ) { grid.add(cell); } grid.fit_into_columns(flags.blocks.0.len()).to_string() } +#[allow(clippy::too_many_arguments)] // should wrap flags, colors, icons, git_theme into one struct fn inner_display_grid( display_option: &DisplayOption, metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons, + git_theme: &GitTheme, depth: usize, term_width: Option<usize>, ) -> String { @@ -93,6 +119,7 @@ fn inner_display_grid( meta, colors, icons, + git_theme, flags, display_option, &padding_rules, @@ -152,6 +179,7 @@ fn inner_display_grid( flags, colors, icons, + git_theme, depth + 1, term_width, ); @@ -192,11 +220,13 @@ fn add_header(flags: &Flags, cells: &[Cell], grid: &mut Grid) { } } +#[allow(clippy::too_many_arguments)] fn inner_display_tree( metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons, + git_theme: &GitTheme, tree_depth_prefix: (usize, &str), padding_rules: &HashMap<Block, usize>, tree_index: usize, @@ -220,6 +250,7 @@ fn inner_display_tree( meta, colors, icons, + git_theme, flags, &DisplayOption::FileName, padding_rules, @@ -248,6 +279,7 @@ fn inner_display_tree( flags, colors, icons, + git_theme, (tree_depth_prefix.0 + 1, &new_prefix), padding_rules, tree_index, @@ -279,10 +311,12 @@ fn display_folder_path(meta: &Meta) -> String { format!("\n{}:\n", meta.path.to_string_lossy()) } +#[allow(clippy::too_many_arguments)] fn get_output( meta: &Meta, colors: &Colors, icons: &Icons, + git_theme: &GitTheme, flags: &Flags, display_option: &DisplayOption, padding_rules: &HashMap<Block, usize>, @@ -366,6 +400,11 @@ fn get_output( block_vec.push(meta.symlink.render(colors, flags)) } } + Block::GitStatus => { + if let Some(_s) = &meta.git_status { + block_vec.push(_s.render(colors, git_theme)); + } + } }; strings.push( block_vec @@ -457,6 +496,7 @@ mod tests { use assert_fs::prelude::*; use clap::Parser; use std::path::Path; + use tempfile::tempdir; #[test] fn test_display_get_visible_width_without_icons() { @@ -559,8 +599,7 @@ mod tests { // check if the color is present. assert!( output.starts_with("\u{1b}[38;5;"), - "{:?} should start with color", - output, + "{output:?} should start with color" ); assert!(output.ends_with("[39m"), "reset foreground color"); @@ -646,7 +685,7 @@ mod tests { dir.child("one.d/.hidden").touch().unwrap(); let mut metas = Meta::from_path(Path::new(dir.path()), false) .unwrap() - .recurse_into(42, &flags) + .recurse_into(42, &flags, None) .unwrap() .0 .unwrap(); @@ -656,6 +695,7 @@ mod tests { &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), + &GitTheme::new(), ); assert_eq!("one.d\n├── .hidden\n└── two\n", output); @@ -678,7 +718,7 @@ mod tests { dir.child("dir/file").touch().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false) .unwrap() - .recurse_into(42, &flags) + .recurse_into(42, &flags, None) .unwrap() .0 .unwrap(); @@ -687,6 +727,7 @@ mod tests { &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), + &GitTheme::new(), ); let length_before_b = |i| -> usize { @@ -718,7 +759,7 @@ mod tests { dir.child("dir/file").touch().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false) .unwrap() - .recurse_into(42, &flags) + .recurse_into(42, &flags, None) .unwrap() .0 .unwrap(); @@ -727,6 +768,7 @@ mod tests { &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), + &GitTheme::new(), ); assert_eq!(output.lines().nth(1).unwrap().chars().next().unwrap(), '└'); @@ -757,7 +799,7 @@ mod tests { dir.child("one.d/two").touch().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false) .unwrap() - .recurse_into(42, &flags) + .recurse_into(42, &flags, None) .unwrap() .0 .unwrap(); @@ -766,6 +808,7 @@ mod tests { &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), + &GitTheme::new(), ); assert!(output.ends_with("└── two\n")); @@ -787,7 +830,7 @@ mod tests { dir.child("test").touch().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false) .unwrap() - .recurse_into(1, &flags) + .recurse_into(1, &flags, None) .unwrap() .0 .unwrap(); @@ -796,6 +839,7 @@ mod tests { &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), + &GitTheme::new(), ); dir.close().unwrap(); @@ -820,7 +864,7 @@ mod tests { dir.child("testdir").create_dir_all().unwrap(); let metas = Meta::from_path(Path::new(dir.path()), false) .unwrap() - .recurse_into(1, &flags) + .recurse_into(1, &flags, None) .unwrap() .0 .unwrap(); @@ -829,6 +873,7 @@ mod tests { &flags, &Colors::new(color::ThemeOption::NoColor), &Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()), + &GitTheme::new(), ); dir.close().unwrap(); @@ -840,4 +885,115 @@ mod tests { assert!(!output.contains("Date Modified")); assert!(!output.contains("Name")); } + + #[test] + fn test_folder_path() { + let tmp_dir = tempdir().expect("failed to create temp dir"); + + let file_path = tmp_dir.path().join("file"); + std::fs::File::create(&file_path).expect("failed to create the file"); + let file = Meta::from_path(&file_path, false).unwrap(); + + let dir_path = tmp_dir.path().join("dir"); + std::fs::create_dir(&dir_path).expect("failed to create the dir"); + let dir = Meta::from_path(&dir_path, false).unwrap(); + + assert_eq!( + display_folder_path(&dir), + format!( + "\n{}{}dir:\n", + tmp_dir.path().to_string_lossy(), + std::path::MAIN_SEPARATOR + ) + ); + + const YES: bool = true; + const NO: bool = false; + + assert_eq!( + should_display_folder_path(0, &[file.clone()], &Flags::default()), + YES // doesn't matter since there is no folder + ); + assert_eq!( + should_display_folder_path(0, &[dir.clone()], &Flags::default()), + NO + ); + assert_eq!( + should_display_folder_path(0, &[file.clone(), dir.clone()], &Flags::default()), + YES + ); + assert_eq!( + should_display_folder_path(0, &[dir.clone(), dir.clone()], &Flags::default()), + YES + ); + assert_eq!( + should_display_folder_path(0, &[file.clone(), file.clone()], &Flags::default()), + YES // doesn't matter since there is no folder + ); + + drop(dir); // to avoid clippy complains about previous .clone() + drop(file); + } + + #[cfg(unix)] + #[test] + fn test_folder_path_with_links() { + let tmp_dir = tempdir().expect("failed to create temp dir"); + + let file_path = tmp_dir.path().join("file"); + std::fs::File::create(&file_path).expect("failed to create the file"); + let file = Meta::from_path(&file_path, false).unwrap(); + + let dir_path = tmp_dir.path().join("dir"); + std::fs::create_dir(&dir_path).expect("failed to create the dir"); + let dir = Meta::from_path(&dir_path, false).unwrap(); + + let link_path = tmp_dir.path().join("link"); + std::os::unix::fs::symlink("dir", &link_path).unwrap(); + let link = Meta::from_path(&link_path, false).unwrap(); + + let grid_flags = Flags { + layout: Layout::Grid, + ..Flags::default() + }; + + let oneline_flags = Flags { + layout: Layout::OneLine, + ..Flags::default() + }; + + const YES: bool = true; + const NO: bool = false; + + assert_eq!( + should_display_folder_path(0, &[link.clone()], &grid_flags), + NO + ); + assert_eq!( + should_display_folder_path(0, &[link.clone()], &oneline_flags), + YES // doesn't matter since this link will be expanded as a directory + ); + + assert_eq!( + should_display_folder_path(0, &[file.clone(), link.clone()], &grid_flags), + YES + ); + assert_eq!( + should_display_folder_path(0, &[file.clone(), link.clone()], &oneline_flags), + YES // doesn't matter since this link will be expanded as a directory + ); + + assert_eq!( + should_display_folder_path(0, &[dir.clone(), link.clone()], &grid_flags), + YES + ); + assert_eq!( + should_display_folder_path(0, &[dir.clone(), link.clone()], &oneline_flags), + YES + ); + + drop(dir); // to avoid clippy complains about previous .clone() + drop(file); + drop(link); + } } diff --git a/src/flags.rs b/src/flags.rs index 1afe913..a73ac65 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -17,7 +17,6 @@ pub mod symlink_arrow; pub mod symlinks; pub mod total_size; -pub use blocks::Block; pub use blocks::Blocks; pub use color::Color; pub use color::{ColorOption, ThemeOption}; diff --git a/src/flags/blocks.rs b/src/flags/blocks.rs index d51834a..0ae53c7 100644 --- a/src/flags/blocks.rs +++ b/src/flags/blocks.rs @@ -66,6 +66,28 @@ impl Blocks { None => self.0.insert(0, Block::Context), } } + + /// Checks whether `self` already contains a [Block] of variant [GitStatus](Block::GitSatus). + fn contains_git_status(&self) -> bool { + self.0.contains(&Block::GitStatus) + } + + /// Put a [Block] of variant [GitStatus](Block::GitSatus) on the left of [GitStatus](Block::Name) to `self`. + fn add_git_status(&mut self) { + if let Some(position) = self.0.iter().position(|&b| b == Block::Name) { + self.0.insert(position, Block::GitStatus); + } else { + self.0.push(Block::GitStatus); + } + } + + /// Prepends a [Block] of variant [GitStatus](Block::GitSatus), if `self` does not already contain a + /// Block of that variant. + fn optional_add_git_status(&mut self) { + if !self.contains_git_status() { + self.add_git_status() + } + } } impl Configurable<Self> for Blocks { @@ -103,6 +125,10 @@ impl Configurable<Self> for Blocks { blocks.optional_prepend_inode(); } + if !cfg!(feature = "no-git") && cli.git && cli.long { + blocks.optional_add_git_status(); + } + blocks } @@ -168,6 +194,7 @@ pub enum Block { Name, INode, Links, + GitStatus, } impl Block { @@ -183,6 +210,7 @@ impl Block { Block::SizeValue => "SizeValue", Block::Date => "Date Modified", Block::Name => "Name", + Block::GitStatus => "Git", } } } @@ -202,6 +230,7 @@ impl TryFrom<&str> for Block { "name" => Ok(Self::Name), "inode" => Ok(Self::INode), "links" => Ok(Self::Links), + "git" => Ok(Self::GitStatus), _ => Err(format!("Not a valid block name: {string}")), } } @@ -371,6 +400,30 @@ mod test_blocks { assert_eq!(Blocks::from_cli(&cli), Some(test_blocks)); } + #[cfg(not(feature = "no-git"))] + #[test] + fn test_from_cli_implicit_add_git_block() { + let argv = vec![ + "lsd", + "--blocks", + "permission,name,group,date", + "--git", + "--long", + ]; + let cli = Cli::try_parse_from(argv).unwrap(); + let test_blocks = Blocks(vec![ + Block::Permission, + Block::GitStatus, + Block::Name, + Block::Group, + Block::Date, + ]); + assert_eq!( + Blocks::configure_from(&cli, &Config::with_none()), + test_blocks + ); + } + #[test] fn test_from_config_none() { assert_eq!(None, Blocks::from_config(&Config::with_none())); @@ -541,5 +594,11 @@ mod test_block { assert_eq!(Block::SizeValue.get_header(), "SizeValue"); assert_eq!(Block::Date.get_header(), "Date Modified"); assert_eq!(Block::Name.get_header(), "Name"); + assert_eq!(Block::GitStatus.get_header(), "Git"); + } + + #[test] + fn test_git_status() { + assert_eq!(Ok(Block::GitStatus), Block::try_from("git")); } } diff --git a/src/flags/sorting.rs b/src/flags/sorting.rs index 07b6b09..3e89593 100644 --- a/src/flags/sorting.rs +++ b/src/flags/sorting.rs @@ -44,6 +44,7 @@ pub enum SortColumn { Time, Size, Version, + GitStatus, } impl Configurable<Self> for SortColumn { @@ -62,6 +63,8 @@ impl Configurable<Self> for SortColumn { Some(Self::Extension) } else if cli.versionsort || sort == Some("version") { Some(Self::Version) + } else if cli.gitsort || sort == Some("git") { + Some(Self::GitStatus) } else if cli.no_sort || sort == Some("none") { Some(Self::None) } else { @@ -213,6 +216,13 @@ mod test_sort_column { } #[test] + fn test_from_cli_git() { + let argv = ["lsd", "--gitsort"]; + let cli = Cli::try_parse_from(argv).unwrap(); + assert_eq!(Some(SortColumn::GitStatus), SortColumn::from_cli(&cli)); + } + + #[test] fn test_from_cli_version() { let argv = ["lsd", "--versionsort"]; let cli = Cli::try_parse_from(argv).unwrap(); @@ -249,6 +259,14 @@ mod test_sort_column { assert_eq!(Some(SortColumn::None), SortColumn::from_cli(&cli)); } + #[cfg(not(feature = "no-git"))] + #[test] + fn test_from_arg_cli_sort_git() { + let argv = ["lsd", "--sort", "git"]; + let cli = Cli::try_parse_from(argv).unwrap(); + assert_eq!(Some(SortColumn::GitStatus), SortColumn::from_cli(&cli)); + } + #[test] fn test_multi_sort() { let argv = ["lsd", "--sort", "size", "--sort", "time"]; @@ -334,6 +352,17 @@ mod test_sort_column { }); assert_eq!(Some(SortColumn::Version), SortColumn::from_config(&c)); } + + #[test] + fn test_from_config_git_status() { + let mut c = Config::with_none(); + c.sorting = Some(Sorting { + column: Some(SortColumn::GitStatus), + reverse: None, + dir_grouping: None, + }); + assert_eq!(Some(SortColumn::GitStatus), SortColumn::from_config(&c)); + } } #[cfg(test)] diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..197e10c --- /dev/null +++ b/src/git.rs @@ -0,0 +1,460 @@ +use crate::meta::git_file_status::GitFileStatus; +use std::path::{Path, PathBuf}; + +#[allow(dead_code)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] +pub enum GitStatus { + /// No status info + #[default] + Default, + /// No changes (got from git status) + Unmodified, + /// Entry is ignored item in workdir + Ignored, + /// Entry does not exist in old version (now in stage) + NewInIndex, + /// Entry does not exist in old version (not in stage) + NewInWorkdir, + /// Type of entry changed between old and new + Typechange, + /// Entry does not exist in new version + Deleted, + /// Entry was renamed between old and new + Renamed, + /// Entry content changed between old and new + Modified, + /// Entry in the index is conflicted + Conflicted, +} + +pub struct GitCache { + #[cfg(not(feature = "no-git"))] + statuses: Vec<(PathBuf, git2::Status)>, +} + +#[cfg(feature = "no-git")] +impl GitCache { + pub fn new(_: &Path) -> Self { + Self {} + } + + pub fn get(&self, _filepath: &PathBuf, _is_directory: bool) -> Option<GitFileStatus> { + None + } +} + +#[cfg(not(feature = "no-git"))] +impl GitCache { + pub fn new(path: &Path) -> GitCache { + let repo = match git2::Repository::discover(path) { + Ok(r) => r, + Err(_e) => { + // Unable to retrieve Git info; it doesn't seem to be a git directory + return Self::empty(); + } + |