Просмотр исходного кода

Merge branch 'master' into chesterliu/dev/win-support

Chester Liu 4 лет назад
Родитель
Сommit
99d653b7fa

+ 54 - 0
.github/workflows/unit-tests.yml

@@ -0,0 +1,54 @@
+name: Unit tests
+
+on:
+  push:
+    branches: [ master ]
+    paths:
+      - '.github/workflows/*'
+      - 'src/**'
+      - 'Cargo.*'
+      - build.rs
+  pull_request:
+    branches: [ master ]
+    paths:
+      - '.github/workflows/*'
+      - 'src/**'
+      - 'Cargo.*'
+      - build.rs
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  unit-tests:
+    runs-on: ${{ matrix.os }}
+
+    continue-on-error: ${{ matrix.rust == 'nightly' }}
+
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest]
+        rust: [1.48.0, stable, beta, nightly]
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+
+      - name: Install Rust toolchain
+        uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: ${{ matrix.rust }}
+          override: true
+
+      - name: Install cargo-hack
+        uses: actions-rs/cargo@v1
+        with:
+          command: install
+          args: cargo-hack
+
+      - name: Run unit tests
+        uses: actions-rs/cargo@v1
+        with:
+          command: hack
+          args: test --feature-powerset

+ 0 - 19
.travis.yml

@@ -1,19 +0,0 @@
-language: rust
-rust:
-  - 1.45.2
-  - stable
-  - beta
-  - nightly
-
-jobs:
-  fast_finish: true
-  allow_failures:
-    - rust: nightly
-
-  include:
-    - name: 'Rust: test with all features'
-      rust: stable
-      install:
-        - cargo install cargo-hack
-      script:
-        - cargo hack test --feature-powerset

+ 6 - 4
Cargo.lock

@@ -1,5 +1,7 @@
 # This file is automatically @generated by Cargo.
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 [[package]]
 name = "ansi_term"
 name = "ansi_term"
 version = "0.12.1"
 version = "0.12.1"
@@ -90,9 +92,9 @@ dependencies = [
 
 
 [[package]]
 [[package]]
 name = "git2"
 name = "git2"
-version = "0.13.18"
+version = "0.13.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b483c6c2145421099df1b4efd50e0f6205479a072199460eff852fa15e5603c7"
+checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba"
 dependencies = [
 dependencies = [
  "bitflags",
  "bitflags",
  "libc",
  "libc",
@@ -151,9 +153,9 @@ checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
 
 
 [[package]]
 [[package]]
 name = "libgit2-sys"
 name = "libgit2-sys"
-version = "0.12.19+1.1.0"
+version = "0.12.21+1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f322155d574c8b9ebe991a04f6908bb49e68a79463338d24a43d6274cb6443e6"
+checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825"
 dependencies = [
 dependencies = [
  "cc",
  "cc",
  "libc",
  "libc",

+ 7 - 4
Cargo.toml

@@ -1,11 +1,11 @@
 [package]
 [package]
 name = "exa"
 name = "exa"
 description = "A modern replacement for ls"
 description = "A modern replacement for ls"
-
 authors = ["Benjamin Sago <ogham@bsago.me>"]
 authors = ["Benjamin Sago <ogham@bsago.me>"]
 categories = ["command-line-utilities"]
 categories = ["command-line-utilities"]
 edition = "2018"
 edition = "2018"
 exclude = ["/devtools/*", "/Justfile", "/Vagrantfile", "/screenshots.png"]
 exclude = ["/devtools/*", "/Justfile", "/Vagrantfile", "/screenshots.png"]
+readme = "README.md"
 homepage = "https://the.exa.website/"
 homepage = "https://the.exa.website/"
 license = "MIT"
 license = "MIT"
 repository = "https://github.com/ogham/exa"
 repository = "https://github.com/ogham/exa"
@@ -65,7 +65,7 @@ lto = true
 
 
 
 
 [package.metadata.deb]
 [package.metadata.deb]
-license-file = [ "LICENCE" ]
+license-file = [ "LICENCE", "4" ]
 depends = "$auto"
 depends = "$auto"
 extended-description = """
 extended-description = """
 exa is a replacement for ls written in Rust.
 exa is a replacement for ls written in Rust.
@@ -74,6 +74,9 @@ section = "utils"
 priority = "optional"
 priority = "optional"
 assets = [
 assets = [
     [ "target/release/exa", "/usr/bin/exa", "0755" ],
     [ "target/release/exa", "/usr/bin/exa", "0755" ],
-    [ "contrib/man/exa.1", "/usr/share/man/man1/exa.1", "0644" ],
-    [ "contrib/completions.bash", "/etc/bash_completion.d/exa", "0644" ],
+    [ "target/release/../man/exa.1", "/usr/share/man/man1/exa.1", "0644" ],
+    [ "target/release/../man/exa_colors.5", "/usr/share/man/man5/exa_colors.5", "0644" ],
+    [ "completions/bash/exa", "/usr/share/bash-completion/completions/exa", "0644" ],
+    [ "completions/zsh/_exa", "/usr/share/zsh/site-functions/_exa", "0644" ],
+    [ "completions/fish/exa.fish", "/usr/share/fish/vendor_completions.d/exa.fish", "0644" ],
 ]
 ]

+ 4 - 8
README.md

@@ -1,17 +1,13 @@
 <div align="center">
 <div align="center">
-<h1>exa</h1>
+
+# exa
 
 
 [exa](https://the.exa.website/) is a modern replacement for _ls_.
 [exa](https://the.exa.website/) is a modern replacement for _ls_.
 
 
 **README Sections:** [Options](#options) — [Installation](#installation) — [Development](#development)
 **README Sections:** [Options](#options) — [Installation](#installation) — [Development](#development)
 
 
-<a href="https://travis-ci.org/github/ogham/exa">
-    <img src="https://travis-ci.org/ogham/exa.svg?branch=master" alt="Build status" />
-</a>
-
-<a href="https://saythanks.io/to/ogham%40bsago.me">
-    <img src="https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg" alt="Say thanks!" />
-</a>
+[![Unit tests](https://github.com/ogham/exa/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/ogham/exa/actions/workflows/unit-tests.yml)
+[![Say thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/ogham%40bsago.me)
 </div>
 </div>
 
 
 ![Screenshots of exa](screenshots.png)
 ![Screenshots of exa](screenshots.png)

+ 2 - 1
build.rs

@@ -38,9 +38,10 @@ fn main() -> io::Result<()> {
 
 
     // We need to create these files in the Cargo output directory.
     // We need to create these files in the Cargo output directory.
     let out = PathBuf::from(env::var("OUT_DIR").unwrap());
     let out = PathBuf::from(env::var("OUT_DIR").unwrap());
+    let path = &out.join("version_string.txt");
 
 
     // Bland version text
     // Bland version text
-    let mut f = File::create(&out.join("version_string.txt"))?;
+    let mut f = File::create(path).expect(&path.to_string_lossy());
     writeln!(f, "{}", strip_codes(&ver))?;
     writeln!(f, "{}", strip_codes(&ver))?;
 
 
     Ok(())
     Ok(())

+ 17 - 3
completions/completions.bash → completions/bash/exa

@@ -8,6 +8,11 @@ _exa()
             return
             return
             ;;
             ;;
 
 
+        --colour)
+            COMPREPLY=( $( compgen -W 'always auto never' -- "$cur" ) )
+            return
+            ;;
+
         -L|--level)
         -L|--level)
             COMPREPLY=( $( compgen -W '{0..9}' -- "$cur" ) )
             COMPREPLY=( $( compgen -W '{0..9}' -- "$cur" ) )
             return
             return
@@ -19,19 +24,28 @@ _exa()
             ;;
             ;;
 
 
         -t|--time)
         -t|--time)
-            COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- $cur ) )
+            COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- "$cur" ) )
             return
             return
             ;;
             ;;
 
 
         --time-style)
         --time-style)
-            COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- $cur ) )
+            COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- "$cur" ) )
             return
             return
             ;;
             ;;
     esac
     esac
 
 
     case "$cur" in
     case "$cur" in
+        # _parse_help doesn’t pick up short options when they are on the same line than long options
+        --*)
+            # colo[u]r isn’t parsed correctly so we filter these options out and add them by hand
+            parse_help=$( exa --help | grep -oE ' (\-\-[[:alnum:]@-]+)' | tr -d ' ' | grep -v '\-\-colo' )
+            completions=$( echo '--color --colour --color-scale --colour-scale' $parse_help )
+            COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) )
+            ;;
+
         -*)
         -*)
-            COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) )
+            completions=$( exa --help | grep -oE ' (\-[[:alnum:]@])' | tr -d ' ' )
+            COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) )
             ;;
             ;;
 
 
         *)
         *)

+ 13 - 9
completions/completions.fish → completions/fish/exa.fish

@@ -10,10 +10,14 @@ complete -c exa -s 'x' -l 'across'       -d "Sort the grid across, rather than d
 complete -c exa -s 'R' -l 'recurse'      -d "Recurse into directories"
 complete -c exa -s 'R' -l 'recurse'      -d "Recurse into directories"
 complete -c exa -s 'T' -l 'tree'         -d "Recurse into directories as a tree"
 complete -c exa -s 'T' -l 'tree'         -d "Recurse into directories as a tree"
 complete -c exa -s 'F' -l 'classify'     -d "Display type indicator by file names"
 complete -c exa -s 'F' -l 'classify'     -d "Display type indicator by file names"
-complete -c exa        -l 'color'        -d "When to use terminal colours"
-complete -c exa        -l 'colour'       -d "When to use terminal colours"
-complete -c exa        -l 'color-scale'  -d "Highlight levels of file sizes distinctly"
-complete -c exa        -l 'colour-scale' -d "Highlight levels of file sizes distinctly"
+complete -c exa        -l 'color' \
+                       -l 'colour'       -d "When to use terminal colours" -x -a "
+    always\t'Always use colour'
+    auto\t'Use colour if standard output is a terminal'
+    never\t'Never use colour'
+"
+complete -c exa        -l 'color-scale' \
+                       -l 'colour-scale' -d "Highlight levels of file sizes distinctly"
 complete -c exa        -l 'icons'        -d "Display icons"
 complete -c exa        -l 'icons'        -d "Display icons"
 complete -c exa        -l 'no-icons'     -d "Don't display icons"
 complete -c exa        -l 'no-icons'     -d "Don't display icons"
 
 
@@ -22,9 +26,9 @@ complete -c exa -l 'group-directories-first' -d "Sort directories before other f
 complete -c exa -l 'git-ignore'           -d "Ignore files mentioned in '.gitignore'"
 complete -c exa -l 'git-ignore'           -d "Ignore files mentioned in '.gitignore'"
 complete -c exa -s 'a' -l 'all'       -d "Show hidden and 'dot' files"
 complete -c exa -s 'a' -l 'all'       -d "Show hidden and 'dot' files"
 complete -c exa -s 'd' -l 'list-dirs' -d "List directories like regular files"
 complete -c exa -s 'd' -l 'list-dirs' -d "List directories like regular files"
-complete -c exa -s 'L' -l 'level'     -d "Limit the depth of recursion" -a "1 2 3 4 5 6 7 8 9"
+complete -c exa -s 'L' -l 'level'     -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9"
 complete -c exa -s 'r' -l 'reverse'   -d "Reverse the sort order"
 complete -c exa -s 'r' -l 'reverse'   -d "Reverse the sort order"
-complete -c exa -s 's' -l 'sort'   -x -d "Which field to sort by" -a "
+complete -c exa -s 's' -l 'sort'      -d "Which field to sort by" -x -a "
     accessed\t'Sort by file accessed time'
     accessed\t'Sort by file accessed time'
     age\t'Sort by file modified time (newest first)'
     age\t'Sort by file modified time (newest first)'
     changed\t'Sort by changed time'
     changed\t'Sort by changed time'
@@ -56,10 +60,10 @@ complete -c exa -s 'b' -l 'binary'   -d "List file sizes with binary prefixes"
 complete -c exa -s 'B' -l 'bytes'    -d "List file sizes in bytes, without any prefixes"
 complete -c exa -s 'B' -l 'bytes'    -d "List file sizes in bytes, without any prefixes"
 complete -c exa -s 'g' -l 'group'    -d "List each file's group"
 complete -c exa -s 'g' -l 'group'    -d "List each file's group"
 complete -c exa -s 'h' -l 'header'   -d "Add a header row to each column"
 complete -c exa -s 'h' -l 'header'   -d "Add a header row to each column"
-complete -c exa -s 'h' -l 'links'    -d "List each file's number of hard links"
+complete -c exa -s 'H' -l 'links'    -d "List each file's number of hard links"
 complete -c exa -s 'g' -l 'group'    -d "List each file's inode number"
 complete -c exa -s 'g' -l 'group'    -d "List each file's inode number"
 complete -c exa -s 'S' -l 'blocks'   -d "List each file's number of filesystem blocks"
 complete -c exa -s 'S' -l 'blocks'   -d "List each file's number of filesystem blocks"
-complete -c exa -s 't' -l 'time'  -x -d "Which timestamp field to list" -a "
+complete -c exa -s 't' -l 'time'     -d "Which timestamp field to list" -x -a "
     modified\t'Display modified time'
     modified\t'Display modified time'
     changed\t'Display changed time'
     changed\t'Display changed time'
     accessed\t'Display accessed time'
     accessed\t'Display accessed time'
@@ -70,7 +74,7 @@ complete -c exa -s 'n' -l 'numeric'       -d "List numeric user and group IDs."
 complete -c exa        -l 'changed'       -d "Use the changed timestamp field"
 complete -c exa        -l 'changed'       -d "Use the changed timestamp field"
 complete -c exa -s 'u' -l 'accessed'      -d "Use the accessed timestamp field"
 complete -c exa -s 'u' -l 'accessed'      -d "Use the accessed timestamp field"
 complete -c exa -s 'U' -l 'created'       -d "Use the created timestamp field"
 complete -c exa -s 'U' -l 'created'       -d "Use the created timestamp field"
-complete -c exa        -l 'time-style' -x -d "How to format timestamps" -a "
+complete -c exa        -l 'time-style'    -d "How to format timestamps" -x -a "
     default\t'Use the default time style'
     default\t'Use the default time style'
     iso\t'Display brief ISO timestamps'
     iso\t'Display brief ISO timestamps'
     long-iso\t'Display longer ISO timestaps, up to the minute'
     long-iso\t'Display longer ISO timestaps, up to the minute'

+ 2 - 2
completions/completions.zsh → completions/zsh/_exa

@@ -1,7 +1,7 @@
 #compdef exa
 #compdef exa
 
 
 # Save this file as _exa in /usr/local/share/zsh/site-functions or in any
 # Save this file as _exa in /usr/local/share/zsh/site-functions or in any
-# other folder in $fpath.  E. g. save it in a folder called ~/.zfunc and add a
+# other folder in $fpath.  E.g. save it in a folder called ~/.zfunc and add a
 # line containing `fpath=(~/.zfunc $fpath)` somewhere before `compinit` in your
 # line containing `fpath=(~/.zfunc $fpath)` somewhere before `compinit` in your
 # ~/.zshrc.
 # ~/.zshrc.
 
 
@@ -19,7 +19,7 @@ __exa() {
         {-R,--recurse}"[Recurse into directories]" \
         {-R,--recurse}"[Recurse into directories]" \
         {-T,--tree}"[Recurse into directories as a tree]" \
         {-T,--tree}"[Recurse into directories as a tree]" \
         {-F,--classify}"[Display type indicator by file names]" \
         {-F,--classify}"[Display type indicator by file names]" \
-        --colo{,u}r"[When to use terminal colours]" \
+        --colo{,u}r="[When to use terminal colours]:(when):(always auto never)" \
         --colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \
         --colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \
         --icons"[Display icons]" \
         --icons"[Display icons]" \
         --no-icons"[Hide icons]" \
         --no-icons"[Hide icons]" \

+ 12 - 11
devtools/dev-bash.sh

@@ -11,17 +11,17 @@ bash /vagrant/devtools/dev-versions.sh
 # Configure the Cool Prompt™ (not actually trademarked).
 # Configure the Cool Prompt™ (not actually trademarked).
 # The Cool Prompt tells you whether you’re in debug or strict mode, whether
 # The Cool Prompt tells you whether you’re in debug or strict mode, whether
 # you have colours configured, and whether your last command failed.
 # you have colours configured, and whether your last command failed.
-function nonzero_return() { RETVAL=$?; [ $RETVAL -ne 0 ] && echo "$RETVAL "; }
-function debug_mode()  { [ "$EXA_DEBUG" == "trace" ] && echo -n "trace-"; [ -n "$EXA_DEBUG" ]  && echo "debug "; }
-function strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; }
-function lsc_mode()    { [ -n "$LS_COLORS" ]  && echo "lsc "; }
-function exac_mode()   { [ -n "$EXA_COLORS" ] && echo "exac "; }
+nonzero_return() { RETVAL=$?; [ "$RETVAL" -ne 0 ] && echo "$RETVAL "; }
+debug_mode()  { [ "$EXA_DEBUG" == "trace" ] && echo -n "trace-"; [ -n "$EXA_DEBUG" ] && echo "debug "; }
+strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; }
+lsc_mode()    { [ -n "$LS_COLORS" ]  && echo "lsc "; }
+exac_mode()   { [ -n "$EXA_COLORS" ] && echo "exac "; }
 export PS1="\[\e[1;36m\]\h \[\e[32m\]\w \[\e[31m\]\`nonzero_return\`\[\e[35m\]\`debug_mode\`\[\e[32m\]\`lsc_mode\`\[\e[1;32m\]\`exac_mode\`\[\e[33m\]\`strict_mode\`\[\e[36m\]\\$\[\e[0m\] "
 export PS1="\[\e[1;36m\]\h \[\e[32m\]\w \[\e[31m\]\`nonzero_return\`\[\e[35m\]\`debug_mode\`\[\e[32m\]\`lsc_mode\`\[\e[1;32m\]\`exac_mode\`\[\e[33m\]\`strict_mode\`\[\e[36m\]\\$\[\e[0m\] "
 
 
 
 
 # The ‘debug’ function lets you switch debug mode on and off.
 # The ‘debug’ function lets you switch debug mode on and off.
 # Turn it on if you need to see exa’s debugging logs.
 # Turn it on if you need to see exa’s debugging logs.
-function debug () {
+debug() {
   case "$1" in
   case "$1" in
     ""|"on")  export EXA_DEBUG=1 ;;
     ""|"on")  export EXA_DEBUG=1 ;;
     "off")    export EXA_DEBUG= ;;
     "off")    export EXA_DEBUG= ;;
@@ -33,11 +33,12 @@ function debug () {
 
 
 # The ‘strict’ function lets you switch strict mode on and off.
 # The ‘strict’ function lets you switch strict mode on and off.
 # Turn it on if you’d like exa’s command-line arguments checked.
 # Turn it on if you’d like exa’s command-line arguments checked.
-function strict () {
-  case "$1" in "on") export EXA_STRICT=1 ;;
+strict() {
+  case "$1" in
+    "on")  export EXA_STRICT=1 ;;
     "off") export EXA_STRICT= ;;
     "off") export EXA_STRICT= ;;
-    "") [ -n "$EXA_STRICT" ] && echo "strict on" || echo "strict off" ;;
-    *) echo "Usage: strict on|off"; return 1 ;;
+    "")    [ -n "$EXA_STRICT" ] && echo "strict on" || echo "strict off" ;;
+    *)     echo "Usage: strict on|off"; return 1 ;;
   esac;
   esac;
 }
 }
 
 
@@ -45,7 +46,7 @@ function strict () {
 # environment variables. There’s also a ‘hacker’ theme which turns everything
 # environment variables. There’s also a ‘hacker’ theme which turns everything
 # green, which is usually used for checking that all colour codes work, and
 # green, which is usually used for checking that all colour codes work, and
 # for looking cool while you phreak some mainframes or whatever.
 # for looking cool while you phreak some mainframes or whatever.
-function colors () {
+colors() {
   case "$1" in
   case "$1" in
     "ls")
     "ls")
       export LS_COLORS="di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43"
       export LS_COLORS="di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43"

+ 17 - 3
devtools/dev-create-test-filesystem.sh

@@ -252,7 +252,7 @@ sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/attributes"
 
 
 # A sample Git repository
 # A sample Git repository
 # This uses cd because it's easier than telling Git where to go each time
 # This uses cd because it's easier than telling Git where to go each time
-echo -e "\033[1m[10/13]\033[0m Creating Git testcases (1/3)"
+echo -e "\033[1m[10/13]\033[0m Creating Git testcases (1/4)"
 mkdir "$TEST_ROOT/git"
 mkdir "$TEST_ROOT/git"
 cd    "$TEST_ROOT/git"
 cd    "$TEST_ROOT/git"
 git init >/dev/null
 git init >/dev/null
@@ -281,7 +281,7 @@ sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git"
 
 
 # A second Git repository
 # A second Git repository
 # for testing two at once
 # for testing two at once
-echo -e "\033[1m[11/13]\033[0m Creating Git testcases (2/3)"
+echo -e "\033[1m[11/13]\033[0m Creating Git testcases (2/4)"
 mkdir -p "$TEST_ROOT/git2/deeply/nested/directory"
 mkdir -p "$TEST_ROOT/git2/deeply/nested/directory"
 cd       "$TEST_ROOT/git2"
 cd       "$TEST_ROOT/git2"
 git init >/dev/null
 git init >/dev/null
@@ -321,7 +321,7 @@ sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git2"
 
 
 # A third Git repository
 # A third Git repository
 # Regression test for https://github.com/ogham/exa/issues/526
 # Regression test for https://github.com/ogham/exa/issues/526
-echo -e "\033[1m[12/13]\033[0m Creating Git testcases (3/3)"
+echo -e "\033[1m[12/13]\033[0m Creating Git testcases (3/4)"
 mkdir -p "$TEST_ROOT/git3"
 mkdir -p "$TEST_ROOT/git3"
 cd       "$TEST_ROOT/git3"
 cd       "$TEST_ROOT/git3"
 git init >/dev/null
 git init >/dev/null
@@ -334,6 +334,20 @@ find "$TEST_ROOT/git3" -exec touch {} -h -t $FIXED_DATE \;
 sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git3"
 sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git3"
 
 
 
 
+# A fourth Git repository
+# Regression test for https://github.com/ogham/exa/issues/698
+echo -e "\033[1m[12/13]\033[0m Creating Git testcases (4/4)"
+mkdir -p "$TEST_ROOT/git4"
+cd       "$TEST_ROOT/git4"
+git init >/dev/null
+
+# Create a non UTF-8 file
+touch 'P'$'\b\211''UUU'
+
+find "$TEST_ROOT/git4" -exec touch {} -h -t $FIXED_DATE \;
+sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git4"
+
+
 # Hidden and dot file testcases.
 # Hidden and dot file testcases.
 # We need to set the permissions of `.` and `..` because they actually
 # We need to set the permissions of `.` and `..` because they actually
 # get displayed in the output here, so this has to come last.
 # get displayed in the output here, so this has to come last.

+ 7 - 6
devtools/dev-package-for-linux.sh

@@ -9,7 +9,7 @@ set -e
 
 
 
 
 # Linux check!
 # Linux check!
-uname=`uname -s`
+uname=$(uname -s)
 if [[ "$uname" != "Linux" ]]; then
 if [[ "$uname" != "Linux" ]]; then
   echo "Gotta be on Linux to run this (detected '$uname')!"
   echo "Gotta be on Linux to run this (detected '$uname')!"
   exit 1
   exit 1
@@ -29,8 +29,8 @@ fi
 
 
 # Weekly builds have a bit more information in their version number (see build.rs).
 # Weekly builds have a bit more information in their version number (see build.rs).
 if [[ "$1" == "--weekly" ]]; then
 if [[ "$1" == "--weekly" ]]; then
-  git_hash=`GIT_DIR=/vagrant/.git git rev-parse --short --verify HEAD`
-  date=`date +"%Y-%m-%d"`
+  git_hash=$(GIT_DIR=/vagrant/.git git rev-parse --short --verify HEAD)
+  date=$(date +"%Y-%m-%d")
   echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash"
   echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash"
 else
 else
   echo "Building exa v$exa_version"
   echo "Building exa v$exa_version"
@@ -57,9 +57,10 @@ strip -v "$exa_linux_binary"
 # the binaries can have consistent names, and it’s still possible to tell
 # the binaries can have consistent names, and it’s still possible to tell
 # different *downloads* apart.
 # different *downloads* apart.
 echo -e "\n\033[4mZipping binary...\033[0m"
 echo -e "\n\033[4mZipping binary...\033[0m"
-if [[ "$1" == "--weekly" ]]
-  then exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}-${date}-${git_hash}.zip"
-  else exa_linux_zip="/vagrant/exa-linux-x86_64.zip"
+if [[ "$1" == "--weekly" ]]; then
+  exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}-${date}-${git_hash}.zip"
+else
+  exa_linux_zip="/vagrant/exa-linux-x86_64.zip"
 fi
 fi
 rm -vf "$exa_linux_zip"
 rm -vf "$exa_linux_zip"
 zip -j "$exa_linux_zip" "$exa_linux_binary"
 zip -j "$exa_linux_zip" "$exa_linux_binary"

+ 7 - 6
devtools/local-package-for-macos.sh

@@ -11,7 +11,7 @@ set -e
 
 
 # Virtualising macOS is a legal minefield, so this script is ‘local’ instead
 # Virtualising macOS is a legal minefield, so this script is ‘local’ instead
 # of ‘dev’: I run it from my actual machine, rather than from a VM.
 # of ‘dev’: I run it from my actual machine, rather than from a VM.
-uname=`uname -s`
+uname=$(uname -s)
 if [[ "$uname" != "Darwin" ]]; then
 if [[ "$uname" != "Darwin" ]]; then
   echo "Gotta be on Darwin to run this (detected '$uname')!"
   echo "Gotta be on Darwin to run this (detected '$uname')!"
   exit 1
   exit 1
@@ -36,8 +36,8 @@ fi
 
 
 # Weekly builds have a bit more information in their version number (see build.rs).
 # Weekly builds have a bit more information in their version number (see build.rs).
 if [[ "$1" == "--weekly" ]]; then
 if [[ "$1" == "--weekly" ]]; then
-  git_hash=`GIT_DIR=$exa_root/.git git rev-parse --short --verify HEAD`
-  date=`date +"%Y-%m-%d"`
+  git_hash=$(GIT_DIR=$exa_root/.git git rev-parse --short --verify HEAD)
+  date=$(date +"%Y-%m-%d")
   echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash"
   echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash"
 else
 else
   echo "Building exa v$exa_version"
   echo "Building exa v$exa_version"
@@ -65,9 +65,10 @@ echo "strip $exa_macos_binary"
 # the binaries can have consistent names, and it’s still possible to tell
 # the binaries can have consistent names, and it’s still possible to tell
 # different *downloads* apart.
 # different *downloads* apart.
 echo -e "\n\033[4mZipping binary...\033[0m"
 echo -e "\n\033[4mZipping binary...\033[0m"
-if [[ "$1" == "--weekly" ]]
-  then exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}-${date}-${git_hash}.zip"
-  else exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}.zip"
+if [[ "$1" == "--weekly" ]]; then
+  exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}-${date}-${git_hash}.zip"
+else
+  exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}.zip"
 fi
 fi
 rm -vf "$exa_macos_zip" | sed 's/^/removing /'
 rm -vf "$exa_macos_zip" | sed 's/^/removing /'
 zip -j "$exa_macos_zip" "$exa_macos_binary"
 zip -j "$exa_macos_zip" "$exa_macos_binary"

+ 1 - 1
src/fs/dir_action.rs

@@ -51,7 +51,7 @@ impl DirAction {
         match self {
         match self {
             Self::AsFile      => true,
             Self::AsFile      => true,
             Self::Recurse(o)  => o.tree,
             Self::Recurse(o)  => o.tree,
-            _                 => false,
+            Self::List        => false,
         }
         }
     }
     }
 }
 }

+ 8 - 0
src/fs/feature/git.rs

@@ -1,5 +1,8 @@
 //! Getting the Git status of files and directories.
 //! Getting the Git status of files and directories.
 
 
+use std::ffi::OsStr;
+#[cfg(target_family = "unix")]
+use std::os::unix::ffi::OsStrExt;
 use std::path::{Path, PathBuf};
 use std::path::{Path, PathBuf};
 use std::sync::Mutex;
 use std::sync::Mutex;
 
 
@@ -205,6 +208,11 @@ fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git {
     match repo.statuses(None) {
     match repo.statuses(None) {
         Ok(es) => {
         Ok(es) => {
             for e in es.iter() {
             for e in es.iter() {
+                #[cfg(target_family = "unix")]
+                let path = workdir.join(Path::new(OsStr::from_bytes(e.path_bytes())));
+                // TODO: handle non Unix systems better:
+                // https://github.com/ogham/exa/issues/698
+                #[cfg(not(target_family = "unix"))]
                 let path = workdir.join(Path::new(e.path().unwrap()));
                 let path = workdir.join(Path::new(e.path().unwrap()));
                 let elem = (path, e.status());
                 let elem = (path, e.status());
                 statuses.push(elem);
                 statuses.push(elem);

+ 7 - 7
src/info/filetype.rs

@@ -26,7 +26,7 @@ impl FileExtensions {
         file.name_is_one_of( &[
         file.name_is_one_of( &[
             "Makefile", "Cargo.toml", "SConstruct", "CMakeLists.txt",
             "Makefile", "Cargo.toml", "SConstruct", "CMakeLists.txt",
             "build.gradle", "pom.xml", "Rakefile", "package.json", "Gruntfile.js",
             "build.gradle", "pom.xml", "Rakefile", "package.json", "Gruntfile.js",
-            "Gruntfile.coffee", "BUILD", "BUILD.bazel", "WORKSPACE", "build.xml",
+            "Gruntfile.coffee", "BUILD", "BUILD.bazel", "WORKSPACE", "build.xml", "Podfile",
             "webpack.config.js", "meson.build", "composer.json", "RoboFile.php", "PKGBUILD",
             "webpack.config.js", "meson.build", "composer.json", "RoboFile.php", "PKGBUILD",
             "Justfile", "Procfile", "Dockerfile", "Containerfile", "Vagrantfile", "Brewfile",
             "Justfile", "Procfile", "Dockerfile", "Containerfile", "Vagrantfile", "Brewfile",
             "Gemfile", "Pipfile", "build.sbt", "mix.exs", "bsconfig.json", "tsconfig.json",
             "Gemfile", "Pipfile", "build.sbt", "mix.exs", "bsconfig.json", "tsconfig.json",
@@ -35,10 +35,10 @@ impl FileExtensions {
 
 
     fn is_image(&self, file: &File<'_>) -> bool {
     fn is_image(&self, file: &File<'_>) -> bool {
         file.extension_is_one_of( &[
         file.extension_is_one_of( &[
-            "png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif",
-            "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw",
-            "svg", "stl", "eps", "dvi", "ps", "cbr", "jpf",
-            "cbz", "xpm", "ico", "cr2", "orf", "nef", "heif",
+            "png", "jfi", "jfif", "jif", "jpe", "jpeg", "jpg", "gif", "bmp",
+            "tiff", "tif", "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw",
+            "svg", "stl", "eps", "dvi", "ps", "cbr", "jpf", "cbz", "xpm",
+            "ico", "cr2", "orf", "nef", "heif", "avif", "jxl",
         ])
         ])
     }
     }
 
 
@@ -80,14 +80,14 @@ impl FileExtensions {
         file.extension_is_one_of( &[
         file.extension_is_one_of( &[
             "zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z",
             "zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z",
             "iso", "dmg", "tc", "rar", "par", "tgz", "xz", "txz",
             "iso", "dmg", "tc", "rar", "par", "tgz", "xz", "txz",
-            "lz", "tlz", "lzma", "deb", "rpm", "zst",
+            "lz", "tlz", "lzma", "deb", "rpm", "zst", "lz4",
         ])
         ])
     }
     }
 
 
     fn is_temp(&self, file: &File<'_>) -> bool {
     fn is_temp(&self, file: &File<'_>) -> bool {
         file.name.ends_with('~')
         file.name.ends_with('~')
             || (file.name.starts_with('#') && file.name.ends_with('#'))
             || (file.name.starts_with('#') && file.name.ends_with('#'))
-            || file.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak", "bk" ])
+            || file.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak", "bkp", "bk" ])
     }
     }
 
 
     fn is_compiled(&self, file: &File<'_>) -> bool {
     fn is_compiled(&self, file: &File<'_>) -> bool {

+ 1 - 0
src/main.rs

@@ -18,6 +18,7 @@
 #![allow(clippy::non_ascii_literal)]
 #![allow(clippy::non_ascii_literal)]
 #![allow(clippy::option_if_let_else)]
 #![allow(clippy::option_if_let_else)]
 #![allow(clippy::too_many_lines)]
 #![allow(clippy::too_many_lines)]
+#![allow(clippy::unnested_or_patterns)] // TODO: remove this when we support Rust 1.53.0
 #![allow(clippy::unused_self)]
 #![allow(clippy::unused_self)]
 #![allow(clippy::upper_case_acronyms)]
 #![allow(clippy::upper_case_acronyms)]
 #![allow(clippy::wildcard_imports)]
 #![allow(clippy::wildcard_imports)]

+ 1 - 1
src/options/parser.rs

@@ -430,7 +430,7 @@ impl<'a> MatchedFlags<'a> {
                             .filter(|tuple| tuple.1.is_some() && predicate(&tuple.0))
                             .filter(|tuple| tuple.1.is_some() && predicate(&tuple.0))
                             .collect::<Vec<_>>();
                             .collect::<Vec<_>>();
 
 
-            if those.len() < 2 { Ok(those.first().cloned().map(|t| t.1.unwrap())) }
+            if those.len() < 2 { Ok(those.first().copied().map(|t| t.1.unwrap())) }
                           else { Err(OptionsError::Duplicate(those[0].0, those[1].0)) }
                           else { Err(OptionsError::Duplicate(those[0].0, those[1].0)) }
         }
         }
         else {
         else {

+ 2 - 2
src/output/grid_details.rs

@@ -158,7 +158,7 @@ impl<'a> Render<'a> {
                              .collect::<Vec<_>>();
                              .collect::<Vec<_>>();
 
 
         let mut last_working_grid = self.make_grid(1, options, &file_names, rows.clone(), &drender);
         let mut last_working_grid = self.make_grid(1, options, &file_names, rows.clone(), &drender);
-        
+
         if file_names.len() == 1 {
         if file_names.len() == 1 {
             return Some((last_working_grid, 1));
             return Some((last_working_grid, 1));
         }
         }
@@ -176,7 +176,7 @@ impl<'a> Render<'a> {
             if the_grid_fits {
             if the_grid_fits {
                 last_working_grid = grid;
                 last_working_grid = grid;
             }
             }
-            
+
             if !the_grid_fits || column_count == file_names.len() {
             if !the_grid_fits || column_count == file_names.len() {
                 let last_column_count = if the_grid_fits { column_count } else { column_count - 1 };
                 let last_column_count = if the_grid_fits { column_count } else { column_count - 1 };
                 // If we’ve figured out how many columns can fit in the user’s terminal,
                 // If we’ve figured out how many columns can fit in the user’s terminal,

+ 13 - 1
src/output/icons.rs

@@ -80,7 +80,7 @@ lazy_static! {
         m.insert("include", '\u{e5fc}'); // 
         m.insert("include", '\u{e5fc}'); // 
         m.insert("lib", '\u{f121}'); // 
         m.insert("lib", '\u{f121}'); // 
         m.insert("localized", '\u{f179}'); // 
         m.insert("localized", '\u{f179}'); // 
-        m.insert("Makefile", '\u{e779}'); // 
+        m.insert("Makefile", '\u{f489}'); // 
         m.insert("node_modules", '\u{e718}'); // 
         m.insert("node_modules", '\u{e718}'); // 
         m.insert("npmignore", '\u{e71e}'); // 
         m.insert("npmignore", '\u{e71e}'); // 
         m.insert("rubydoc", '\u{e73b}'); // 
         m.insert("rubydoc", '\u{e73b}'); // 
@@ -110,6 +110,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
             "apk"           => '\u{e70e}', // 
             "apk"           => '\u{e70e}', // 
             "apple"         => '\u{f179}', // 
             "apple"         => '\u{f179}', // 
             "avi"           => '\u{f03d}', // 
             "avi"           => '\u{f03d}', // 
+            "avif"          => '\u{f1c5}', // 
             "avro"          => '\u{e60b}', // 
             "avro"          => '\u{e60b}', // 
             "awk"           => '\u{f489}', // 
             "awk"           => '\u{f489}', // 
             "bash"          => '\u{f489}', // 
             "bash"          => '\u{f489}', // 
@@ -206,11 +207,16 @@ pub fn icon_for_file(file: &File<'_>) -> char {
             "jad"           => '\u{e256}', // 
             "jad"           => '\u{e256}', // 
             "jar"           => '\u{e204}', // 
             "jar"           => '\u{e204}', // 
             "java"          => '\u{e204}', // 
             "java"          => '\u{e204}', // 
+            "jfi"           => '\u{f1c5}', // 
+            "jfif"          => '\u{f1c5}', // 
+            "jif"           => '\u{f1c5}', // 
+            "jpe"           => '\u{f1c5}', // 
             "jpeg"          => '\u{f1c5}', // 
             "jpeg"          => '\u{f1c5}', // 
             "jpg"           => '\u{f1c5}', // 
             "jpg"           => '\u{f1c5}', // 
             "js"            => '\u{e74e}', // 
             "js"            => '\u{e74e}', // 
             "json"          => '\u{e60b}', // 
             "json"          => '\u{e60b}', // 
             "jsx"           => '\u{e7ba}', // 
             "jsx"           => '\u{e7ba}', // 
+            "jxl"           => '\u{f1c5}', // 
             "ksh"           => '\u{f489}', // 
             "ksh"           => '\u{f489}', // 
             "latex"         => '\u{f034}', // 
             "latex"         => '\u{f034}', // 
             "less"          => '\u{e758}', // 
             "less"          => '\u{e758}', // 
@@ -221,6 +227,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
             "log"           => '\u{f18d}', // 
             "log"           => '\u{f18d}', // 
             "lua"           => '\u{e620}', // 
             "lua"           => '\u{e620}', // 
             "lz"            => '\u{f410}', // 
             "lz"            => '\u{f410}', // 
+            "lz4"           => '\u{f410}', // 
             "lzh"           => '\u{f410}', // 
             "lzh"           => '\u{f410}', // 
             "lzma"          => '\u{f410}', // 
             "lzma"          => '\u{f410}', // 
             "lzo"           => '\u{f410}', // 
             "lzo"           => '\u{f410}', // 
@@ -230,6 +237,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
             "markdown"      => '\u{f48a}', // 
             "markdown"      => '\u{f48a}', // 
             "md"            => '\u{f48a}', // 
             "md"            => '\u{f48a}', // 
             "mjs"           => '\u{e74e}', // 
             "mjs"           => '\u{e74e}', // 
+            "mk"            => '\u{f489}', // 
             "mkd"           => '\u{f48a}', // 
             "mkd"           => '\u{f48a}', // 
             "mkv"           => '\u{f03d}', // 
             "mkv"           => '\u{f03d}', // 
             "mobi"          => '\u{e28b}', // 
             "mobi"          => '\u{e28b}', // 
@@ -292,6 +300,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
             "so"            => '\u{f17c}', // 
             "so"            => '\u{f17c}', // 
             "sql"           => '\u{f1c0}', // 
             "sql"           => '\u{f1c0}', // 
             "sqlite3"       => '\u{e7c4}', // 
             "sqlite3"       => '\u{e7c4}', // 
+            "sty"           => '\u{f034}', // 
             "styl"          => '\u{e600}', // 
             "styl"          => '\u{e600}', // 
             "stylus"        => '\u{e600}', // 
             "stylus"        => '\u{e600}', // 
             "svg"           => '\u{f1c5}', // 
             "svg"           => '\u{f1c5}', // 
@@ -301,7 +310,9 @@ pub fn icon_for_file(file: &File<'_>) -> char {
             "tbz"           => '\u{f410}', // 
             "tbz"           => '\u{f410}', // 
             "tbz2"          => '\u{f410}', // 
             "tbz2"          => '\u{f410}', // 
             "tex"           => '\u{f034}', // 
             "tex"           => '\u{f034}', // 
+            "tgz"           => '\u{f410}', // 
             "tiff"          => '\u{f1c5}', // 
             "tiff"          => '\u{f1c5}', // 
+            "tlz"           => '\u{f410}', // 
             "toml"          => '\u{e615}', // 
             "toml"          => '\u{e615}', // 
             "ts"            => '\u{e628}', // 
             "ts"            => '\u{e628}', // 
             "tsv"           => '\u{f1c3}', // 
             "tsv"           => '\u{f1c3}', // 
@@ -309,6 +320,7 @@ pub fn icon_for_file(file: &File<'_>) -> char {
             "ttf"           => '\u{f031}', // 
             "ttf"           => '\u{f031}', // 
             "twig"          => '\u{e61c}', // 
             "twig"          => '\u{e61c}', // 
             "txt"           => '\u{f15c}', // 
             "txt"           => '\u{f15c}', // 
+            "txz"           => '\u{f410}', // 
             "tz"            => '\u{f410}', // 
             "tz"            => '\u{f410}', // 
             "tzo"           => '\u{f410}', // 
             "tzo"           => '\u{f410}', // 
             "video"         => '\u{f03d}', // 
             "video"         => '\u{f03d}', // 

+ 1 - 1
src/output/render/size.rs

@@ -46,7 +46,7 @@ impl f::Size {
         } else {
         } else {
             numerics.format_int(n.round() as isize)
             numerics.format_int(n.round() as isize)
         };
         };
-        
+
         TextCell {
         TextCell {
             // symbol is guaranteed to be ASCII since unit prefixes are hardcoded.
             // symbol is guaranteed to be ASCII since unit prefixes are hardcoded.
             width: DisplayWidth::from(&*number) + symbol.len(),
             width: DisplayWidth::from(&*number) + symbol.len(),

+ 12 - 0
xtests/git.toml

@@ -144,6 +144,18 @@ status = 0
 tags = [ 'long', 'git' ]
 tags = [ 'long', 'git' ]
 
 
 
 
+
+# The forth Git repo: non UTF-8 file
+
+[[cmd]]
+name = "‘exa --git -l’ handles non UTF8 file in Git repositories"
+shell = "exa --git -l /testcases/git4"
+stdout = { file = "outputs/git4_long.ansitxt" }
+stderr = { empty = true }
+status = 0
+tags = [ 'long', 'git' ]
+
+
 # Both repositories 1 and 2 at once
 # Both repositories 1 and 2 at once
 
 
 [[cmd]]
 [[cmd]]

+ 1 - 0
xtests/outputs/git4_long.ansitxt

@@ -0,0 +1 @@
+.rw-rw-r-- 0 cassowary  1 Jan 12:34 -N P\u{8}�UUU