mirror of
https://github.com/lsd-rs/lsd
synced 2024-12-13 13:42:34 +00:00
Git integration (#822)
This commit is contained in:
parent
6840c01905
commit
2fe3fcdd35
26 changed files with 1056 additions and 38 deletions
|
@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Add [Git integration](https://github.com/Peltoche/lsd/issues/7) from [hpwxf](https://github.com/hpwxf)
|
||||
- In keeping with the coreutils change, add quotes and escapes for necessary filenames from [merelymyself](https://github.com/merelymyself)
|
||||
- Add support for icon theme from [zwpaper](https://github.com/zwpaper)
|
||||
- Add icon for kt and kts from [LeeWeeder](https://github.com/LeeWeeder)
|
||||
|
|
62
Cargo.lock
generated
62
Cargo.lock
generated
|
@ -82,6 +82,9 @@ name = "cc"
|
|||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
|
@ -295,6 +298,19 @@ dependencies = [
|
|||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
|
@ -431,6 +447,15 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.58"
|
||||
|
@ -452,6 +477,30 @@ version = "0.2.139"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.14.2+1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.4"
|
||||
|
@ -503,6 +552,7 @@ dependencies = [
|
|||
"clap_complete",
|
||||
"crossterm",
|
||||
"dirs",
|
||||
"git2",
|
||||
"globset",
|
||||
"human-sort",
|
||||
"libc",
|
||||
|
@ -643,6 +693,12 @@ version = "2.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "1.0.8"
|
||||
|
@ -1104,6 +1160,12 @@ dependencies = [
|
|||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
|
|
|
@ -21,7 +21,7 @@ clap_complete = "4.1"
|
|||
version_check = "0.9.*"
|
||||
|
||||
[dependencies]
|
||||
crossterm = { version = "0.24.0", features = ["serde"]}
|
||||
crossterm = { version = "0.24.0", features = ["serde"] }
|
||||
dirs = "3.0.*"
|
||||
libc = "0.2.*"
|
||||
human-sort = "0.2.2"
|
||||
|
@ -42,6 +42,10 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_yaml = "0.8"
|
||||
url = "2.1.*"
|
||||
|
||||
[target."cfg(not(all(windows, target_arch = \"x86\", target_env = \"gnu\")))".dependencies]
|
||||
# if ssl feature is enabled compilation will fail on arm-unknown-linux-gnueabihf and i686-pc-windows-gnu
|
||||
git2 = { version = "0.16", optional = true, default-features = false }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
users = "0.11.*"
|
||||
xattr = "0.2.*"
|
||||
|
@ -61,7 +65,9 @@ tempfile = "3"
|
|||
serial_test = "0.5"
|
||||
|
||||
[features]
|
||||
default = ["git2"]
|
||||
sudo = []
|
||||
no-git = [] # force disabling git even if available by default
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
|
@ -103,7 +103,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, links
|
||||
# Possible values: permission, user, group, context, size, date, name, inode, links, git
|
||||
blocks:
|
||||
- permission
|
||||
- user
|
||||
|
|
11
build.rs
11
build.rs
|
@ -34,4 +34,15 @@ fn main() {
|
|||
generate_to(Zsh, &mut app, bin_name, &outdir).expect("Failed to generate Zsh completions");
|
||||
generate_to(PowerShell, &mut app, bin_name, &outdir)
|
||||
.expect("Failed to generate PowerShell completions");
|
||||
|
||||
// Disable git feature for these target where git2 is not well supported
|
||||
if !std::env::var("CARGO_FEATURE_GIT2")
|
||||
.map(|flag| flag == "1")
|
||||
.unwrap_or(false)
|
||||
|| std::env::var("TARGET")
|
||||
.map(|target| target == "i686-pc-windows-gnu")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
println!(r#"cargo:rustc-cfg=feature="no-git""#);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
set -ex
|
||||
|
||||
build() {
|
||||
cargo build --target "$TARGET" --release --verbose
|
||||
cargo build --target "$TARGET" --features="$FEATURES" --release --verbose
|
||||
}
|
||||
|
||||
pack() {
|
||||
|
|
|
@ -38,6 +38,9 @@ lsd is a ls command with a lot of pretty colours and some other stuff to enrich
|
|||
`-X`, `--extensionsort`
|
||||
: Sort by file extension
|
||||
|
||||
`--git`
|
||||
: Display git status. Directory git status is a reduction of included file statuses (recursively).
|
||||
|
||||
`--help`
|
||||
: Prints help information
|
||||
|
||||
|
@ -90,7 +93,7 @@ lsd is a ls command with a lot of pretty colours and some other stuff to enrich
|
|||
: Natural sort of (version) numbers within text
|
||||
|
||||
`--blocks <blocks>...`
|
||||
: Specify the blocks that will be displayed and in what order [possible values: permission, user, group, size, date, name, inode]
|
||||
: Specify the blocks that will be displayed and in what order [possible values: permission, user, group, size, date, name, inode, git]
|
||||
|
||||
`--color <color>...`
|
||||
: When to use terminal colours [default: auto] [possible values: always, auto, never]
|
||||
|
@ -126,7 +129,7 @@ lsd is a ls command with a lot of pretty colours and some other stuff to enrich
|
|||
: How to display size [default: default] [possible values: default, short, bytes]
|
||||
|
||||
`--sort <WORD>...`
|
||||
: Sort by WORD instead of name [possible values: size, time, version, extension]
|
||||
: Sort by WORD instead of name [possible values: size, time, version, extension, git]
|
||||
|
||||
`-U`, `--no-sort`
|
||||
: Do not sort. List entries in directory order
|
||||
|
|
49
src/app.rs
49
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
32
src/core.rs
32
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);
|
||||
|
|
180
src/display.rs
180
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -212,6 +215,13 @@ mod test_sort_column {
|
|||
assert_eq!(Some(SortColumn::Size), SortColumn::from_cli(&cli));
|
||||
}
|
||||
|
||||
#[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"];
|
||||
|
@ -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)]
|
||||
|
|
460
src/git.rs
Normal file
460
src/git.rs
Normal file
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(workdir) = repo.workdir().and_then(|x| std::fs::canonicalize(x).ok()) {
|
||||
let mut statuses = Vec::new();
|
||||
// Retrieving Git statuses for workdir
|
||||
match repo.statuses(None) {
|
||||
Ok(status_list) => {
|
||||
for status_entry in status_list.iter() {
|
||||
// git2-rs provides / separated path even on Windows. We have to rebuild it
|
||||
let str_path = status_entry.path().unwrap();
|
||||
let path: PathBuf =
|
||||
str_path.split('/').collect::<Vec<_>>().iter().collect();
|
||||
let path = workdir.join(path);
|
||||
let elem = (path, status_entry.status());
|
||||
statuses.push(elem);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
crate::print_error!(
|
||||
"Cannot retrieve Git statuses for directory {:?}: {}",
|
||||
workdir,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GitCache { statuses }
|
||||
} else {
|
||||
// No workdir
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
GitCache {
|
||||
statuses: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, filepath: &PathBuf, is_directory: bool) -> Option<GitFileStatus> {
|
||||
match std::fs::canonicalize(filepath) {
|
||||
Ok(filename) => Some(self.inner_get(&filename, is_directory)),
|
||||
Err(err) => {
|
||||
crate::print_error!("Cannot get git status for {:?}: {}", filepath, err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_get(&self, filepath: &PathBuf, is_directory: bool) -> GitFileStatus {
|
||||
if is_directory {
|
||||
self.statuses
|
||||
.iter()
|
||||
.filter(|&x| x.0.starts_with(filepath))
|
||||
.map(|x| GitFileStatus::new(x.1))
|
||||
.fold(GitFileStatus::default(), |acc, x| GitFileStatus {
|
||||
index: std::cmp::max(acc.index, x.index),
|
||||
workdir: std::cmp::max(acc.workdir, x.workdir),
|
||||
})
|
||||
} else {
|
||||
self.statuses
|
||||
.iter()
|
||||
.find(|&x| filepath == &x.0)
|
||||
.map(|e| GitFileStatus::new(e.1))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "no-git"))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assert_fs::prelude::*;
|
||||
use assert_fs::TempDir;
|
||||
use git2::build::CheckoutBuilder;
|
||||
use git2::{CherrypickOptions, Index, Oid, Repository, RepositoryInitOptions};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::remove_file;
|
||||
#[allow(unused)]
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn compare_git_status() {
|
||||
assert!(GitStatus::Unmodified < GitStatus::Conflicted);
|
||||
}
|
||||
|
||||
macro_rules! t {
|
||||
($e:expr) => {
|
||||
match $e {
|
||||
Ok(e) => e,
|
||||
Err(e) => panic!("{} failed with {}", stringify!($e), e),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn repo_init() -> (TempDir, Repository) {
|
||||
let td = t!(TempDir::new());
|
||||
let mut opts = RepositoryInitOptions::new();
|
||||
opts.initial_head("master");
|
||||
let repo = Repository::init_opts(td.path(), &opts).unwrap();
|
||||
{
|
||||
let mut config = t!(repo.config());
|
||||
t!(config.set_str("user.name", "name"));
|
||||
t!(config.set_str("user.email", "email"));
|
||||
let mut index = t!(repo.index());
|
||||
let id = t!(index.write_tree());
|
||||
let tree = t!(repo.find_tree(id));
|
||||
let sig = t!(repo.signature());
|
||||
t!(repo.commit(Some("HEAD"), &sig, &sig, "initial commit", &tree, &[]));
|
||||
}
|
||||
(td, repo)
|
||||
}
|
||||
|
||||
fn commit(repo: &Repository, index: &mut Index, msg: &str) -> (Oid, Oid) {
|
||||
let tree_id = t!(index.write_tree());
|
||||
let tree = t!(repo.find_tree(tree_id));
|
||||
let sig = t!(repo.signature());
|
||||
let head_id = t!(repo.refname_to_id("HEAD"));
|
||||
let parent = t!(repo.find_commit(head_id));
|
||||
let commit = t!(repo.commit(Some("HEAD"), &sig, &sig, msg, &tree, &[&parent]));
|
||||
(commit, tree_id)
|
||||
}
|
||||
|
||||
fn check_cache(root: &Path, statuses: &HashMap<&PathBuf, GitFileStatus>, msg: &str) {
|
||||
let cache = GitCache::new(root);
|
||||
for (&path, status) in statuses.iter() {
|
||||
if let Ok(filename) = std::fs::canonicalize(&root.join(path)) {
|
||||
let is_directory = filename.is_dir();
|
||||
assert_eq!(
|
||||
&cache.inner_get(&filename, is_directory),
|
||||
status,
|
||||
"Invalid status for file {} at stage {}",
|
||||
filename.to_string_lossy(),
|
||||
msg
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_workflow() {
|
||||
// rename as test_git_workflow
|
||||
let (root, repo) = repo_init();
|
||||
let mut index = repo.index().unwrap();
|
||||
let mut expected_statuses = HashMap::new();
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "initialization");
|
||||
|
||||
let f0 = PathBuf::from(".gitignore");
|
||||
root.child(&f0).write_str("*.bak").unwrap();
|
||||
expected_statuses.insert(
|
||||
&f0,
|
||||
GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::NewInWorkdir,
|
||||
},
|
||||
);
|
||||
|
||||
let _success = Command::new("git")
|
||||
.current_dir(root.path())
|
||||
.arg("status")
|
||||
.status()
|
||||
.expect("Git status failed")
|
||||
.success();
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "new .gitignore");
|
||||
|
||||
index.add_path(f0.as_path()).unwrap();
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "unstaged .gitignore");
|
||||
|
||||
index.write().unwrap();
|
||||
*expected_statuses.get_mut(&f0).unwrap() = GitFileStatus {
|
||||
index: GitStatus::NewInIndex,
|
||||
workdir: GitStatus::Unmodified,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "staged .gitignore");
|
||||
|
||||
commit(&repo, &mut index, "Add gitignore");
|
||||
*expected_statuses.get_mut(&f0).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Default,
|
||||
workdir: GitStatus::Default,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "Committed .gitignore");
|
||||
|
||||
let d1 = PathBuf::from("d1");
|
||||
let f1 = d1.join("f1");
|
||||
root.child(&f1).touch().unwrap();
|
||||
let f2 = d1.join("f2.bak");
|
||||
root.child(&f2).touch().unwrap();
|
||||
expected_statuses.insert(
|
||||
&d1,
|
||||
GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::NewInWorkdir,
|
||||
},
|
||||
);
|
||||
expected_statuses.insert(
|
||||
&f1,
|
||||
GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::NewInWorkdir,
|
||||
},
|
||||
);
|
||||
expected_statuses.insert(
|
||||
&f2,
|
||||
GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::Ignored,
|
||||
},
|
||||
);
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "New files");
|
||||
|
||||
index.add_path(f1.as_path()).unwrap();
|
||||
index.write().unwrap();
|
||||
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::NewInIndex,
|
||||
workdir: GitStatus::Ignored,
|
||||
};
|
||||
*expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::NewInIndex,
|
||||
workdir: GitStatus::Unmodified,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "Unstaged new files");
|
||||
|
||||
index.add_path(f2.as_path()).unwrap();
|
||||
index.write().unwrap();
|
||||
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::NewInIndex,
|
||||
workdir: GitStatus::Unmodified,
|
||||
};
|
||||
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
|
||||
index: GitStatus::NewInIndex,
|
||||
workdir: GitStatus::Unmodified,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "Staged new files");
|
||||
|
||||
let (commit1_oid, _) = commit(&repo, &mut index, "Add new files");
|
||||
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Default,
|
||||
workdir: GitStatus::Default,
|
||||
};
|
||||
*expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Default,
|
||||
workdir: GitStatus::Default,
|
||||
};
|
||||
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Default,
|
||||
workdir: GitStatus::Default,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "Committed new files");
|
||||
|
||||
remove_file(root.child(&f2).path()).unwrap();
|
||||
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::Deleted,
|
||||
};
|
||||
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::Deleted,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "Remove file");
|
||||
|
||||
root.child(&f1).write_str("New content").unwrap();
|
||||
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::Modified,
|
||||
}; // more important to see modified vs deleted ?
|
||||
*expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::Modified,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "Change file");
|
||||
|
||||
index.remove_path(&f2).unwrap();
|
||||
index.write().unwrap();
|
||||
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Deleted,
|
||||
workdir: GitStatus::Modified,
|
||||
};
|
||||
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Deleted,
|
||||
workdir: GitStatus::Unmodified,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(root.path(), &expected_statuses, "Staged changes");
|
||||
|
||||
commit(&repo, &mut index, "Remove backup file");
|
||||
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::Modified,
|
||||
};
|
||||
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Default,
|
||||
workdir: GitStatus::Default,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(
|
||||
root.path(),
|
||||
&expected_statuses,
|
||||
"Committed changes (first part)",
|
||||
);
|
||||
|
||||
index.add_path(&f1).unwrap();
|
||||
index.write().unwrap();
|
||||
commit(&repo, &mut index, "Save modified file");
|
||||
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Default,
|
||||
workdir: GitStatus::Default,
|
||||
};
|
||||
*expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Default,
|
||||
workdir: GitStatus::Default,
|
||||
};
|
||||
|
||||
// Check now
|
||||
check_cache(
|
||||
root.path(),
|
||||
&expected_statuses,
|
||||
"Committed changes (second part)",
|
||||
);
|
||||
|
||||
let branch_commit = repo.find_commit(commit1_oid).unwrap();
|
||||
let branch = repo
|
||||
.branch("conflict-branch", &branch_commit, true)
|
||||
.unwrap();
|
||||
repo.set_head(format!("refs/heads/{}", branch.name().unwrap().unwrap()).as_str())
|
||||
.unwrap();
|
||||
let mut checkout_opts = CheckoutBuilder::new();
|
||||
checkout_opts.force();
|
||||
repo.checkout_head(Some(&mut checkout_opts)).unwrap();
|
||||
|
||||
root.child(&f1)
|
||||
.write_str("New conflicting content")
|
||||
.unwrap();
|
||||
root.child(&f2)
|
||||
.write_str("New conflicting content")
|
||||
.unwrap();
|
||||
index.add_path(&f1).unwrap();
|
||||
index.add_path(&f2).unwrap();
|
||||
index.write().unwrap();
|
||||
let (commit2_oid, _) = commit(&repo, &mut index, "Save conflicting changes");
|
||||
|
||||
// Check now
|
||||
check_cache(
|
||||
root.path(),
|
||||
&expected_statuses,
|
||||
"Committed changes in branch",
|
||||
);
|
||||
|
||||
repo.set_head("refs/heads/master").unwrap();
|
||||
repo.checkout_head(Some(&mut checkout_opts)).unwrap();
|
||||
let mut cherrypick_opts = CherrypickOptions::new();
|
||||
let branch_commit = repo.find_commit(commit2_oid).unwrap();
|
||||
repo.cherrypick(&branch_commit, Some(&mut cherrypick_opts))
|
||||
.unwrap();
|
||||
*expected_statuses.get_mut(&d1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::Conflicted,
|
||||
};
|
||||
*expected_statuses.get_mut(&f1).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::Conflicted,
|
||||
};
|
||||
*expected_statuses.get_mut(&f2).unwrap() = GitFileStatus {
|
||||
index: GitStatus::Unmodified,
|
||||
workdir: GitStatus::Conflicted,
|
||||
};
|
||||
|
||||
// let _success = Command::new("git")
|
||||
// .current_dir(root.path())
|
||||
// .arg("status")
|
||||
// .status()
|
||||
// .expect("Git status failed")
|
||||
// .success();
|
||||
|
||||
// Check now
|
||||
check_cache(
|
||||
root.path(),
|
||||
&expected_statuses,
|
||||
"Conflict between master and branch",
|
||||
);
|
||||
}
|
||||
}
|
31
src/git_theme.rs
Normal file
31
src/git_theme.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::git::GitStatus;
|
||||
use crate::theme::git::GitThemeSymbols;
|
||||
|
||||
pub struct GitTheme {
|
||||
symbols: GitThemeSymbols,
|
||||
}
|
||||
|
||||
impl GitTheme {
|
||||
pub fn new() -> GitTheme {
|
||||
let git_symbols = GitThemeSymbols::default();
|
||||
Self {
|
||||
symbols: git_symbols,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_symbol(&self, status: &GitStatus) -> String {
|
||||
let symbol = match status {
|
||||
GitStatus::Default => &self.symbols.default,
|
||||
GitStatus::Unmodified => &self.symbols.unmodified,
|
||||
GitStatus::Ignored => &self.symbols.ignored,
|
||||
GitStatus::NewInIndex => &self.symbols.new_in_index,
|
||||
GitStatus::NewInWorkdir => &self.symbols.new_in_workdir,
|
||||
GitStatus::Typechange => &self.symbols.typechange,
|
||||
GitStatus::Deleted => &self.symbols.deleted,
|
||||
GitStatus::Renamed => &self.symbols.renamed,
|
||||
GitStatus::Modified => &self.symbols.modified,
|
||||
GitStatus::Conflicted => &self.symbols.conflicted,
|
||||
};
|
||||
symbol.to_string()
|
||||
}
|
||||
}
|
|
@ -224,7 +224,7 @@ mod test {
|
|||
let tmp_dir = tempdir().expect("failed to create temp dir");
|
||||
|
||||
for (ext, file_icon) in &IconTheme::get_default_icons_by_extension() {
|
||||
let file_path = tmp_dir.path().join(format!("file.{}", ext));
|
||||
let file_path = tmp_dir.path().join(format!("file.{ext}"));
|
||||
File::create(&file_path).expect("failed to create file");
|
||||
let meta = Meta::from_path(&file_path, false).unwrap();
|
||||
|
||||
|
|
|
@ -33,6 +33,8 @@ mod config_file;
|
|||
mod core;
|
||||
mod display;
|
||||
mod flags;
|
||||
mod git;
|
||||
mod git_theme;
|
||||
mod icon;
|
||||
mod meta;
|
||||
mod sort;
|
||||
|
|
67
src/meta/git_file_status.rs
Normal file
67
src/meta/git_file_status.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use crate::color::{self, ColoredString, Colors};
|
||||
use crate::git::GitStatus;
|
||||
use crate::git_theme::GitTheme;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct GitFileStatus {
|
||||
pub index: GitStatus,
|
||||
pub workdir: GitStatus,
|
||||
}
|
||||
|
||||
impl Default for GitFileStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
index: GitStatus::Default,
|
||||
workdir: GitStatus::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GitFileStatus {
|
||||
#[cfg(not(feature = "no-git"))]
|
||||
pub fn new(status: git2::Status) -> Self {
|
||||
Self {
|
||||
index: match status {
|
||||
s if s.contains(git2::Status::INDEX_NEW) => GitStatus::NewInIndex,
|
||||
s if s.contains(git2::Status::INDEX_DELETED) => GitStatus::Deleted,
|
||||
s if s.contains(git2::Status::INDEX_MODIFIED) => GitStatus::Modified,
|
||||
s if s.contains(git2::Status::INDEX_RENAMED) => GitStatus::Renamed,
|
||||
s if s.contains(git2::Status::INDEX_TYPECHANGE) => GitStatus::Typechange,
|
||||
_ => GitStatus::Unmodified,
|
||||
},
|
||||
|
||||
workdir: match status {
|
||||
s if s.contains(git2::Status::WT_NEW) => GitStatus::NewInWorkdir,
|
||||
s if s.contains(git2::Status::WT_DELETED) => GitStatus::Deleted,
|
||||
s if s.contains(git2::Status::WT_MODIFIED) => GitStatus::Modified,
|
||||
s if s.contains(git2::Status::WT_RENAMED) => GitStatus::Renamed,
|
||||
s if s.contains(git2::Status::IGNORED) => GitStatus::Ignored,
|
||||
s if s.contains(git2::Status::WT_TYPECHANGE) => GitStatus::Typechange,
|
||||
s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflicted,
|
||||
_ => GitStatus::Unmodified,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, colors: &Colors, git_theme: &GitTheme) -> ColoredString {
|
||||
let res = [
|
||||
colors.colorize(
|
||||
git_theme.get_symbol(&self.index),
|
||||
&color::Elem::GitStatus { status: self.index },
|
||||
),
|
||||
colors.colorize(
|
||||
git_theme.get_symbol(&self.workdir),
|
||||
&color::Elem::GitStatus {
|
||||
status: self.workdir,
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
// From the experiment, the maximum string size is 153 bytes
|
||||
.fold(String::with_capacity(160), |mut acc, x| {
|
||||
acc.push_str(&x.to_string());
|
||||
acc
|
||||
});
|
||||
ColoredString::new(Colors::default_style(), res)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
mod access_control;
|
||||
mod date;
|
||||
mod filetype;
|
||||
pub mod git_file_status;
|
||||
mod indicator;
|
||||
mod inode;
|
||||
mod links;
|
||||
|
@ -17,6 +18,7 @@ mod windows_utils;
|
|||
pub use self::access_control::AccessControl;
|
||||
pub use self::date::Date;
|
||||
pub use self::filetype::FileType;
|
||||
pub use self::git_file_status::GitFileStatus;
|
||||
pub use self::indicator::Indicator;
|
||||
pub use self::inode::INode;
|
||||
pub use self::links::Links;
|
||||
|
@ -30,6 +32,7 @@ pub use crate::icon::Icons;
|
|||
use crate::flags::{Display, Flags, Layout};
|
||||
use crate::{print_error, ExitCode};
|
||||
|
||||
use crate::git::GitCache;
|
||||
use std::io::{self, Error, ErrorKind};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
|
@ -48,6 +51,7 @@ pub struct Meta {
|
|||
pub links: Option<Links>,
|
||||
pub content: Option<Vec<Meta>>,
|
||||
pub access_control: Option<AccessControl>,
|
||||
pub git_status: Option<GitFileStatus>,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
|
@ -55,6 +59,7 @@ impl Meta {
|
|||
&self,
|
||||
depth: usize,
|
||||
flags: &Flags,
|
||||
cache: Option<&GitCache>,
|
||||
) -> io::Result<(Option<Vec<Meta>>, ExitCode)> {
|
||||
if depth == 0 {
|
||||
return Ok((None, ExitCode::OK));
|
||||
|
@ -94,6 +99,9 @@ impl Meta {
|
|||
Self::from_path(&self.path.join(Component::ParentDir), flags.dereference.0)?;
|
||||
parent_meta.name.name = "..".to_owned();
|
||||
|
||||
current_meta.git_status = cache.and_then(|cache| cache.get(¤t_meta.path, true));
|
||||
parent_meta.git_status = cache.and_then(|cache| cache.get(&parent_meta.path, true));
|
||||
|
||||
content.push(current_meta);
|
||||
content.push(parent_meta);
|
||||
}
|
||||
|
@ -150,7 +158,7 @@ impl Meta {
|
|||
|
||||
// check dereferencing
|
||||
if flags.dereference.0 || !matches!(entry_meta.file_type, FileType::SymLink { .. }) {
|
||||
match entry_meta.recurse_into(depth - 1, flags) {
|
||||
match entry_meta.recurse_into(depth - 1, flags, cache) {
|
||||
Ok((content, rec_exit_code)) => {
|
||||
entry_meta.content = content;
|
||||
exit_code.set_if_greater(rec_exit_code);
|
||||
|
@ -163,6 +171,9 @@ impl Meta {
|
|||
};
|
||||
}
|
||||
|
||||
let is_directory = entry.file_type()?.is_dir();
|
||||
entry_meta.git_status =
|
||||
cache.and_then(|cache| cache.get(&entry_meta.path, is_directory));
|
||||
content.push(entry_meta);
|
||||
}
|
||||
|
||||
|
@ -300,6 +311,7 @@ impl Meta {
|
|||
file_type,
|
||||
content: None,
|
||||
access_control,
|
||||
git_status: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@ impl Name {
|
|||
if let Ok(url) = Url::from_file_path(rp) {
|
||||
// Crossterm does not support hyperlinks as of now
|
||||
// https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||
format!("\x1B]8;;{}\x1B\x5C{}\x1B]8;;\x1B\x5C", url, name)
|
||||
format!("\x1B]8;;{url}\x1B\x5C{name}\x1B]8;;\x1B\x5C")
|
||||
} else {
|
||||
print_error!("{}: unable to form url.", name);
|
||||
name
|
||||
|
|
|
@ -23,6 +23,7 @@ pub fn assemble_sorters(flags: &Flags) -> Vec<(SortOrder, SortFn)> {
|
|||
SortColumn::Time => sorters.push((flags.sorting.order, by_date)),
|
||||
SortColumn::Version => sorters.push((flags.sorting.order, by_version)),
|
||||
SortColumn::Extension => sorters.push((flags.sorting.order, by_extension)),
|
||||
SortColumn::GitStatus => sorters.push((flags.sorting.order, by_git_status)),
|
||||
SortColumn::None => {}
|
||||
}
|
||||
sorters
|
||||
|
@ -72,6 +73,10 @@ fn by_extension(a: &Meta, b: &Meta) -> Ordering {
|
|||
a.name.extension().cmp(&b.name.extension())
|
||||
}
|
||||
|
||||
fn by_git_status(a: &Meta, b: &Meta) -> Ordering {
|
||||
a.git_status.cmp(&b.git_status)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod color;
|
||||
pub mod git;
|
||||
pub mod icon;
|
||||
|
||||
use std::path::Path;
|
||||
|
@ -11,6 +12,7 @@ use crate::config_file;
|
|||
use crate::print_error;
|
||||
|
||||
use color::ColorTheme;
|
||||
use git::GitThemeSymbols;
|
||||
use icon::IconTheme;
|
||||
|
||||
#[derive(Debug, Deserialize, Default, PartialEq, Eq)]
|
||||
|
@ -20,6 +22,7 @@ use icon::IconTheme;
|
|||
pub struct Theme {
|
||||
pub color: ColorTheme,
|
||||
pub icon: IconTheme,
|
||||
pub git_theme: GitThemeSymbols,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
|
|
@ -96,6 +96,7 @@ pub struct ColorTheme {
|
|||
#[serde(deserialize_with = "deserialize_color")]
|
||||
pub tree_edge: Color,
|
||||
pub links: Links,
|
||||
pub git_status: GitStatus,
|
||||
|
||||
#[serde(skip)]
|
||||
pub file_type: FileType,
|
||||
|
@ -233,6 +234,15 @@ pub struct Links {
|
|||
pub invalid: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(default)]
|
||||
pub struct GitStatus {
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
pub default: Color,
|
||||
}
|
||||
|
||||
impl Default for Permission {
|
||||
fn default() -> Self {
|
||||
Permission {
|
||||
|
@ -324,6 +334,14 @@ impl Default for Links {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for GitStatus {
|
||||
fn default() -> Self {
|
||||
GitStatus {
|
||||
default: Color::AnsiValue(13), // Pink
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ColorTheme {
|
||||
fn default() -> Self {
|
||||
// TODO(zwpaper): check terminal color and return light or dark
|
||||
|
@ -343,6 +361,7 @@ impl ColorTheme {
|
|||
inode: INode::default(),
|
||||
links: Links::default(),
|
||||
tree_edge: Color::AnsiValue(245), // Grey
|
||||
git_status: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
src/theme/git.rs
Normal file
35
src/theme/git.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(default)]
|
||||
pub struct GitThemeSymbols {
|
||||
pub default: String,
|
||||
pub unmodified: String,
|
||||
pub new_in_index: String,
|
||||
pub new_in_workdir: String,
|
||||
pub deleted: String,
|
||||
pub modified: String,
|
||||
pub renamed: String,
|
||||
pub ignored: String,
|
||||
pub typechange: String,
|
||||
pub conflicted: String,
|
||||
}
|
||||
|
||||
impl Default for GitThemeSymbols {
|
||||
fn default() -> GitThemeSymbols {
|
||||
GitThemeSymbols {
|
||||
default: "-".into(),
|
||||
unmodified: ".".into(),
|
||||
new_in_index: "N".into(),
|
||||
new_in_workdir: "?".into(),
|
||||
deleted: "D".into(),
|
||||
modified: "M".into(),
|
||||
renamed: "R".into(),
|
||||
ignored: "I".into(),
|
||||
typechange: "T".into(),
|
||||
conflicted: "C".into(),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue