summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPascal H <hpwxf@haveneer.com>2023-04-30 18:26:15 +0200
committerGitHub <noreply@github.com>2023-05-01 00:26:15 +0800
commit2fe3fcdd3564836962eab8ba6b1444996fe24e1e (patch)
treefad504cec71bd00aa6b1a5e6c34fe042e4a1cfec /src
parent6840c01905a7601b0d7f0d4dc2e08c5a5a581321 (diff)
Git integration (#822)
Diffstat (limited to 'src')
-rw-r--r--src/app.rs49
-rw-r--r--src/color.rs7
-rw-r--r--src/config_file.rs4
-rw-r--r--src/core.rs32
-rw-r--r--src/display.rs180
-rw-r--r--src/flags.rs1
-rw-r--r--src/flags/blocks.rs59
-rw-r--r--src/flags/sorting.rs29
-rw-r--r--src/git.rs460
-rw-r--r--src/git_theme.rs31
-rw-r--r--src/icon.rs2
-rw-r--r--src/main.rs2
-rw-r--r--src/meta/git_file_status.rs67
-rw-r--r--src/meta/mod.rs14
-rw-r--r--src/meta/name.rs2
-rw-r--r--src/sort.rs5
-rw-r--r--src/theme.rs3
-rw-r--r--src/theme/color.rs19
-rw-r--r--src/theme/git.rs35
19 files changed, 968 insertions, 33 deletions
diff --git a/src/app.rs b/src/app.rs
index 5003866..bd6defa 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -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();
+ }
+