diff --git a/.clippy.toml b/.clippy.toml index 7c7c792b85..3d894c832e 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,2 +1,2 @@ -msrv = "1.70.0" +msrv = "1.81.0" cognitive-complexity-threshold = 18 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b15568ab49..57a9e664cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - rust: [nightly, stable, '1.70'] + rust: [nightly, stable, '1.81'] runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.rust == 'nightly' }} @@ -41,6 +41,9 @@ jobs: toolchain: ${{ matrix.rust }} components: clippy + - name: Override rust toolchain + run: rustup override set ${{ matrix.rust }} + - name: Rustup Show run: rustup show @@ -89,7 +92,7 @@ jobs: strategy: fail-fast: false matrix: - rust: [nightly, stable, '1.70'] + rust: [nightly, stable, '1.81'] continue-on-error: ${{ matrix.rust == 'nightly' }} steps: - uses: actions/checkout@v4 @@ -112,6 +115,12 @@ jobs: - name: Manually install target run: rustup target add x86_64-unknown-linux-musl + - name: Override rust toolchain + run: rustup override set ${{ matrix.rust }} + + - name: Rustup Show + run: rustup show + - name: Setup MUSL run: | sudo apt-get -qq install musl-tools @@ -135,7 +144,7 @@ jobs: strategy: fail-fast: false matrix: - rust: [nightly, stable, '1.70'] + rust: [nightly, stable, '1.81'] continue-on-error: ${{ matrix.rust == 'nightly' }} steps: - uses: actions/checkout@v4 @@ -151,6 +160,10 @@ jobs: uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} + + - name: Override rust toolchain + run: rustup override set ${{ matrix.rust }} + - name: Setup ARM toolchain run: | rustup target add aarch64-unknown-linux-gnu @@ -166,6 +179,9 @@ jobs: echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH + - name: Rustup Show + run: rustup show + - name: Build Debug run: | make build-linux-arm-debug @@ -179,7 +195,7 @@ jobs: strategy: fail-fast: false matrix: - rust: [nightly, stable, '1.70'] + rust: [nightly, stable, '1.81'] continue-on-error: ${{ matrix.rust == 'nightly' }} steps: - uses: actions/checkout@v4 @@ -195,9 +211,16 @@ jobs: uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} + + - name: Override rust toolchain + run: rustup override set ${{ matrix.rust }} + - name: Setup target run: rustup target add x86_64-apple-darwin + - name: Rustup Show + run: rustup show + - name: Build Debug run: | make build-apple-x86-debug @@ -275,3 +298,31 @@ jobs: with: name: release-notes.txt path: ./release-notes.txt + + test-homebrew: + name: Test Homebrew Formula (macOS) + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install stable Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Install Homebrew + run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + + - name: Set up Homebrew in PATH + run: | + echo "$HOMEBREW_PREFIX/bin:$HOMEBREW_PREFIX/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_PATH + + - name: Update Homebrew + run: brew update + + - name: Let Homebrew build gitui from source + run: brew install --head --build-from-source gitui + + - name: Run Homebrew test + run: brew test gitui diff --git a/CHANGELOG.md b/CHANGELOG.md index 012b60e5ff..53799e3457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +* execute git-hooks directly if possible (on *nix) else use sh instead of bash (without reading SHELL variable) [[@Joshix](https://github.com/Joshix-1)] ([#2483](https://github.com/extrawurst/gitui/pull/2483)) ### Added +* Files and status tab support pageUp and pageDown [[@fatpandac](https://github.com/fatpandac)] ([#1951](https://github.com/extrawurst/gitui/issues/1951)) * support loading custom syntax highlighting themes from a file [[@acuteenvy](https://github.com/acuteenvy)] ([#2565](https://github.com/gitui-org/gitui/pull/2565)) * Select syntax highlighting theme out of the defaults from syntect [[@vasilismanol](https://github.com/vasilismanol)] ([#1931](https://github.com/extrawurst/gitui/issues/1931)) * new command-line option to override the default log file path (`--logfile`) [[@acuteenvy](https://github.com/acuteenvy)] ([#2539](https://github.com/gitui-org/gitui/pull/2539)) @@ -15,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * add `use_selection_fg` to theme file to allow customizing selection foreground color [[@Upsylonbare](https://github.com/Upsylonbare)] ([#2515](https://github.com/gitui-org/gitui/pull/2515)) ### Changed +* Respect `XDG_CONFIG_HOME` and `XDG_CACHE_HOME` irrespective of OS [[@KlassyKat](https://github.com/KlassyKat)] ([#1498](https://github.com/gitui-org/gitui/issues/1498)) +* improve error messages [[@acuteenvy](https://github.com/acuteenvy)] ([#2617](https://github.com/gitui-org/gitui/pull/2617)) +* increase MSRV from 1.70 to 1.81 [[@naseschwarz](https://github.com/naseschwarz)] ([#2094](https://github.com/gitui-org/gitui/issues/2094)) * improve syntax highlighting file detection [[@acuteenvy](https://github.com/acuteenvy)] ([#2524](https://github.com/extrawurst/gitui/pull/2524)) * Updated project links to point to `gitui-org` instead of `extrawurst` [[@vasleymus](https://github.com/vasleymus)] ([#2538](https://github.com/gitui-org/gitui/pull/2538)) * After commit: jump back to unstaged area [[@tommady](https://github.com/tommady)] ([#2476](https://github.com/extrawurst/gitui/issues/2476)) @@ -24,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * set the terminal title to `gitui ({repo_path})` [[@acuteenvy](https://github.com/acuteenvy)] ([#2462](https://github.com/gitui-org/gitui/issues/2462)) * respect `.mailmap` [[@acuteenvy](https://github.com/acuteenvy)] ([#2406](https://github.com/gitui-org/gitui/issues/2406)) +### Fixes +* resolve `core.hooksPath` relative to `GIT_WORK_TREE` [[@naseschwarz](https://github.com/naseschwarz)] ([#2571](https://github.com/gitui-org/gitui/issues/2571)) +* yanking commit ranges no longer generates incorrect dotted range notations, but lists each individual commit [[@naseschwarz](https://github.com/naseschwarz)] (https://github.com/gitui-org/gitui/issues/2576) + ## [0.27.0] - 2024-01-14 **new: manage remotes** diff --git a/Cargo.lock b/Cargo.lock index f15e14f983..52e26fd1b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arc-swap" @@ -208,7 +208,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -432,18 +432,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.32" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -525,9 +525,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -714,23 +714,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -834,14 +834,14 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "env_logger" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] @@ -924,9 +924,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "libz-ng-sys", @@ -1138,6 +1138,7 @@ version = "0.4.0" dependencies = [ "git2", "git2-testing", + "gix-path", "log", "pretty_assertions", "shellexpand", @@ -1206,9 +1207,9 @@ dependencies = [ [[package]] name = "gix" -version = "0.70.0" +version = "0.71.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736f14636705f3a56ea52b553e67282519418d9a35bb1e90b3a9637a00296b68" +checksum = "a61e71ec6817fc3c9f12f812682cfe51ee6ea0d2e27e02fc3849c35524617435" dependencies = [ "gix-actor", "gix-commitgraph", @@ -1247,9 +1248,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.33.2" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20018a1a6332e065f1fcc8305c1c932c6b8c9985edea2284b3c79dc6fa3ee4b2" +checksum = "f438c87d4028aca4b82f82ba8d8ab1569823cfb3e5bc5fa8456a71678b2a20e7" dependencies = [ "bstr", "gix-date", @@ -1279,25 +1280,25 @@ dependencies = [ [[package]] name = "gix-command" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb410b84d6575db45e62025a9118bdbf4d4b099ce7575a76161e898d9ca98df1" +checksum = "c0378995847773a697f8e157fe2963ecf3462fe64be05b7b3da000b3b472def8" dependencies = [ "bstr", "gix-path", + "gix-quote", "gix-trace", "shell-words", ] [[package]] name = "gix-commitgraph" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e23a8ec2d8a16026a10dafdb6ed51bcfd08f5d97f20fa52e200bc50cb72e4877" +checksum = "043cbe49b7a7505150db975f3cb7c15833335ac1e26781f615454d9d640a28fe" dependencies = [ "bstr", "gix-chunk", - "gix-features", "gix-hash", "memmap2", "thiserror 2.0.12", @@ -1305,9 +1306,9 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377c1efd2014d5d469e0b3cd2952c8097bce9828f634e04d5665383249f1d9e9" +checksum = "9c6f830bf746604940261b49abf7f655d2c19cadc9f4142ae9379e3a316e8cfa" dependencies = [ "bstr", "gix-config-value", @@ -1326,9 +1327,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.11" +version = "0.14.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11365144ef93082f3403471dbaa94cfe4b5e72743bdb9560719a251d439f4cee" +checksum = "8dc2c844c4cf141884678cabef736fd91dd73068b9146e6f004ba1a0457944b6" dependencies = [ "bitflags 2.9.0", "bstr", @@ -1339,9 +1340,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c57c477b645ee248b173bb1176b52dd528872f12c50375801a58aaf5ae91113f" +checksum = "daa30058ec7d3511fbc229e4f9e696a35abd07ec5b82e635eff864a2726217e4" dependencies = [ "bstr", "itoa", @@ -1351,9 +1352,9 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.50.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62afb7f4ca0acdf4e9dad92065b2eb1bf2993bcc5014b57bc796e3a365b17c4d" +checksum = "a2c975dad2afc85e4e233f444d1efbe436c3cdcf3a07173984509c436d00a3f8" dependencies = [ "bstr", "gix-hash", @@ -1363,9 +1364,9 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c2414bdf04064e0f5a5aa029dfda1e663cf9a6c4bfc8759f2d369299bb65d8" +checksum = "f7fb8a4349b854506a3915de18d3341e5f1daa6b489c8affc9ca0d69efe86781" dependencies = [ "bstr", "dunce", @@ -1379,42 +1380,43 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.40.0" +version = "0.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bfdd4838a8d42bd482c9f0cb526411d003ee94cc7c7b08afe5007329c71d554" +checksum = "016d6050219458d14520fe22bdfdeb9cb71631dec9bc2724767c983f60109634" dependencies = [ "crc32fast", "crossbeam-channel", "flate2", - "gix-hash", + "gix-path", "gix-trace", "gix-utils", "libc", "once_cell", "parking_lot", "prodash", - "sha1", - "sha1_smol", "thiserror 2.0.12", "walkdir", ] [[package]] name = "gix-fs" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "182e7fa7bfdf44ffb7cfe7451b373cdf1e00870ac9a488a49587a110c562063d" +checksum = "951e886120dc5fa8cac053e5e5c89443f12368ca36811b2e43d1539081f9c111" dependencies = [ + "bstr", "fastrand", "gix-features", + "gix-path", "gix-utils", + "thiserror 2.0.12", ] [[package]] name = "gix-glob" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9c7249fa0a78f9b363aa58323db71e0a6161fd69860ed6f48dedf0ef3a314e" +checksum = "20972499c03473e773a2099e5fd0c695b9b72465837797a51a43391a1635a030" dependencies = [ "bitflags 2.9.0", "bstr", @@ -1424,19 +1426,21 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e81c5ec48649b1821b3ed066a44efb95f1a268b35c1d91295e61252539fbe9f8" +checksum = "834e79722063958b03342edaa1e17595cd2939bb2b3306b3225d0815566dcb49" dependencies = [ "faster-hex", + "gix-features", + "sha1-checked", "thiserror 2.0.12", ] [[package]] name = "gix-hashtable" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189130bc372accd02e0520dc5ab1cef318dcc2bc829b76ab8d84bbe90ac212d1" +checksum = "f06066d8702a9186dc1fdc1ed751ff2d7e924ceca21cb5d51b8f990c9c2e014a" dependencies = [ "gix-hash", "hashbrown 0.14.5", @@ -1445,9 +1449,9 @@ dependencies = [ [[package]] name = "gix-index" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd12e3626879369310fffe2ac61acc828613ef656b50c4ea984dd59d7dc85d8" +checksum = "855bece2d4153453aa5d0a80d51deea1ce8cd6a3b4cf213da85ac344ccb908a7" dependencies = [ "bitflags 2.9.0", "bstr", @@ -1473,9 +1477,9 @@ dependencies = [ [[package]] name = "gix-lock" -version = "16.0.0" +version = "17.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9739815270ff6940968441824d162df9433db19211ca9ba8c3fc1b50b849c642" +checksum = "df47b8f11c34520db5541bc5fc9fbc8e4b0bdfcec3736af89ccb1a5728a0126f" dependencies = [ "gix-tempfile", "gix-utils", @@ -1484,9 +1488,9 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.47.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc4b3a0044244f0fe22347fb7a79cca165e37829d668b41b85ff46a43e5fd68" +checksum = "4943fcdae6ffc135920c9ea71e0362ed539182924ab7a85dd9dac8d89b0dd69a" dependencies = [ "bstr", "gix-actor", @@ -1505,9 +1509,9 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.67.0" +version = "0.68.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e93457df69cd09573608ce9fa4f443fbd84bc8d15d8d83adecd471058459c1b" +checksum = "50306d40dcc982eb6b7593103f066ea6289c7b094cb9db14f3cd2be0b9f5e610" dependencies = [ "arc-swap", "gix-date", @@ -1526,9 +1530,9 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.57.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc13a475b3db735617017fb35f816079bf503765312d4b1913b18cf96f3fa515" +checksum = "9b65fffb09393c26624ca408d32cfe8776fb94cd0a5cdf984905e1d2f39779cb" dependencies = [ "clru", "gix-chunk", @@ -1545,9 +1549,9 @@ dependencies = [ [[package]] name = "gix-packetline" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e5ae6bc3ac160a6bf44a55f5537813ca3ddb08549c0fd3e7ef699c73c439cd" +checksum = "123844a70cf4d5352441dc06bab0da8aef61be94ec239cb631e0ba01dc6d3a04" dependencies = [ "bstr", "faster-hex", @@ -1557,9 +1561,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.14" +version = "0.10.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40f12bb65a8299be0cfb90fe718e3be236b7a94b434877012980863a883a99f" +checksum = "f910668e2f6b2a55ff35a1f04df88a1a049f7b868507f4cbeeaa220eaba7be87" dependencies = [ "bstr", "gix-trace", @@ -1570,9 +1574,9 @@ dependencies = [ [[package]] name = "gix-protocol" -version = "0.48.0" +version = "0.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c61bd61afc6b67d213241e2100394c164be421e3f7228d3521b04f48ca5ba90" +checksum = "5678ddae1d62880bc30e2200be1b9387af3372e0e88e21f81b4e7f8367355b5a" dependencies = [ "bstr", "gix-date", @@ -1589,9 +1593,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.4.15" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e49357fccdb0c85c0d3a3292a9f6db32d9b3535959b5471bb9624908f4a066c6" +checksum = "1b005c550bf84de3b24aa5e540a23e6146a1c01c7d30470e35d75a12f827f969" dependencies = [ "bstr", "gix-utils", @@ -1600,9 +1604,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.50.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47adf4c5f933429f8554e95d0d92eee583cfe4b95d2bf665cd6fd4a1531ee20c" +checksum = "b2e1f7eb6b7ce82d2d19961f74bd637bab3ea79b1bc7bfb23dbefc67b0415d8b" dependencies = [ "gix-actor", "gix-features", @@ -1621,9 +1625,9 @@ dependencies = [ [[package]] name = "gix-refspec" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59650228d8f612f68e7f7a25f517fcf386c5d0d39826085492e94766858b0a90" +checksum = "1d8587b21e2264a6e8938d940c5c99662779c13a10741a5737b15fc85c252ffc" dependencies = [ "bstr", "gix-hash", @@ -1635,9 +1639,9 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fe28bbccca55da6d66e6c6efc6bb4003c29d407afd8178380293729733e6b53" +checksum = "342caa4e158df3020cadf62f656307c3948fe4eacfdf67171d7212811860c3e9" dependencies = [ "bitflags 2.9.0", "bstr", @@ -1653,9 +1657,9 @@ dependencies = [ [[package]] name = "gix-revwalk" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ecb80c235b1e9ef2b99b23a81ea50dd569a88a9eb767179793269e0e616247" +checksum = "2dc7c3d7e5cdc1ab8d35130106e4af0a4f9f9eca0c81f4312b690780e92bde0d" dependencies = [ "gix-commitgraph", "gix-date", @@ -1668,9 +1672,9 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.11" +version = "0.10.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d84dae13271f4313f8d60a166bf27e54c968c7c33e2ffd31c48cafe5da649875" +checksum = "47aeb0f13de9ef2f3033f5ff218de30f44db827ac9f1286f9ef050aacddd5888" dependencies = [ "bitflags 2.9.0", "gix-path", @@ -1680,9 +1684,9 @@ dependencies = [ [[package]] name = "gix-shallow" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab72543011e303e52733c85bef784603ef39632ddf47f69723def52825e35066" +checksum = "cc0598aacfe1d52575a21c9492fee086edbb21e228ec36c819c42ab923f434c3" dependencies = [ "bstr", "gix-hash", @@ -1692,9 +1696,9 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "16.0.0" +version = "17.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2558f423945ef24a8328c55d1fd6db06b8376b0e7013b1bb476cc4ffdf678501" +checksum = "3d6de439bbb9a5d3550c9c7fab0e16d2d637d120fcbe0dfbc538772a187f099b" dependencies = [ "gix-fs", "libc", @@ -1711,9 +1715,9 @@ checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7" [[package]] name = "gix-transport" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11187418489477b1b5b862ae1aedbbac77e582f2c4b0ef54280f20cfe5b964d9" +checksum = "b3f68c2870bfca8278389d2484a7f2215b67d0b0cc5277d3c72ad72acf41787e" dependencies = [ "bstr", "gix-command", @@ -1727,9 +1731,9 @@ dependencies = [ [[package]] name = "gix-traverse" -version = "0.44.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bec70e53896586ef32a3efa7e4427b67308531ed186bb6120fb3eca0f0d61b4" +checksum = "36c0b049f8bdb61b20016694102f7b507f2e1727e83e9c5e6dad4f7d84ff7384" dependencies = [ "bitflags 2.9.0", "gix-commitgraph", @@ -1744,9 +1748,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29218c768b53dd8f116045d87fec05b294c731a4b2bdd257eeca2084cc150b13" +checksum = "48dfe23f93f1ddb84977d80bb0dd7aa09d1bf5d5afc0c9b6820cccacc25ae860" dependencies = [ "bstr", "gix-features", @@ -1758,9 +1762,9 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.14" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff08f24e03ac8916c478c8419d7d3c33393da9bb41fa4c24455d5406aeefd35f" +checksum = "189f8724cf903e7fd57cfe0b7bc209db255cacdcb22c781a022f52c3a774f8d0" dependencies = [ "fastrand", "unicode-normalization", @@ -1768,9 +1772,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eaa01c3337d885617c0a42e92823922a2aea71f4caeace6fe87002bdcadbd90" +checksum = "34b5f1253109da6c79ed7cf6e1e38437080bb6d704c76af14c93e2f255234084" dependencies = [ "bstr", "thiserror 2.0.12", @@ -1832,12 +1836,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "iana-time-zone" version = "0.1.61" @@ -2008,9 +2006,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -2101,10 +2099,11 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" -version = "0.1.21" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0ce60560149333a8e41ca7dc78799c47c5fd435e2bc18faf6a054382eec037" +checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" dependencies = [ + "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", @@ -2113,17 +2112,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "jiff-static" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jiff-tzdb" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" [[package]] name = "jiff-tzdb-platform" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ "jiff-tzdb", ] @@ -2314,9 +2324,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" dependencies = [ "adler2", ] @@ -2443,9 +2453,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "onig" @@ -2492,9 +2502,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -2567,7 +2577,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2744,18 +2754,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" -version = "29.0.0" +version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a266d8d6020c61a437be704c5e618037588e1985c7dbb7bf8d265db84cffe325" +checksum = "9ee7ce24c980b976607e2d6ae4aae92827994d23fed71659c3ede3f92528b58b" dependencies = [ "log", "parking_lot", @@ -2861,13 +2871,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom", "libredox", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -2911,9 +2921,9 @@ dependencies = [ [[package]] name = "ron" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63f3aa105dea217ef30d89581b65a4d527a19afc95ef5750be3890e8d3c5b837" +checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" dependencies = [ "base64", "bitflags 2.9.0", @@ -3106,24 +3116,18 @@ dependencies = [ "cfg-if", "cpufeatures", "digest", - "sha1-asm", ] [[package]] -name = "sha1-asm" -version = "0.5.3" +name = "sha1-checked" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "286acebaf8b67c1130aedffad26f594eff0c1292389158135327d2e23aed582b" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ - "cc", + "digest", + "sha1", ] -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - [[package]] name = "sha2" version = "0.10.8" @@ -3149,9 +3153,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "shellexpand" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" dependencies = [ "dirs", ] @@ -3320,18 +3324,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "struct-patch" -version = "0.8.7" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde1b55ce4b9efe4b5c302dea2d0f1297a522963024e160a587a2670c24f3f04" +checksum = "38a8aef6d40b8eda588b2036cf3519526894fdd49305063ee43233c2942136b6" dependencies = [ "struct-patch-derive", ] [[package]] name = "struct-patch-derive" -version = "0.8.7" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac94fea04bf721f57ed7f421e64d3a04858e15708d00e8aa814cad7507427503" +checksum = "8b3e885afd59c2097c651763bf9cc2cd09703f2cff1d7ad6bf6dbcc19e1d0317" dependencies = [ "proc-macro2", "quote", @@ -3368,9 +3372,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.96" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -3813,7 +3817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ "windows-core 0.57.0", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3822,7 +3826,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3834,7 +3838,7 @@ dependencies = [ "windows-implement", "windows-interface", "windows-result", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3871,16 +3875,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -3889,7 +3884,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3898,22 +3893,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -3922,46 +3902,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3974,48 +3936,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4024,9 +3962,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.22" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 75bb1cd9c6..ae7b777f08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.27.0" authors = ["extrawurst "] description = "blazing fast terminal-ui for git" edition = "2021" -rust-version = "1.70" +rust-version = "1.81" exclude = [".github/*", ".vscode/*", "assets/*"] homepage = "https://github.com/gitui-org/gitui" repository = "https://github.com/gitui-org/gitui" @@ -27,7 +27,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] } clap = { version = "4.5", features = ["env", "cargo"] } crossbeam-channel = "0.5" crossterm = { version = "0.28", features = ["serde"] } -dirs = "5.0" +dirs = "6.0" easy-cast = "0.5" filetreelist = { path = "./filetreelist", version = "0.5" } fuzzy-matcher = "0.3" @@ -45,13 +45,13 @@ ratatui = { version = "0.29", default-features = false, features = [ 'serde', ] } rayon-core = "1.12" -ron = "0.9" +ron = "0.10" scopeguard = "1.2" scopetime = { path = "./scopetime", version = "0.1" } serde = "1.0" shellexpand = "3.1" simplelog = { version = "0.12", default-features = false } -struct-patch = "0.8" +struct-patch = "0.9" syntect = { version = "5.2", default-features = false, features = [ "parsing", "default-syntaxes", diff --git a/KEY_CONFIG.md b/KEY_CONFIG.md index 658aad11b6..d43351e023 100644 --- a/KEY_CONFIG.md +++ b/KEY_CONFIG.md @@ -19,12 +19,17 @@ Create a `key_bindings.ron` file like this: ) ``` -The config file format based on the [Ron file format](https://github.com/ron-rs/ron). +The keybinding config file uses the [Ron file format](https://github.com/ron-rs/ron). The location of the file depends on your OS: -* `$HOME/.config/gitui/key_bindings.ron` (mac) -* `$XDG_CONFIG_HOME/gitui/key_bindings.ron` (linux using XDG) -* `$HOME/.config/gitui/key_bindings.ron` (linux) -* `%APPDATA%/gitui/key_bindings.ron` (Windows) +`gitui` will look for an existing `/gitui` in the following order: +* `$XDG_CONFIG_HOME/gitui/` (with `XDG_CONFIG_HOME` set) +* `$HOME/.config/gitui/` +* Default OS Location: + * `$HOME/Library/Application Support/` (mac) + * `$HOME/.config/gitui/` (linux) + * `%APPDATA%/gitui/` (Windows) + +Key bindings are configured in `key_bindings.ron` within your first found `gitui` config folder. See all possible keys to overwrite in gitui: [here](https://github.com/gitui-org/gitui/blob/master/src/keys/key_list.rs#L83) diff --git a/README.md b/README.md index 4b8b90e7d6..3067111396 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ see [NIGHTLIES.md](./NIGHTLIES.md) ### Requirements -- Minimum supported `rust`/`cargo` version: `1.70` +- Minimum supported `rust`/`cargo` version: `1.81` - See [Install Rust](https://www.rust-lang.org/tools/install) - To build openssl dependency (see https://docs.rs/openssl/latest/openssl/) @@ -250,9 +250,9 @@ see [FAQs page](./FAQ.md) To run with logging enabled run `gitui -l`. This will log to: - +- With `XDG_CACHE_HOME` set: `$XDG_CACHE_HOME/gitui/gitui.log` +or default to - macOS: `$HOME/Library/Caches/gitui/gitui.log` -- Linux using `XDG`: `$XDG_CACHE_HOME/gitui/gitui.log` - Linux: `$HOME/.cache/gitui/gitui.log` - Windows: `%LOCALAPPDATA%/gitui/gitui.log` diff --git a/THEMES.md b/THEMES.md index 52c235b496..376334d349 100644 --- a/THEMES.md +++ b/THEMES.md @@ -7,14 +7,19 @@ default on light terminal: To change the colors of the default theme you need to add a `theme.ron` file that contains the colors you want to override. Note that you don’t have to specify the full theme anymore (as of 0.23). Instead, it is sufficient to override just the values that you want to differ from their default values. -The file uses the [Ron format](https://github.com/ron-rs/ron) and is located at one of the following paths, depending on your operating system: - -* `$HOME/.config/gitui/theme.ron` (mac) -* `$XDG_CONFIG_HOME/gitui/theme.ron` (linux using XDG) -* `$HOME/.config/gitui/theme.ron` (linux) -* `%APPDATA%/gitui/theme.ron` (Windows) - -Alternatively, you can create a theme in the same directory mentioned above and use it with the `-t` flag followed by the name of the file in the directory. E.g. If you are on linux calling `gitui -t arc.ron`, this will load the theme in `$XDG_CONFIG_HOME/gitui/arc.ron` or `$HOME/.config/gitui/arc.ron`. +The theme file uses the [Ron file format](https://github.com/ron-rs/ron). +The location of the file depends on your OS: +`gitui` will look for an existing `/gitui` in the following order: +* `$XDG_CONFIG_HOME/gitui/` (with `XDG_CONFIG_HOME` set) +* `$HOME/.config/gitui/` +* Default OS Location: + * `$HOME/Library/Application Support/` (mac) + * `$HOME/.config/gitui/` (linux) + * `%APPDATA%/gitui/` (Windows) + +The theme is configured in `theme.ron` within your first found `gitui` config folder. + +Alternatively, you can create a theme in the same directory mentioned above and use it with the `-t` flag followed by the name of the file in the directory. E.g. Calling `gitui -t arc.ron` will load the `arc.ron` theme from your first found `/gitui` config folder using the logic above. Example theme override: diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index 51971123b3..7966e82e5c 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -14,12 +14,12 @@ keywords = ["git"] [dependencies] bitflags = "2" crossbeam-channel = "0.5" -dirs = "5.0" +dirs = "6.0" easy-cast = "0.5" fuzzy-matcher = "0.3" git2 = "0.20" git2-hooks = { path = "../git2-hooks", version = ">=0.4" } -gix = { version = "0.70.0", default-features = false, features = [ +gix = { version = "0.71.0", default-features = false, features = [ "max-performance", "revision", ] } diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs index 8bd730096e..2113e93d6e 100644 --- a/asyncgit/src/error.rs +++ b/asyncgit/src/error.rs @@ -1,5 +1,3 @@ -#![allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)] - use std::{ num::TryFromIntError, path::StripPrefixError, string::FromUtf8Error, diff --git a/asyncgit/src/sync/branch/mod.rs b/asyncgit/src/sync/branch/mod.rs index 93185d6f45..81ec2ec586 100644 --- a/asyncgit/src/sync/branch/mod.rs +++ b/asyncgit/src/sync/branch/mod.rs @@ -271,7 +271,7 @@ pub fn config_is_pull_rebase(repo_path: &RepoPath) -> Result { let value = rebase.value().map(String::from).unwrap_or_default(); return Ok(value == "true"); - }; + } Ok(false) } @@ -701,7 +701,7 @@ mod tests_branches { &root.as_os_str().to_str().unwrap().into(); let upstream_merge_res = - get_branch_upstream_merge(&repo_path, "master"); + get_branch_upstream_merge(repo_path, "master"); assert!( upstream_merge_res.is_ok_and(|v| v.as_ref().is_none()) ); @@ -721,12 +721,12 @@ mod tests_branches { config .set_str( &format!("branch.{branch_name}.merge"), - &upstrem_merge, + upstrem_merge, ) .expect("fail set branch merge config"); let upstream_merge_res = - get_branch_upstream_merge(&repo_path, &branch_name); + get_branch_upstream_merge(repo_path, branch_name); assert!(upstream_merge_res .as_ref() .is_ok_and(|v| v.as_ref().is_some())); diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs index bb7e6384c5..f4e0b194ea 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -55,7 +55,6 @@ pub fn amend( /// Wrap `Repository::signature` to allow unknown user.name. /// /// See . -#[allow(clippy::redundant_pub_crate)] pub(crate) fn signature_allow_undefined_name( repo: &Repository, ) -> std::result::Result, git2::Error> { diff --git a/asyncgit/src/sync/commits_info.rs b/asyncgit/src/sync/commits_info.rs index df426249e7..111a2b9bce 100644 --- a/asyncgit/src/sync/commits_info.rs +++ b/asyncgit/src/sync/commits_info.rs @@ -49,6 +49,18 @@ impl CommitId { let commit_obj = repo.revparse_single(revision)?; Ok(commit_obj.id().into()) } + + /// Tries to convert a &str representation of a commit id into + /// a `CommitId` + pub fn from_str_unchecked(commit_id_str: &str) -> Result { + match Oid::from_str(commit_id_str) { + Err(e) => Err(crate::Error::Generic(format!( + "Could not convert {}", + e.message() + ))), + Ok(v) => Ok(Self::new(v)), + } + } } impl Display for CommitId { @@ -73,7 +85,7 @@ impl From for CommitId { } /// -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CommitInfo { /// pub message: String, diff --git a/asyncgit/src/sync/config.rs b/asyncgit/src/sync/config.rs index 17a62b6736..9b5d85d823 100644 --- a/asyncgit/src/sync/config.rs +++ b/asyncgit/src/sync/config.rs @@ -1,6 +1,3 @@ -//TODO: hopefully released in next rust (see https://github.com/rust-lang/rust-clippy/issues/9440) -#![allow(clippy::use_self)] - use crate::error::Result; use git2::Repository; use scopetime::scope_time; diff --git a/asyncgit/src/sync/hooks.rs b/asyncgit/src/sync/hooks.rs index 3e82cf6f8d..111b426259 100644 --- a/asyncgit/src/sync/hooks.rs +++ b/asyncgit/src/sync/hooks.rs @@ -74,18 +74,61 @@ pub fn hooks_prepare_commit_msg( #[cfg(test)] mod tests { + use std::{ffi::OsString, io::Write as _, path::Path}; + + use git2::Repository; + use tempfile::TempDir; + use super::*; - use crate::sync::tests::repo_init; + use crate::sync::tests::repo_init_with_prefix; + + fn repo_init() -> Result<(TempDir, Repository)> { + let mut os_string: OsString = OsString::new(); + + os_string.push("gitui $# ' "); + + #[cfg(target_os = "linux")] + { + use std::os::unix::ffi::OsStrExt; + + const INVALID_UTF8: &[u8] = b"\xED\xA0\x80"; + + os_string.push(std::ffi::OsStr::from_bytes(INVALID_UTF8)); + + assert!(os_string.to_str().is_none()); + } + + os_string.push(" "); + + repo_init_with_prefix(os_string) + } + + fn create_hook_in_path(path: &Path, hook_script: &[u8]) { + std::fs::File::create(path) + .unwrap() + .write_all(hook_script) + .unwrap(); + + #[cfg(unix)] + { + std::process::Command::new("chmod") + .arg("+x") + .arg(path) + // .current_dir(path) + .output() + .unwrap(); + } + } #[test] fn test_post_commit_hook_reject_in_subfolder() { let (_td, repo) = repo_init().unwrap(); - let root = repo.path().parent().unwrap(); + let root = repo.workdir().unwrap(); let hook = b"#!/bin/sh echo 'rejected' exit 1 - "; + "; git2_hooks::create_hook( &repo, @@ -96,9 +139,7 @@ mod tests { let subfolder = root.join("foo/"); std::fs::create_dir_all(&subfolder).unwrap(); - let res = - hooks_post_commit(&subfolder.to_str().unwrap().into()) - .unwrap(); + let res = hooks_post_commit(&subfolder.into()).unwrap(); assert_eq!( res, @@ -113,17 +154,13 @@ mod tests { #[cfg(unix)] fn test_pre_commit_workdir() { let (_td, repo) = repo_init().unwrap(); - let root = repo.path().parent().unwrap(); - let repo_path: &RepoPath = - &root.as_os_str().to_str().unwrap().into(); - let workdir = - crate::sync::utils::repo_work_dir(repo_path).unwrap(); + let root = repo.workdir().unwrap(); + let repo_path: &RepoPath = &root.to_path_buf().into(); let hook = b"#!/bin/sh - echo $(pwd) + echo \"$(pwd)\" exit 1 - "; - + "; git2_hooks::create_hook( &repo, git2_hooks::HOOK_PRE_COMMIT, @@ -132,8 +169,9 @@ mod tests { let res = hooks_pre_commit(repo_path).unwrap(); if let HookResult::NotOk(res) = res { assert_eq!( - std::path::Path::new(res.trim_end()), - std::path::Path::new(&workdir) + res.trim_end().trim_end_matches('/'), + // TODO: fix if output isn't utf8. + root.to_string_lossy().trim_end_matches('/'), ); } else { assert!(false); @@ -143,13 +181,13 @@ mod tests { #[test] fn test_hooks_commit_msg_reject_in_subfolder() { let (_td, repo) = repo_init().unwrap(); - let root = repo.path().parent().unwrap(); + let root = repo.workdir().unwrap(); let hook = b"#!/bin/sh - echo 'msg' > $1 + echo 'msg' > \"$1\" echo 'rejected' exit 1 - "; + "; git2_hooks::create_hook( &repo, @@ -161,11 +199,8 @@ mod tests { std::fs::create_dir_all(&subfolder).unwrap(); let mut msg = String::from("test"); - let res = hooks_commit_msg( - &subfolder.to_str().unwrap().into(), - &mut msg, - ) - .unwrap(); + let res = + hooks_commit_msg(&subfolder.into(), &mut msg).unwrap(); assert_eq!( res, @@ -174,4 +209,34 @@ mod tests { assert_eq!(msg, String::from("msg\n")); } + + #[test] + fn test_hooks_commit_msg_reject_in_hooks_folder_githooks_moved_absolute( + ) { + let (_td, repo) = repo_init().unwrap(); + let root = repo.workdir().unwrap(); + let mut config = repo.config().unwrap(); + + const HOOKS_DIR: &str = "my_hooks"; + config.set_str("core.hooksPath", HOOKS_DIR).unwrap(); + + let hook = b"#!/bin/sh + echo 'msg' > \"$1\" + echo 'rejected' + exit 1 + "; + let hooks_folder = root.join(HOOKS_DIR); + std::fs::create_dir_all(&hooks_folder).unwrap(); + create_hook_in_path(&hooks_folder.join("commit-msg"), hook); + + let mut msg = String::from("test"); + let res = + hooks_commit_msg(&hooks_folder.into(), &mut msg).unwrap(); + assert_eq!( + res, + HookResult::NotOk(String::from("rejected\n")) + ); + + assert_eq!(msg, String::from("msg\n")); + } } diff --git a/asyncgit/src/sync/logwalker.rs b/asyncgit/src/sync/logwalker.rs index 743376d34f..b42447642c 100644 --- a/asyncgit/src/sync/logwalker.rs +++ b/asyncgit/src/sync/logwalker.rs @@ -1,4 +1,3 @@ -#![allow(dead_code)] use super::{CommitId, SharedCommitFilterFn}; use crate::error::Result; use git2::{Commit, Oid, Repository}; diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index c52a556aad..09d4e6ef53 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -123,7 +123,7 @@ pub mod tests { }; use crate::error::Result; use git2::Repository; - use std::{path::Path, process::Command}; + use std::{ffi::OsStr, path::Path, process::Command}; use tempfile::TempDir; /// @@ -144,11 +144,19 @@ pub mod tests { /// pub fn repo_init() -> Result<(TempDir, Repository)> { + repo_init_with_prefix("gitui") + } + + /// + #[inline] + pub fn repo_init_with_prefix( + prefix: impl AsRef, + ) -> Result<(TempDir, Repository)> { init_log(); sandbox_config_files(); - let td = TempDir::new()?; + let td = TempDir::with_prefix(prefix)?; let repo = Repository::init(td.path())?; { let mut config = repo.config()?; diff --git a/asyncgit/src/sync/patches.rs b/asyncgit/src/sync/patches.rs index e87a3c9777..4107dae4b5 100644 --- a/asyncgit/src/sync/patches.rs +++ b/asyncgit/src/sync/patches.rs @@ -2,14 +2,12 @@ use super::diff::{get_diff_raw, DiffOptions, HunkHeader}; use crate::error::{Error, Result}; use git2::{Diff, DiffLine, Patch, Repository}; -#[allow(clippy::redundant_pub_crate)] -pub(crate) struct HunkLines<'a> { +pub struct HunkLines<'a> { pub hunk: HunkHeader, pub lines: Vec>, } -#[allow(clippy::redundant_pub_crate)] -pub(crate) fn get_file_diff_patch<'a>( +pub fn get_file_diff_patch<'a>( repo: &'a Repository, file: &str, is_staged: bool, diff --git a/asyncgit/src/sync/remotes/push.rs b/asyncgit/src/sync/remotes/push.rs index b28b8a22d3..0916bfdc91 100644 --- a/asyncgit/src/sync/remotes/push.rs +++ b/asyncgit/src/sync/remotes/push.rs @@ -17,6 +17,7 @@ use crate::{ use crossbeam_channel::Sender; use git2::{PackBuilderStage, PushOptions}; use scopetime::scope_time; +use std::fmt::Write as _; /// pub trait AsyncProgress: Clone + Send + Sync { @@ -182,7 +183,7 @@ pub fn push_raw( if let Ok(Some(branch_upstream_merge)) = get_branch_upstream_merge(repo_path, branch) { - push_ref.push_str(&format!(":{branch_upstream_merge}")); + let _ = write!(push_ref, ":{branch_upstream_merge}"); } } @@ -289,7 +290,7 @@ mod tests { // Attempt force push, // should work as it forces the push through - assert!(!push_branch( + assert!(push_branch( &tmp_other_repo_dir.path().to_str().unwrap().into(), "origin", "master", @@ -298,7 +299,7 @@ mod tests { None, None, ) - .is_err()); + .is_ok()); } #[test] diff --git a/asyncgit/src/sync/repository.rs b/asyncgit/src/sync/repository.rs index 2a0af47dbd..ea251c5e46 100644 --- a/asyncgit/src/sync/repository.rs +++ b/asyncgit/src/sync/repository.rs @@ -42,6 +42,12 @@ impl RepoPath { } } +impl From for RepoPath { + fn from(value: PathBuf) -> Self { + Self::Path(value) + } +} + impl From<&str> for RepoPath { fn from(p: &str) -> Self { Self::Path(PathBuf::from(p)) diff --git a/asyncgit/src/sync/staging/mod.rs b/asyncgit/src/sync/staging/mod.rs index d037499a07..06e09fe66b 100644 --- a/asyncgit/src/sync/staging/mod.rs +++ b/asyncgit/src/sync/staging/mod.rs @@ -68,9 +68,9 @@ impl NewFromOldContent { } } -// this is the heart of the per line discard,stage,unstage. heavily inspired by the great work in nodegit: https://github.com/nodegit/nodegit -#[allow(clippy::redundant_pub_crate)] -pub(crate) fn apply_selection( +// this is the heart of the per line discard,stage,unstage. heavily inspired by the great work in +// nodegit: https://github.com/nodegit/nodegit +pub fn apply_selection( lines: &[DiffLinePosition], hunks: &[HunkLines], old_lines: &[&str], diff --git a/asyncgit/src/sync/stash.rs b/asyncgit/src/sync/stash.rs index c496168116..8a65f92feb 100644 --- a/asyncgit/src/sync/stash.rs +++ b/asyncgit/src/sync/stash.rs @@ -144,7 +144,7 @@ mod tests { let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into(); - assert!(!stash_save(repo_path, None, true, false).is_ok()); + assert!(stash_save(repo_path, None, true, false).is_err()); assert!(get_stashes(repo_path).unwrap().is_empty()); } diff --git a/asyncgit/src/sync/utils.rs b/asyncgit/src/sync/utils.rs index 8e767bbfd4..ebae31beb0 100644 --- a/asyncgit/src/sync/utils.rs +++ b/asyncgit/src/sync/utils.rs @@ -242,7 +242,7 @@ mod tests { let root = repo.path().parent().unwrap(); let repo_path = root.as_os_str().to_str().unwrap(); - assert!(!stage_add_file(&repo_path.into(), file_path).is_ok()); + assert!(stage_add_file(&repo_path.into(), file_path).is_err()); } #[test] @@ -440,7 +440,7 @@ mod tests { let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into(); - assert!(!get_head(repo_path).is_ok()); + assert!(get_head(repo_path).is_err()); Ok(()) } diff --git a/filetreelist/src/error.rs b/filetreelist/src/error.rs index 068db8b4c8..7c929e37e0 100644 --- a/filetreelist/src/error.rs +++ b/filetreelist/src/error.rs @@ -1,5 +1,3 @@ -#![allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)] - use std::{num::TryFromIntError, path::PathBuf}; use thiserror::Error; diff --git a/filetreelist/src/filetree.rs b/filetreelist/src/filetree.rs index 023da8caf9..7ffd5c01b9 100644 --- a/filetreelist/src/filetree.rs +++ b/filetreelist/src/filetree.rs @@ -2,7 +2,7 @@ use crate::{ error::Result, filetreeitems::FileTreeItems, tree_iter::TreeIterator, TreeItemInfo, }; -use std::{collections::BTreeSet, path::Path}; +use std::{cell::Cell, collections::BTreeSet, path::Path}; /// #[derive(Copy, Clone, Debug)] @@ -30,6 +30,7 @@ pub struct FileTree { selection: Option, // caches the absolute selection translated to visual index visual_selection: Option, + pub window_height: Cell>, } impl FileTree { @@ -42,6 +43,7 @@ impl FileTree { items: FileTreeItems::new(list, collapsed)?, selection: if list.is_empty() { None } else { Some(0) }, visual_selection: None, + window_height: None.into(), }; new_self.visual_selection = new_self.calc_visual_selection(); @@ -112,6 +114,18 @@ impl FileTree { } } + fn selection_page_updown( + &self, + range: impl Iterator, + ) -> Option { + let page_size = self.window_height.get().unwrap_or(0); + + range + .filter(|index| self.is_visible_index(*index)) + .take(page_size) + .last() + } + /// pub fn move_selection(&mut self, dir: MoveSelection) -> bool { self.selection.is_some_and(|selection| { @@ -130,9 +144,13 @@ impl FileTree { Self::selection_start(selection) } MoveSelection::End => self.selection_end(selection), - MoveSelection::PageDown | MoveSelection::PageUp => { - None + MoveSelection::PageUp => { + self.selection_page_updown((0..=selection).rev()) } + MoveSelection::PageDown => self + .selection_page_updown( + selection..(self.items.len()), + ), }; let changed_index = @@ -514,4 +532,36 @@ mod test { assert_eq!(s.count, 3); assert_eq!(s.index, 2); } + + #[test] + fn test_selection_page_updown() { + let items = vec![ + Path::new("a/b/c"), // + Path::new("a/b/c2"), // + Path::new("a/d"), // + Path::new("a/e"), // + ]; + + //0 a/ + //1 b/ + //2 c + //3 c2 + //4 d + //5 e + + let mut tree = + FileTree::new(&items, &BTreeSet::new()).unwrap(); + + tree.window_height.set(Some(3)); + + tree.selection = Some(0); + assert!(tree.move_selection(MoveSelection::PageDown)); + assert_eq!(tree.selection, Some(2)); + assert!(tree.move_selection(MoveSelection::PageDown)); + assert_eq!(tree.selection, Some(4)); + assert!(tree.move_selection(MoveSelection::PageUp)); + assert_eq!(tree.selection, Some(2)); + assert!(tree.move_selection(MoveSelection::PageUp)); + assert_eq!(tree.selection, Some(0)); + } } diff --git a/filetreelist/src/filetreeitems.rs b/filetreelist/src/filetreeitems.rs index 8547d37032..637bfb3781 100644 --- a/filetreelist/src/filetreeitems.rs +++ b/filetreelist/src/filetreeitems.rs @@ -144,8 +144,6 @@ impl FileTreeItems { let item_path = Path::new(item.info().full_path_str()); - //TODO: fix once FP in clippy is fixed - #[allow(clippy::needless_borrow)] if item_path.starts_with(&path) { item.hide(); } else { diff --git a/filetreelist/src/item.rs b/filetreelist/src/item.rs index f664031b14..b06c275df2 100644 --- a/filetreelist/src/item.rs +++ b/filetreelist/src/item.rs @@ -55,7 +55,7 @@ impl TreeItemInfo { Path::new( self.full_path .components() - .last() + .next_back() .and_then(|c| c.as_os_str().to_str()) .unwrap_or_default(), ) diff --git a/filetreelist/src/treeitems_iter.rs b/filetreelist/src/treeitems_iter.rs index 4feeb3e8ca..fddf35a1e9 100644 --- a/filetreelist/src/treeitems_iter.rs +++ b/filetreelist/src/treeitems_iter.rs @@ -35,7 +35,7 @@ impl<'a> Iterator for TreeItemsIterator<'a> { *i += 1; } else { self.increments = Some(0); - }; + } loop { if !init { diff --git a/git2-hooks/Cargo.toml b/git2-hooks/Cargo.toml index 3fab403cd2..6bdd0497a9 100644 --- a/git2-hooks/Cargo.toml +++ b/git2-hooks/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["git"] [dependencies] git2 = ">=0.17" +gix-path = "0.10" log = "0.4" shellexpand = "3.1" thiserror = "2.0" diff --git a/git2-hooks/src/hookspath.rs b/git2-hooks/src/hookspath.rs index 3648676ee2..fc4db5c87a 100644 --- a/git2-hooks/src/hookspath.rs +++ b/git2-hooks/src/hookspath.rs @@ -3,7 +3,7 @@ use git2::Repository; use crate::{error::Result, HookResult, HooksError}; use std::{ - env, + ffi::{OsStr, OsString}, path::{Path, PathBuf}, process::Command, str::FromStr, @@ -17,6 +17,7 @@ pub struct HookPaths { const CONFIG_HOOKS_PATH: &str = "core.hooksPath"; const DEFAULT_HOOKS_PATH: &str = "hooks"; +const ENOEXEC: i32 = 8; impl HookPaths { /// `core.hooksPath` always takes precedence. @@ -41,16 +42,8 @@ impl HookPaths { if let Some(config_path) = Self::config_hook_path(repo)? { let hooks_path = PathBuf::from(config_path); - let hook = hooks_path.join(hook); - - let hook = shellexpand::full( - hook.as_os_str() - .to_str() - .ok_or(HooksError::PathToString)?, - )?; - - let hook = PathBuf::from_str(hook.as_ref()) - .map_err(|_| HooksError::PathToString)?; + let hook = + Self::expand_path(&hooks_path.join(hook), &pwd)?; return Ok(Self { git: git_dir, @@ -66,6 +59,41 @@ impl HookPaths { }) } + /// Expand path according to the rule of githooks and config + /// core.hooksPath + fn expand_path(path: &Path, pwd: &Path) -> Result { + let hook_expanded = shellexpand::full( + path.as_os_str() + .to_str() + .ok_or(HooksError::PathToString)?, + )?; + let hook_expanded = PathBuf::from_str(hook_expanded.as_ref()) + .map_err(|_| HooksError::PathToString)?; + + // `man git-config`: + // + // > A relative path is taken as relative to the + // > directory where the hooks are run (see the + // > "DESCRIPTION" section of githooks[5]). + // + // `man githooks`: + // + // > Before Git invokes a hook, it changes its + // > working directory to either $GIT_DIR in a bare + // > repository or the root of the working tree in a + // > non-bare repository. + // + // I.e. relative paths in core.hooksPath in non-bare + // repositories are always relative to GIT_WORK_TREE. + Ok({ + if hook_expanded.is_absolute() { + hook_expanded + } else { + pwd.join(hook_expanded) + } + }) + } + fn config_hook_path(repo: &Repository) -> Result> { Ok(repo.config()?.get_string(CONFIG_HOOKS_PATH).ok()) } @@ -107,30 +135,76 @@ impl HookPaths { /// this function calls hook scripts based on conventions documented here /// see pub fn run_hook(&self, args: &[&str]) -> Result { - let hook = self.hook.clone(); - - let arg_str = format!("{:?} {}", hook, args.join(" ")); - // Use -l to avoid "command not found" on Windows. - let bash_args = - vec!["-l".to_string(), "-c".to_string(), arg_str]; + self.run_hook_os_str(args) + } + /// this function calls hook scripts based on conventions documented here + /// see + pub fn run_hook_os_str(&self, args: I) -> Result + where + I: IntoIterator + Copy, + S: AsRef, + { + let hook = self.hook.clone(); log::trace!("run hook '{:?}' in '{:?}'", hook, self.pwd); - let git_shell = find_bash_executable() - .or_else(find_default_unix_shell) - .unwrap_or_else(|| "bash".into()); - let output = Command::new(git_shell) - .args(bash_args) - .with_no_window() - .current_dir(&self.pwd) - // This call forces Command to handle the Path environment correctly on windows, - // the specific env set here does not matter - // see https://github.com/rust-lang/rust/issues/37519 - .env( - "DUMMY_ENV_TO_FIX_WINDOWS_CMD_RUNS", - "FixPathHandlingOnWindows", + let run_command = |command: &mut Command| { + command + .args(args) + .current_dir(&self.pwd) + .with_no_window() + .output() + }; + + let output = if cfg!(windows) { + // execute hook in shell + let command = { + // SEE: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_02 + // Enclosing characters in single-quotes ( '' ) shall preserve the literal value of each character within the single-quotes. + // A single-quote cannot occur within single-quotes. + const REPLACEMENT: &str = concat!( + "'", // closing single-quote + "\\'", // one escaped single-quote (outside of single-quotes) + "'", // new single-quote + ); + + let mut os_str = OsString::new(); + os_str.push("'"); + if let Some(hook) = hook.to_str() { + os_str.push(hook.replace('\'', REPLACEMENT)); + } else { + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + if hook + .as_os_str() + .encode_wide() + .any(|x| x == u16::from(b'\'')) + { + // TODO: escape single quotes instead of failing + return Err(HooksError::PathToString); + } + } + + os_str.push(hook.as_os_str()); + } + os_str.push("'"); + os_str.push(" \"$@\""); + + os_str + }; + run_command( + sh_command().arg("-c").arg(command).arg(&hook), ) - .output()?; + } else { + // execute hook directly + match run_command(&mut Command::new(&hook)) { + Err(err) if err.raw_os_error() == Some(ENOEXEC) => { + run_command(sh_command().arg(&hook)) + } + result => result, + } + }?; if output.status.success() { Ok(HookResult::Ok { hook }) @@ -150,6 +224,25 @@ impl HookPaths { } } +fn sh_command() -> Command { + let mut command = Command::new(gix_path::env::shell()); + + if cfg!(windows) { + // This call forces Command to handle the Path environment correctly on windows, + // the specific env set here does not matter + // see https://github.com/rust-lang/rust/issues/37519 + command.env( + "DUMMY_ENV_TO_FIX_WINDOWS_CMD_RUNS", + "FixPathHandlingOnWindows", + ); + + // Use -l to avoid "command not found" + command.arg("-l"); + } + + command +} + #[cfg(unix)] fn is_executable(path: &Path) -> bool { use std::os::unix::fs::PermissionsExt; @@ -168,40 +261,12 @@ fn is_executable(path: &Path) -> bool { } #[cfg(windows)] -/// windows does not consider bash scripts to be executable so we consider everything +/// windows does not consider shell scripts to be executable so we consider everything /// to be executable (which is not far from the truth for windows platform.) const fn is_executable(_: &Path) -> bool { true } -// Find bash.exe, and avoid finding wsl's bash.exe on Windows. -// None for non-Windows. -fn find_bash_executable() -> Option { - if cfg!(windows) { - Command::new("where.exe") - .arg("git") - .output() - .ok() - .map(|out| { - PathBuf::from(Into::::into( - String::from_utf8_lossy(&out.stdout), - )) - }) - .as_deref() - .and_then(Path::parent) - .and_then(Path::parent) - .map(|p| p.join("usr/bin/bash.exe")) - .filter(|p| p.exists()) - } else { - None - } -} - -// Find default shell on Unix-like OS. -fn find_default_unix_shell() -> Option { - env::var_os("SHELL").map(PathBuf::from) -} - trait CommandExt { /// The process is a console application that is being run without a /// console window. Therefore, the console handle for the application is @@ -232,3 +297,35 @@ impl CommandExt for Command { self } } + +#[cfg(test)] +mod test { + use super::HookPaths; + use std::path::Path; + + #[test] + fn test_hookspath_relative() { + assert_eq!( + HookPaths::expand_path( + Path::new("pre-commit"), + Path::new("example_git_root"), + ) + .unwrap(), + Path::new("example_git_root").join("pre-commit") + ); + } + + #[test] + fn test_hookspath_absolute() { + let absolute_hook = + std::env::current_dir().unwrap().join("pre-commit"); + assert_eq!( + HookPaths::expand_path( + &absolute_hook, + Path::new("example_git_root"), + ) + .unwrap(), + absolute_hook + ); + } +} diff --git a/git2-hooks/src/lib.rs b/git2-hooks/src/lib.rs index 2a458856d7..4c949a1e46 100644 --- a/git2-hooks/src/lib.rs +++ b/git2-hooks/src/lib.rs @@ -132,10 +132,7 @@ pub fn hooks_commit_msg( let temp_file = hook.git.join(HOOK_COMMIT_MSG_TEMP_FILE); File::create(&temp_file)?.write_all(msg.as_bytes())?; - let res = hook.run_hook(&[temp_file - .as_os_str() - .to_string_lossy() - .as_ref()])?; + let res = hook.run_hook_os_str([&temp_file])?; // load possibly altered msg msg.clear(); @@ -282,7 +279,7 @@ exit 0 let hook = br#"#!/bin/sh COMMIT_MSG="$(cat "$1")" -printf "$COMMIT_MSG" | sed 's/sth/shell_command/g' >"$1" +printf "$COMMIT_MSG" | sed 's/sth/shell_command/g' > "$1" exit 0 "#; @@ -309,6 +306,41 @@ exit 0 assert!(res.is_ok()); } + #[test] + fn test_hook_with_missing_shebang() { + const TEXT: &str = "Hello, world!"; + + let (_td, repo) = repo_init(); + + let hook = b"echo \"$@\"\nexit 42"; + + create_hook(&repo, HOOK_PRE_COMMIT, hook); + + let hook = + HookPaths::new(&repo, None, HOOK_PRE_COMMIT).unwrap(); + + assert!(hook.found()); + + let result = hook.run_hook(&[TEXT]).unwrap(); + + let HookResult::RunNotSuccessful { + code, + stdout, + stderr, + hook: h, + } = result + else { + unreachable!("run_hook should've failed"); + }; + + let stdout = stdout.as_str().trim_ascii_end(); + + assert_eq!(code, Some(42)); + assert_eq!(h, hook.hook); + assert_eq!(stdout, TEXT, "{:?} != {TEXT:?}", stdout); + assert!(stderr.is_empty()); + } + #[test] fn test_no_hook_found() { let (_td, repo) = repo_init(); @@ -388,6 +420,8 @@ exit 1 #[test] fn test_env_containing_path() { + const PATH_EXPORT: &str = "export PATH"; + let (_td, repo) = repo_init(); let hook = b"#!/bin/sh @@ -402,9 +436,12 @@ exit 1 unreachable!() }; - assert!(stdout - .lines() - .any(|line| line.starts_with("export PATH"))); + assert!( + stdout + .lines() + .any(|line| line.starts_with(PATH_EXPORT)), + "Could not find line starting with {PATH_EXPORT:?} in: {stdout:?}" + ); } #[test] @@ -470,7 +507,7 @@ sys.exit(0) create_hook(&repo, HOOK_PRE_COMMIT, hook); let res = hooks_pre_commit(&repo, None).unwrap(); - assert!(res.is_ok()); + assert!(res.is_ok(), "{res:?}"); } #[test] @@ -499,9 +536,9 @@ sys.exit(1) let (_td, repo) = repo_init(); let hook = b"#!/bin/sh -echo 'msg' > $1 -echo 'rejected' -exit 1 + echo 'msg' > \"$1\" + echo 'rejected' + exit 1 "; create_hook(&repo, HOOK_COMMIT_MSG, hook); @@ -525,7 +562,7 @@ exit 1 let (_td, repo) = repo_init(); let hook = b"#!/bin/sh -echo 'msg' > $1 +echo 'msg' > \"$1\" exit 0 "; @@ -565,7 +602,7 @@ exit 0 let (_td, repo) = repo_init(); let hook = b"#!/bin/sh -echo msg:$2 > $1 +echo \"msg:$2\" > \"$1\" exit 0 "; @@ -589,7 +626,7 @@ exit 0 let (_td, repo) = repo_init(); let hook = b"#!/bin/sh -echo $2,$3 > $1 +echo \"$2,$3\" > \"$1\" echo 'rejected' exit 2 "; diff --git a/src/app.rs b/src/app.rs index 45037f048f..68d2987c6f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -275,7 +275,7 @@ impl App { 3 => self.stashing_tab.draw(f, chunks_main[1])?, 4 => self.stashlist_tab.draw(f, chunks_main[1])?, _ => bail!("unknown tab"), - }; + } } self.draw_popups(f)?; @@ -905,7 +905,7 @@ impl App { InternalEvent::CommitSearch(options) => { self.revlog.search(options); } - }; + } Ok(flags) } @@ -997,7 +997,7 @@ impl App { undo_last_commit(&self.repo.borrow()) ); } - }; + } flags.insert(NeedsUpdate::ALL); @@ -1019,7 +1019,7 @@ impl App { )); self.tags_popup.update_tags()?; - }; + } Ok(()) } diff --git a/src/args.rs b/src/args.rs index 8ef3aeb42f..c39afad331 100644 --- a/src/args.rs +++ b/src/args.rs @@ -38,7 +38,6 @@ pub fn process_cmdline() -> Result { .get_one::("directory") .map_or_else(|| PathBuf::from("."), PathBuf::from); - #[allow(clippy::option_if_let_else)] let repo_path = if let Some(w) = workdir { RepoPath::Workdir { gitdir, workdir: w } } else { @@ -50,7 +49,6 @@ pub fn process_cmdline() -> Result { .map_or_else(|| PathBuf::from("theme.ron"), PathBuf::from); let confpath = get_app_config_path()?; - fs::create_dir_all(&confpath)?; let theme = confpath.join(arg_theme); let notify_watcher: bool = @@ -139,7 +137,7 @@ fn setup_logging(path_override: Option) -> Result<()> { path }; - println!("Logging enabled. Log written to: {path:?}"); + println!("Logging enabled. Log written to: {}", path.display()); WriteLogger::init( LevelFilter::Trace, @@ -150,28 +148,104 @@ fn setup_logging(path_override: Option) -> Result<()> { Ok(()) } +fn get_path_from_candidates( + candidates: impl IntoIterator>, +) -> Result { + let mut target_dir = None; + + // Filter into existing directories + for potential_dir in + candidates.into_iter().flatten().filter(|p| p.is_dir()) + { + let search_path = potential_dir.join("gitui"); + + // Prefer preexisting gitui directory + if search_path.is_dir() { + target_dir = Some(search_path); + break; + } + + // Fallback to first existing directory + target_dir.get_or_insert(search_path); + } + + target_dir.ok_or_else(|| { + anyhow!("failed to find valid path within candidates") + }) +} + fn get_app_cache_path() -> Result { - let mut path = dirs::cache_dir() - .ok_or_else(|| anyhow!("failed to find os cache dir."))?; + let cache_dir_candidates = [ + env::var_os("XDG_CACHE_HOME").map(PathBuf::from), + dirs::cache_dir(), + ]; + + let cache_dir = get_path_from_candidates(cache_dir_candidates) + .map_err(|_| anyhow!("failed to find os cache dir."))?; - path.push("gitui"); - fs::create_dir_all(&path)?; - Ok(path) + fs::create_dir_all(&cache_dir)?; + Ok(cache_dir) } pub fn get_app_config_path() -> Result { - let mut path = if cfg!(target_os = "macos") { - dirs::home_dir().map(|h| h.join(".config")) - } else { - dirs::config_dir() + // List of potential config directories in order of priority + let config_dir_candidates = [ + env::var_os("XDG_CONFIG_HOME").map(PathBuf::from), + // This is in the list since it was the hardcoded behavior on macos before + // I expect this to be what most people have XDG_CONFIG_HOME set to already + // But explicitly including this will avoid breaking anyone's existing config + dirs::home_dir().map(|p| p.join(".config")), + dirs::config_dir(), + ]; + + get_path_from_candidates(config_dir_candidates) + .map_err(|_| anyhow!("failed to find os config dir.")) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::{app, get_path_from_candidates}; + use tempfile::tempdir; + + #[test] + fn verify_app() { + app().debug_assert(); } - .ok_or_else(|| anyhow!("failed to find os config dir."))?; - path.push("gitui"); - Ok(path) -} + #[test] + fn test_config_dir_candidates_from_preexisting() { + let temp_dummy_1 = tempdir().expect("should create temp dir"); + let temp_dummy_2 = tempdir().expect("should create temp dir"); + let temp_target = tempdir().expect("should create temp dir"); + let temp_goal = temp_target.path().join("gitui"); + + fs::create_dir_all(&temp_goal) + .expect("should create temp target directory"); + + let candidates = [ + Some(temp_dummy_1.path().to_path_buf()), + Some(temp_target.path().to_path_buf()), + Some(temp_dummy_2.path().to_path_buf()), + ]; + let result = get_path_from_candidates(candidates) + .expect("should find the included target"); + assert_eq!(result, temp_goal); + } + + #[test] + fn test_config_dir_candidates_no_preexisting() { + let temp_dummy_1 = tempdir().expect("should create temp dir"); + let temp_dummy_2 = tempdir().expect("should create temp dir"); -#[test] -fn verify_app() { - app().debug_assert(); + let candidates = [ + Some(temp_dummy_1.path().to_path_buf()), + Some(temp_dummy_2.path().to_path_buf()), + ]; + + let result = get_path_from_candidates(candidates) + .expect("should return first candidate"); + assert_eq!(result, temp_dummy_1.path().join("gitui")); + } } diff --git a/src/components/changes.rs b/src/components/changes.rs index 7b0bc4270b..48883d20e8 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -92,7 +92,7 @@ impl ChangesComponent { &self.repo.borrow(), path, )?, - }; + } } else { let config = self.options.borrow().status_show_untracked(); diff --git a/src/components/commit_details/compare_details.rs b/src/components/commit_details/compare_details.rs index e10974f337..3438447c61 100644 --- a/src/components/commit_details/compare_details.rs +++ b/src/components/commit_details/compare_details.rs @@ -58,7 +58,6 @@ impl CompareDetailsComponent { }); } - #[allow(unstable_name_collisions)] fn get_commit_text(&self, data: &CommitDetails) -> Vec { let mut res = vec![ Line::from(vec![ diff --git a/src/components/commit_details/details.rs b/src/components/commit_details/details.rs index 0609982548..95aa018ac5 100644 --- a/src/components/commit_details/details.rs +++ b/src/components/commit_details/details.rs @@ -155,7 +155,7 @@ impl DetailsComponent { .collect() } - #[allow(unstable_name_collisions, clippy::too_many_lines)] + #[allow(clippy::too_many_lines)] fn get_text_info(&self) -> Vec { self.data.as_ref().map_or_else(Vec::new, |data| { let mut res = vec![ diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 52e4e7be9d..dde38ba06e 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -115,6 +115,10 @@ impl CommitList { } /// + #[expect( + clippy::missing_const_for_fn, + reason = "as of 1.86.0 clippy wants this to be const even though that breaks" + )] pub fn marked(&self) -> &[(usize, CommitId)] { &self.marked } @@ -132,10 +136,9 @@ impl CommitList { commits } - /// - pub fn copy_commit_hash(&self) -> Result<()> { - let marked = self.marked.as_slice(); - let yank: Option = match marked { + /// Build string of marked or selected (if none are marked) commit ids + fn concat_selected_commit_ids(&self) -> Option { + match self.marked.as_slice() { [] => self .items .iter() @@ -144,24 +147,19 @@ impl CommitList { .saturating_sub(self.items.index_offset()), ) .map(|e| e.id.to_string()), - [(_idx, commit)] => Some(commit.to_string()), - [first, .., last] => { - let marked_consecutive = - marked.windows(2).all(|w| w[0].0 + 1 == w[1].0); - - let yank = if marked_consecutive { - format!("{}^..{}", first.1, last.1) - } else { - marked - .iter() - .map(|(_idx, commit)| commit.to_string()) - .join(" ") - }; - Some(yank) - } - }; + marked => Some( + marked + .iter() + .map(|(_idx, commit)| commit.to_string()) + .join(" "), + ), + } + } - if let Some(yank) = yank { + /// Copy currently marked or selected (if none are marked) commit ids + /// to clipboard + pub fn copy_commit_hash(&self) -> Result<()> { + if let Some(yank) = self.concat_selected_commit_ids() { crate::clipboard::copy_string(&yank)?; self.queue.push(InternalEvent::ShowInfoMsg( strings::copy_success(&yank), @@ -490,24 +488,36 @@ impl CommitList { txt.push(splitter.clone()); } - let style_hash = normal - .then(|| theme.commit_hash(selected)) - .unwrap_or_else(|| theme.commit_unhighlighted()); - let style_time = normal - .then(|| theme.commit_time(selected)) - .unwrap_or_else(|| theme.commit_unhighlighted()); - let style_author = normal - .then(|| theme.commit_author(selected)) - .unwrap_or_else(|| theme.commit_unhighlighted()); - let style_tags = normal - .then(|| theme.tags(selected)) - .unwrap_or_else(|| theme.commit_unhighlighted()); - let style_branches = normal - .then(|| theme.branch(selected, true)) - .unwrap_or_else(|| theme.commit_unhighlighted()); - let style_msg = normal - .then(|| theme.text(true, selected)) - .unwrap_or_else(|| theme.commit_unhighlighted()); + let style_hash = if normal { + theme.commit_hash(selected) + } else { + theme.commit_unhighlighted() + }; + let style_time = if normal { + theme.commit_time(selected) + } else { + theme.commit_unhighlighted() + }; + let style_author = if normal { + theme.commit_author(selected) + } else { + theme.commit_unhighlighted() + }; + let style_tags = if normal { + theme.tags(selected) + } else { + theme.commit_unhighlighted() + }; + let style_branches = if normal { + theme.branch(selected, true) + } else { + theme.commit_unhighlighted() + }; + let style_msg = if normal { + theme.text(true, selected) + } else { + theme.commit_unhighlighted() + }; // commit hash txt.push(Span::styled(Cow::from(&*e.hash_short), style_hash)); @@ -893,8 +903,36 @@ impl Component for CommitList { #[cfg(test)] mod tests { + use asyncgit::sync::CommitInfo; + use super::*; + impl Default for CommitList { + fn default() -> Self { + Self { + title: String::new().into_boxed_str(), + selection: 0, + highlighted_selection: Option::None, + highlights: Option::None, + tags: Option::None, + items: ItemBatch::default(), + commits: IndexSet::default(), + marked: Vec::default(), + scroll_top: Cell::default(), + local_branches: BTreeMap::default(), + remote_branches: BTreeMap::default(), + theme: SharedTheme::default(), + key_config: SharedKeyConfig::default(), + scroll_state: (Instant::now(), 0.0), + current_size: Cell::default(), + repo: RepoPathRef::new(sync::RepoPath::Path( + std::path::PathBuf::default(), + )), + queue: Queue::default(), + } + } + } + #[test] fn test_string_width_align() { assert_eq!(string_width_align("123", 3), "123"); @@ -916,4 +954,126 @@ mod tests { "Jon Grythe Stødle " ); } + + /// Build a commit list with a few commits loaded + fn build_commit_list_with_some_commits() -> CommitList { + let mut items = ItemBatch::default(); + let basic_commit_info = CommitInfo { + message: String::default(), + time: 0, + author: String::default(), + id: CommitId::default(), + }; + // This just creates a sequence of fake ordered ids + // 0000000000000000000000000000000000000000 + // 0000000000000000000000000000000000000001 + // 0000000000000000000000000000000000000002 + // ... + items.set_items( + 2, /* randomly choose an offset */ + (0..20) + .map(|idx| CommitInfo { + id: CommitId::from_str_unchecked(&format!( + "{idx:040}", + )) + .unwrap(), + ..basic_commit_info.clone() + }) + .collect(), + None, + ); + CommitList { + items, + selection: 4, // Randomly select one commit + ..Default::default() + } + } + + /// Build a value for cl.marked based on indices into cl.items + fn build_marked_from_indices( + cl: &CommitList, + marked_indices: &[usize], + ) -> Vec<(usize, CommitId)> { + let offset = cl.items.index_offset(); + marked_indices + .iter() + .map(|idx| { + (*idx, cl.items.iter().nth(*idx - offset).unwrap().id) + }) + .collect() + } + + #[test] + fn test_copy_commit_list_empty() { + assert_eq!( + CommitList::default().concat_selected_commit_ids(), + None + ); + } + + #[test] + fn test_copy_commit_none_marked() { + let cl = CommitList { + selection: 4, + ..build_commit_list_with_some_commits() + }; + // ids from build_commit_list_with_some_commits() are + // offset by two, so we expect commit id 2 for + // selection = 4 + assert_eq!( + cl.concat_selected_commit_ids(), + Some(String::from( + "0000000000000000000000000000000000000002" + )) + ); + } + + #[test] + fn test_copy_commit_one_marked() { + let cl = build_commit_list_with_some_commits(); + let cl = CommitList { + marked: build_marked_from_indices(&cl, &[3]), + ..cl + }; + assert_eq!( + cl.concat_selected_commit_ids(), + Some(String::from( + "0000000000000000000000000000000000000001", + )) + ); + } + + #[test] + fn test_copy_commit_range_marked() { + let cl = build_commit_list_with_some_commits(); + let cl = CommitList { + marked: build_marked_from_indices(&cl, &[4, 5, 6, 7]), + ..cl + }; + assert_eq!( + cl.concat_selected_commit_ids(), + Some(String::from(concat!( + "0000000000000000000000000000000000000002 ", + "0000000000000000000000000000000000000003 ", + "0000000000000000000000000000000000000004 ", + "0000000000000000000000000000000000000005" + ))) + ); + } + + #[test] + fn test_copy_commit_random_marked() { + let cl = build_commit_list_with_some_commits(); + let cl = CommitList { + marked: build_marked_from_indices(&cl, &[4, 7]), + ..cl + }; + assert_eq!( + cl.concat_selected_commit_ids(), + Some(String::from(concat!( + "0000000000000000000000000000000000000002 ", + "0000000000000000000000000000000000000005" + ))) + ); + } } diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index 8a56071684..f3fec043d0 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -275,6 +275,8 @@ impl RevisionFilesComponent { let tree_height = usize::from(area.height.saturating_sub(2)); let tree_width = usize::from(area.width); + self.tree.window_height.set(Some(tree_height)); + self.tree.visual_selection().map_or_else( || { self.scroll.reset(); diff --git a/src/components/status_tree.rs b/src/components/status_tree.rs index 4fb762c1af..1d5f39cb6f 100644 --- a/src/components/status_tree.rs +++ b/src/components/status_tree.rs @@ -351,6 +351,7 @@ impl DrawableComponent for StatusTreeComponent { .map(|idx| idx.saturating_sub(selection_offset)) .unwrap_or_default(); let tree_height = r.height.saturating_sub(2) as usize; + self.tree.window_height.set(Some(tree_height)); self.scroll_top.set(ui::calc_scroll_top( self.scroll_top.get(), @@ -504,6 +505,15 @@ impl Component for StatusTreeComponent { || key_match(e, self.key_config.keys.shift_down) { Ok(self.move_selection(MoveSelection::End).into()) + } else if key_match(e, self.key_config.keys.page_up) { + Ok(self + .move_selection(MoveSelection::PageUp) + .into()) + } else if key_match(e, self.key_config.keys.page_down) + { + Ok(self + .move_selection(MoveSelection::PageDown) + .into()) } else if key_match(e, self.key_config.keys.move_left) { Ok(self diff --git a/src/components/textinput.rs b/src/components/textinput.rs index e6586b8355..e67d19eac9 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -176,7 +176,7 @@ impl TextInputComponent { ) .title(self.title.clone()), ); - }; + } text_area }); } diff --git a/src/components/utils/emoji.rs b/src/components/utils/emoji.rs index 75fe84dc5d..3980cd30b3 100644 --- a/src/components/utils/emoji.rs +++ b/src/components/utils/emoji.rs @@ -7,9 +7,10 @@ static EMOJI_REPLACER: Lazy = // Replace markdown emojis with Unicode equivalent // :hammer: --> 🔨 #[inline] -pub fn emojifi_string(s: &mut String) { - let resulting_cow = EMOJI_REPLACER.replace_all(s); - if let Cow::Owned(altered_s) = resulting_cow { - *s = altered_s; +pub fn emojifi_string(s: String) -> String { + if let Cow::Owned(altered_s) = EMOJI_REPLACER.replace_all(&s) { + altered_s + } else { + s } } diff --git a/src/components/utils/filetree.rs b/src/components/utils/filetree.rs index 8b298dbb93..a0b507fa82 100644 --- a/src/components/utils/filetree.rs +++ b/src/components/utils/filetree.rs @@ -90,7 +90,7 @@ impl FileTreeItem { match path .components() - .last() + .next_back() .map(std::path::Component::as_os_str) .map(OsStr::to_string_lossy) .map(String::from) diff --git a/src/components/utils/logitems.rs b/src/components/utils/logitems.rs index 4c980b65fa..9e0706226e 100644 --- a/src/components/utils/logitems.rs +++ b/src/components/utils/logitems.rs @@ -41,12 +41,11 @@ impl From for LogEntry { }; let author = c.author; - #[allow(unused_mut)] - let mut msg = c.message; + let msg = c.message; // Replace markdown emojis with Unicode equivalent #[cfg(feature = "ghemoji")] - emojifi_string(&mut msg); + let msg = emojifi_string(msg); Self { author: author.into(), @@ -175,9 +174,7 @@ mod tests { use super::*; fn test_conversion(s: &str) -> String { - let mut s = s.to_string(); - emojifi_string(&mut s); - s + emojifi_string(s.into()) } #[test] diff --git a/src/components/utils/statustree.rs b/src/components/utils/statustree.rs index 2118ab0ed8..32e6c22025 100644 --- a/src/components/utils/statustree.rs +++ b/src/components/utils/statustree.rs @@ -3,7 +3,7 @@ use super::filetree::{ }; use anyhow::Result; use asyncgit::StatusItem; -use std::{cmp, collections::BTreeSet}; +use std::{cell::Cell, cmp, collections::BTreeSet}; //TODO: use new `filetreelist` crate @@ -16,6 +16,8 @@ pub struct StatusTree { // some folders may be folded up, this allows jumping // over folders which are folded into their parent pub available_selections: Vec, + + pub window_height: Cell>, } /// @@ -27,6 +29,8 @@ pub enum MoveSelection { Right, Home, End, + PageDown, + PageUp, } #[derive(Copy, Clone, Debug)] @@ -143,6 +147,15 @@ impl StatusTree { } MoveSelection::Home => SelectionChange::new(0, false), MoveSelection::End => self.selection_end(), + MoveSelection::PageUp => self.selection_page_updown( + selection, + (0..=selection).rev(), + ), + MoveSelection::PageDown => self + .selection_page_updown( + selection, + selection..(self.tree.len()), + ), }; let changed_index = @@ -283,6 +296,25 @@ impl StatusTree { SelectionChange::new(new_index, false) } + fn selection_page_updown( + &self, + current_index: usize, + range: impl Iterator, + ) -> SelectionChange { + let page_size = self.window_height.get().unwrap_or(0); + + let new_index = range + .filter(|index| { + self.available_selections.contains(index) + && self.is_visible_index(*index) + }) + .take(page_size) + .last() + .unwrap_or(current_index); + + SelectionChange::new(new_index, false) + } + fn is_visible_index(&self, idx: usize) -> bool { self.tree[idx].info.visible } diff --git a/src/keys/key_config.rs b/src/keys/key_config.rs index 9cd4eb73f2..fe6a987765 100644 --- a/src/keys/key_config.rs +++ b/src/keys/key_config.rs @@ -40,6 +40,10 @@ impl KeyConfig { Ok(Self { keys, symbols }) } + #[expect( + clippy::missing_const_for_fn, + reason = "as of 1.86.0 clippy wants this to be const even though that breaks" + )] fn get_key_symbol(&self, k: KeyCode) -> &str { match k { KeyCode::Enter => &self.symbols.enter, @@ -106,6 +110,10 @@ impl KeyConfig { } } + #[expect( + clippy::missing_const_for_fn, + reason = "as of 1.86.0 clippy wants this to be const even though that breaks" + )] fn get_modifier_hint(&self, modifier: KeyModifiers) -> &str { match modifier { KeyModifiers::CONTROL => &self.symbols.control, diff --git a/src/main.rs b/src/main.rs index feea894491..2e8e9b8575 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,7 +73,6 @@ use std::{ io::{self, Stdout}, panic, path::Path, - process, time::{Duration, Instant}, }; use ui::style::Theme; @@ -120,20 +119,24 @@ enum Updater { NotifyWatcher, } +/// Do `log::error!` and `eprintln!` in one line. +macro_rules! log_eprintln { + ( $($arg:tt)* ) => {{ + log::error!($($arg)*); + eprintln!($($arg)*); + }}; +} + fn main() -> Result<()> { let app_start = Instant::now(); let cliargs = process_cmdline()?; asyncgit::register_tracing_logging(); - - if !valid_path(&cliargs.repo_path) { - eprintln!("invalid path\nplease run gitui inside of a non-bare git repository"); - return Ok(()); - } + ensure_valid_path(&cliargs.repo_path)?; let key_config = KeyConfig::init() - .map_err(|e| eprintln!("KeyConfig loading error: {e}")) + .map_err(|e| log_eprintln!("KeyConfig loading error: {e}")) .unwrap_or_default(); let theme = Theme::init(&cliargs.theme); @@ -142,7 +145,7 @@ fn main() -> Result<()> { shutdown_terminal(); } - set_panic_handlers()?; + set_panic_handler()?; let mut repo_path = cliargs.repo_path; let mut terminal = start_terminal(io::stdout(), &repo_path)?; @@ -292,13 +295,13 @@ fn shutdown_terminal() { io::stdout().execute(LeaveAlternateScreen).map(|_f| ()); if let Err(e) = leave_screen { - eprintln!("leave_screen failed:\n{e}"); + log::error!("leave_screen failed:\n{e}"); } let leave_raw_mode = disable_raw_mode(); if let Err(e) = leave_raw_mode { - eprintln!("leave_raw_mode failed:\n{e}"); + log::error!("leave_raw_mode failed:\n{e}"); } } @@ -316,12 +319,14 @@ fn draw(terminal: &mut Terminal, app: &App) -> io::Result<()> { Ok(()) } -fn valid_path(repo_path: &RepoPath) -> bool { - let error = asyncgit::sync::repo_open_error(repo_path); - if let Some(error) = &error { - log::error!("repo open error: {error}"); +fn ensure_valid_path(repo_path: &RepoPath) -> Result<()> { + match asyncgit::sync::repo_open_error(repo_path) { + Some(e) => { + log::error!("{e}"); + bail!(e) + } + None => Ok(()), } - error.is_none() } fn select_event( @@ -389,30 +394,15 @@ fn start_terminal( Ok(terminal) } -// do log::error! and eprintln! in one line, pass string, error and backtrace -macro_rules! log_eprintln { - ($string:expr, $e:expr, $bt:expr) => { - log::error!($string, $e, $bt); - eprintln!($string, $e, $bt); - }; -} - -fn set_panic_handlers() -> Result<()> { - // regular panic handler +fn set_panic_handler() -> Result<()> { panic::set_hook(Box::new(|e| { let backtrace = Backtrace::new(); shutdown_terminal(); - log_eprintln!("\nGitUI was close due to an unexpected panic.\nPlease file an issue on https://github.com/gitui-org/gitui/issues with the following info:\n\n{:?}\ntrace:\n{:?}", e, backtrace); + log_eprintln!("\nGitUI was closed due to an unexpected panic.\nPlease file an issue on https://github.com/gitui-org/gitui/issues with the following info:\n\n{e}\n\ntrace:\n{backtrace:?}"); })); // global threadpool rayon_core::ThreadPoolBuilder::new() - .panic_handler(|e| { - let backtrace = Backtrace::new(); - shutdown_terminal(); - log_eprintln!("\nGitUI was close due to an unexpected panic.\nPlease file an issue on https://github.com/gitui-org/gitui/issues with the following info:\n\n{:?}\ntrace:\n{:?}", e, backtrace); - process::abort(); - }) .num_threads(4) .build_global()?; diff --git a/src/options.rs b/src/options.rs index db04802092..0fafe64151 100644 --- a/src/options.rs +++ b/src/options.rs @@ -152,8 +152,6 @@ impl Options { Ok(from_bytes(&buffer)?) } - //TODO: fix once FP in clippy is fixed - #[allow(clippy::needless_borrow)] fn save_failable(&self) -> Result<()> { let dir = Self::options_file(&self.repo)?; diff --git a/src/popups/blame_file.rs b/src/popups/blame_file.rs index 273ca5ace3..9672965bcb 100644 --- a/src/popups/blame_file.rs +++ b/src/popups/blame_file.rs @@ -41,6 +41,10 @@ struct SyntaxFileBlame { } impl SyntaxFileBlame { + #[expect( + clippy::missing_const_for_fn, + reason = "as of 1.86.0 clippy wants this to be const even though that breaks" + )] fn path(&self) -> &str { &self.file_blame.path } diff --git a/src/popups/commit.rs b/src/popups/commit.rs index 4dcfc3241c..008fc6f8a7 100644 --- a/src/popups/commit.rs +++ b/src/popups/commit.rs @@ -27,7 +27,9 @@ use ratatui::{ widgets::Paragraph, Frame, }; + use std::{ + fmt::Write as _, fs::{read_to_string, File}, io::{Read, Write}, path::PathBuf, @@ -470,9 +472,7 @@ impl CommitPopup { let mut msg = msg.to_owned(); if let (Some(user), Some(mail)) = (user, mail) { - msg.push_str(&format!( - "\n\nSigned-off-by: {user} <{mail}>" - )); + let _ = write!(msg, "\n\nSigned-off-by: {user} <{mail}>"); } Ok(msg) diff --git a/src/popups/create_remote.rs b/src/popups/create_remote.rs index ac64934da5..8e464931e8 100644 --- a/src/popups/create_remote.rs +++ b/src/popups/create_remote.rs @@ -209,6 +209,6 @@ impl CreateRemotePopup { self.hide(); } - }; + } } } diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index c946323b5d..6bbbffbe8a 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -521,7 +521,7 @@ impl Component for FileRevlogPopup { InspectCommitOpen::new(commit_id), ), )); - }; + } } else if key_match(key, self.key_config.keys.blame) { if let Some(open_request) = self.open_request.clone() diff --git a/src/popups/options.rs b/src/popups/options.rs index 0b06131b48..e74e8bdc0b 100644 --- a/src/popups/options.rs +++ b/src/popups/options.rs @@ -207,7 +207,7 @@ impl OptionsPopup { .borrow_mut() .diff_hunk_lines_change(true); } - }; + } } else { match self.selection { AppOption::StatusShowUntracked => { @@ -246,7 +246,7 @@ impl OptionsPopup { .borrow_mut() .diff_hunk_lines_change(false); } - }; + } } self.queue diff --git a/src/queue.rs b/src/queue.rs index 44268a851d..635fbc9e71 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -160,7 +160,7 @@ pub enum InternalEvent { } /// single threaded simple queue for components to communicate with each other -#[derive(Clone)] +#[derive(Clone, Default)] pub struct Queue { data: Rc>>, } diff --git a/src/tabs/stashing.rs b/src/tabs/stashing.rs index e5564563aa..5b2fe4b496 100644 --- a/src/tabs/stashing.rs +++ b/src/tabs/stashing.rs @@ -245,7 +245,7 @@ impl Component for Stashing { } else { Ok(EventState::NotConsumed) }; - }; + } } Ok(EventState::NotConsumed) diff --git a/src/tabs/stashlist.rs b/src/tabs/stashlist.rs index c8dceb2f63..1a97a0ac38 100644 --- a/src/tabs/stashlist.rs +++ b/src/tabs/stashlist.rs @@ -102,7 +102,7 @@ impl StashList { Action::StashDrop(ids) => self.drop(repo, ids)?, Action::StashPop(id) => self.pop(repo, *id)?, _ => (), - }; + } Ok(()) } diff --git a/src/tabs/status.rs b/src/tabs/status.rs index 034ffe39e6..40cf210786 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -348,7 +348,7 @@ impl Status { self.diff.focus(true); } - }; + } self.update_diff()?; diff --git a/src/ui/scrollbar.rs b/src/ui/scrollbar.rs index 6ae7ca3ee2..a0a9b3df80 100644 --- a/src/ui/scrollbar.rs +++ b/src/ui/scrollbar.rs @@ -49,7 +49,7 @@ impl Scrollbar { let right = area.right().saturating_sub(1); if right <= area.left() { return; - }; + } let (bar_top, bar_height) = { let scrollbar_area = area.inner(Margin { @@ -86,7 +86,7 @@ impl Scrollbar { let bottom = area.bottom().saturating_sub(1); if bottom <= area.top() { return; - }; + } let (bar_left, bar_width) = { let scrollbar_area = area.inner(Margin { diff --git a/src/ui/syntax_text.rs b/src/ui/syntax_text.rs index 8d758f20cc..057f9e3588 100644 --- a/src/ui/syntax_text.rs +++ b/src/ui/syntax_text.rs @@ -168,6 +168,10 @@ impl SyntaxText { } /// + #[expect( + clippy::missing_const_for_fn, + reason = "as of 1.86.0 clippy wants this to be const even though that breaks" + )] pub fn path(&self) -> &Path { &self.path }