Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package lychee for openSUSE:Factory checked in at 2026-05-04 12:53:16 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/lychee (Old) and /work/SRC/openSUSE:Factory/.lychee.new.30200 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "lychee" Mon May 4 12:53:16 2026 rev:16 rq:1350441 version:0.24.2 Changes: -------- --- /work/SRC/openSUSE:Factory/lychee/lychee.changes 2026-04-26 21:15:27.355570327 +0200 +++ /work/SRC/openSUSE:Factory/.lychee.new.30200/lychee.changes 2026-05-04 12:56:16.340283420 +0200 @@ -1,0 +2,15 @@ +Sat May 02 09:34:14 UTC 2026 - Alessio Biancalana <[email protected]> + +- Update 001-exclude-octocrab.patch: + * update the patch to the latest dependencies +- Update to version 0.24.2: + * chore: release v0.24.2 (#2166) + * chore: add pypi release (#1931) + * feat: user hints (#2021) + * chore: update all container images to debian trixie (#2177) + * chore(deps): bump the dependencies group with 8 updates + * ci: fix flaky binstall-check job (#2170) + * fix: typo in README.md (#2173) + * fix: update binstall metadata after 0.24.0 restructure (#2165) + +------------------------------------------------------------------- Old: ---- lychee-0.24.1.tar.zst New: ---- lychee-0.24.2.tar.zst ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ lychee.spec ++++++ --- /var/tmp/diff_new_pack.dO997S/_old 2026-05-04 12:56:17.616335940 +0200 +++ /var/tmp/diff_new_pack.dO997S/_new 2026-05-04 12:56:17.620336104 +0200 @@ -17,7 +17,7 @@ Name: lychee -Version: 0.24.1 +Version: 0.24.2 Release: 0 Summary: Fast, async, stream-based link checker written in Rust License: Apache-2.0 OR MIT ++++++ 001-exclude-octocrab.patch ++++++ --- /var/tmp/diff_new_pack.dO997S/_old 2026-05-04 12:56:17.652337421 +0200 +++ /var/tmp/diff_new_pack.dO997S/_new 2026-05-04 12:56:17.660337751 +0200 @@ -1,11 +1,11 @@ diff --git a/Cargo.toml b/Cargo.toml -index 14eea4ec..db0646ff 100644 +index 1e103671..0554e004 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = ["lychee-bin", "lychee-lib", "examples/*", "benches", "test-utils"] -+exclude = ["vendor/octocrab-0.49.8"] ++exclude = ["vendor/octocrab-0.49.9"] resolver = "2" [workspace.package] ++++++ _service ++++++ --- /var/tmp/diff_new_pack.dO997S/_old 2026-05-04 12:56:17.700339397 +0200 +++ /var/tmp/diff_new_pack.dO997S/_new 2026-05-04 12:56:17.704339562 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/lycheeverse/lychee.git</param> <param name="versionformat">@PARENT_TAG@</param> <param name="scm">git</param> - <param name="revision">lychee-v0.24.1</param> + <param name="revision">lychee-v0.24.2</param> <param name="match-tag">lychee-v*</param> <param name="versionrewrite-pattern">lychee-v(.*)</param> <param name="versionrewrite-replacement">\1</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.dO997S/_old 2026-05-04 12:56:17.748341373 +0200 +++ /var/tmp/diff_new_pack.dO997S/_new 2026-05-04 12:56:17.752341537 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/lycheeverse/lychee.git</param> - <param name="changesrevision">1f7e8d5524df4e6b3a9335445886fa54250063e2</param></service></servicedata> + <param name="changesrevision">2bba271688c1abb1503097a064e6c3bc1d1b6a9b</param></service></servicedata> (No newline at EOF) ++++++ lychee-0.24.1.tar.zst -> lychee-0.24.2.tar.zst ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/.devcontainer/Dockerfile new/lychee-0.24.2/.devcontainer/Dockerfile --- old/lychee-0.24.1/.devcontainer/Dockerfile 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/.devcontainer/Dockerfile 2026-05-01 17:38:40.000000000 +0200 @@ -1,4 +1,4 @@ -# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.236.0/containers/rust/.devcontainer/base.Dockerfile -# [Choice] Debian OS version (use bookworm on local arm64/Apple Silicon): buster, bullseye, bookworm -ARG VARIANT="bookworm" -FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT} \ No newline at end of file +# See here for image contents: https://github.com/devcontainers/images/blob/v0.4.26/src/rust/.devcontainer/Dockerfile +# [Choice] Debian OS version (use bookworm, or bullseye on local arm64/Apple Silicon): bookworm, buster, bullseye +ARG VARIANT="trixie" +FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/.devcontainer/devcontainer.json new/lychee-0.24.2/.devcontainer/devcontainer.json --- old/lychee-0.24.1/.devcontainer/devcontainer.json 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/.devcontainer/devcontainer.json 2026-05-01 17:38:40.000000000 +0200 @@ -5,9 +5,9 @@ "build": { "dockerfile": "Dockerfile", "args": { - // Use the VARIANT arg to pick a Debian OS version: buster, bullseye, bookworm - // Use bookworm when on local on arm64/Apple Silicon. - "VARIANT": "bookworm" + // Use the VARIANT arg to pick a Debian OS version: bookworm, buster, bullseye + // Use bookworm, or bullseye on local arm64/Apple Silicon. + "VARIANT": "trixie" } }, "runArgs": [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/.github/workflows/ci.yml new/lychee-0.24.2/.github/workflows/ci.yml --- old/lychee-0.24.1/.github/workflows/ci.yml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/.github/workflows/ci.yml 2026-05-01 17:38:40.000000000 +0200 @@ -68,6 +68,24 @@ - name: Verify the MSRV run: make verify + binstall-check: + runs-on: ubuntu-latest + # Skip on release-plz PRs: those bump the version to one that has not been + # published yet, so binstall cannot find a matching release. We still want + # PR coverage on regular PRs to catch regressions in the binstall metadata. + if: ${{ !startsWith(github.head_ref, 'release-plz-') }} + env: + # Authenticate against api.github.com to avoid 403 rate-limit failures + # when binstall queries GitHub releases. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v6 + - uses: cargo-bins/cargo-binstall@main + # Use `--manifest-path .` so binstall validates the in-tree binstall + # metadata. Resolving against the latest published version would mask + # metadata fixes (e.g. #2165) until after the next release ships. + - run: cargo binstall --manifest-path . --strategies crate-meta-data lychee --no-confirm + publish-check: runs-on: ubuntu-latest steps: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/.github/workflows/pypi.yml new/lychee-0.24.2/.github/workflows/pypi.yml --- old/lychee-0.24.1/.github/workflows/pypi.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/lychee-0.24.2/.github/workflows/pypi.yml 2026-05-01 17:38:40.000000000 +0200 @@ -0,0 +1,165 @@ +# This file is autogenerated by maturin v1.10.2 +# To update, run +# +# maturin generate-ci github +# +name: PyPI + +on: + push: + tags: + - "lychee-v*" + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + # check the issue https://github.com/aws/aws-lc-rs/issues/1022 + container: ghcr.io/rust-cross/manylinux_2_28-cross:aarch64 + - runner: ubuntu-22.04 + target: armv7 + - runner: ubuntu-22.04 + target: s390x + - runner: ubuntu-22.04 + target: ppc64le + steps: + - uses: actions/checkout@v6 + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: auto + container: ${{ matrix.platform.container }} + - name: Upload wheels + uses: actions/upload-artifact@v5 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + steps: + - uses: actions/checkout@v6 + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: musllinux_1_2 + - name: Upload wheels + uses: actions/upload-artifact@v5 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: dist + + windows: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: windows-latest + target: x64 + - runner: windows-latest + target: x86 + steps: + - uses: actions/checkout@v6 + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload wheels + uses: actions/upload-artifact@v5 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: dist + + macos: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: macos-15-intel + target: x86_64 + - runner: macos-14 + target: aarch64 + steps: + - uses: actions/checkout@v6 + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload wheels + uses: actions/upload-artifact@v5 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v5 + with: + name: wheels-sdist + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} + needs: [linux, musllinux, windows, macos, sdist] + permissions: + # Use to sign the release artifacts + id-token: write + # Used to upload release artifacts + contents: write + # Used to generate artifact attestation + attestations: write + steps: + - uses: actions/download-artifact@v6 + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v3 + with: + subject-path: "wheels-*/*" + - name: Publish to PyPI + if: ${{ startsWith(github.ref, 'refs/tags/') }} + uses: PyO3/maturin-action@v1 + with: + command: upload + args: --non-interactive --skip-existing wheels-*/* diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/Cargo.lock new/lychee-0.24.2/Cargo.lock --- old/lychee-0.24.1/Cargo.lock 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/Cargo.lock 2026-05-01 17:38:40.000000000 +0200 @@ -133,9 +133,9 @@ [[package]] name = "assert_cmd" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a686bbee5efb88a82df0621b236e74d925f470e5445d3220a5648b892ec99c9" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" dependencies = [ "anstyle", "bstr", @@ -451,9 +451,9 @@ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -473,18 +473,18 @@ [[package]] name = "clap_complete" -version = "4.6.1" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406e68b4de5c59cfb8f750a7cbd4d31ae153788b8352167c1e5f4fc26e8c91e9" +checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -592,11 +592,12 @@ [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -2188,6 +2189,21 @@ ] [[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2274,7 +2290,7 @@ [[package]] name = "lychee" -version = "0.24.1" +version = "0.24.2" dependencies = [ "anyhow", "assert-json-diff", @@ -2325,7 +2341,7 @@ [[package]] name = "lychee-lib" -version = "0.24.1" +version = "0.24.2" dependencies = [ "async-stream", "async-trait", @@ -2583,9 +2599,9 @@ [[package]] name = "octocrab" -version = "0.49.7" +version = "0.49.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63f6687a23731011d0117f9f4c3cdabaa7b5e42ca671f42b5cc0657c492540e3" +checksum = "4ddbc3bb87e8c680febf16f56855bbd8b44a38e18c913334213ab34908e71a09" dependencies = [ "arc-swap", "async-trait", @@ -3346,9 +3362,9 @@ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64", "bytes", @@ -4205,7 +4221,7 @@ [[package]] name = "test-utils" -version = "0.24.1" +version = "0.24.2" [[package]] name = "testing_table" @@ -4333,9 +4349,9 @@ [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -4654,9 +4670,9 @@ [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.1", "js-sys", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/Cargo.toml new/lychee-0.24.2/Cargo.toml --- old/lychee-0.24.1/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -3,7 +3,7 @@ resolver = "2" [workspace.package] -version = "0.24.1" +version = "0.24.2" [profile.release] debug = true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/Dockerfile new/lychee-0.24.2/Dockerfile --- old/lychee-0.24.1/Dockerfile 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/Dockerfile 2026-05-01 17:38:40.000000000 +0200 @@ -1,4 +1,4 @@ -FROM rust:bookworm as builder +FROM rust:trixie AS builder RUN USER=root cargo new --bin lychee WORKDIR /lychee @@ -23,7 +23,7 @@ # Our production image starts here, which uses # the files from the builder image above. -FROM debian:bookworm-slim +FROM debian:trixie-slim RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y \ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/Dockerfile-CI.Dockerfile new/lychee-0.24.2/Dockerfile-CI.Dockerfile --- old/lychee-0.24.1/Dockerfile-CI.Dockerfile 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/Dockerfile-CI.Dockerfile 2026-05-01 17:38:40.000000000 +0200 @@ -1,4 +1,4 @@ -FROM debian:bookworm-slim AS builder +FROM debian:trixie-slim AS builder WORKDIR /builder ARG LYCHEE_VERSION="latest" @@ -21,7 +21,7 @@ && wget -O - "$BASE_URL/lychee-$ARCH-unknown-linux-gnu.tar.gz" | tar -xz --strip-components 1 \ && chmod +x lychee -FROM debian:bookworm-slim +FROM debian:trixie-slim RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y \ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/README.md new/lychee-0.24.2/README.md --- old/lychee-0.24.1/README.md 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/README.md 2026-05-01 17:38:40.000000000 +0200 @@ -178,7 +178,7 @@ Note that in the past lychee could be configured to use either OpenSSL or Rustls. [It was decided](https://github.com/lycheeverse/lychee/pull/1928) to fully switch to Rustls and drop OpenSSL support. -Please tell us if this this negatively affects you in any way. +Please tell us if this negatively affects you in any way. ## Features diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/benches/Cargo.toml new/lychee-0.24.2/benches/Cargo.toml --- old/lychee-0.24.1/benches/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/benches/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -10,7 +10,7 @@ [dependencies] lychee-lib = { path = "../lychee-lib", default-features = false } criterion = "0.8.2" -tokio = "1.51.1" +tokio = "1.52.1" [features] email-check = ["lychee-lib/email-check"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/examples/archive/Cargo.toml new/lychee-0.24.2/examples/archive/Cargo.toml --- old/lychee-0.24.1/examples/archive/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/examples/archive/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -9,7 +9,7 @@ [dependencies] lychee-lib = { path = "../../lychee-lib", default-features = false } -tokio = { version = "1.51.1", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } url = "2.5.8" [features] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/examples/builder/Cargo.toml new/lychee-0.24.2/examples/builder/Cargo.toml --- old/lychee-0.24.1/examples/builder/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/examples/builder/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -9,10 +9,10 @@ [dependencies] lychee-lib = { path = "../../lychee-lib", default-features = false } -tokio = { version = "1.51.1", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } regex = "1.12.2" http = "1.4.0" -reqwest = "0.13.2" +reqwest = "0.13.3" [features] email-check = ["lychee-lib/email-check"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/examples/chain/Cargo.toml new/lychee-0.24.2/examples/chain/Cargo.toml --- old/lychee-0.24.1/examples/chain/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/examples/chain/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -10,8 +10,8 @@ [dependencies] async-trait = "0.1.88" lychee-lib = { path = "../../lychee-lib", default-features = false } -reqwest = "0.13.2" -tokio = { version = "1.51.1", features = ["full"] } +reqwest = "0.13.3" +tokio = { version = "1.52.1", features = ["full"] } [features] email-check = ["lychee-lib/email-check"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/examples/client_pool/Cargo.toml new/lychee-0.24.2/examples/client_pool/Cargo.toml --- old/lychee-0.24.1/examples/client_pool/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/examples/client_pool/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -11,7 +11,7 @@ futures = "0.3.32" tokio-stream = "0.1.18" lychee-lib = { path = "../../lychee-lib", default-features = false } -tokio = { version = "1.51.1", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } [features] email-check = ["lychee-lib/email-check"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/examples/collect_links/Cargo.toml new/lychee-0.24.2/examples/collect_links/Cargo.toml --- old/lychee-0.24.1/examples/collect_links/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/examples/collect_links/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -9,11 +9,11 @@ [dependencies] lychee-lib = { path = "../../lychee-lib", default-features = false } -tokio = { version = "1.51.1", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } regex = "1.12.2" http = "1.4.0" tokio-stream = "0.1.18" -reqwest = "0.13.2" +reqwest = "0.13.3" [features] email-check = ["lychee-lib/email-check"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/examples/extract/Cargo.toml new/lychee-0.24.2/examples/extract/Cargo.toml --- old/lychee-0.24.1/examples/extract/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/examples/extract/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -9,7 +9,7 @@ [dependencies] lychee-lib = { path = "../../lychee-lib", default-features = false } -tokio = { version = "1.51.1", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } [features] email-check = ["lychee-lib/email-check"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/examples/simple/Cargo.toml new/lychee-0.24.2/examples/simple/Cargo.toml --- old/lychee-0.24.1/examples/simple/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/examples/simple/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -9,7 +9,7 @@ [dependencies] lychee-lib = { path = "../../lychee-lib", default-features = false } -tokio = { version = "1.51.1", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } [features] email-check = ["lychee-lib/email-check"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/fixtures/TEST_GITHUB_404.md new/lychee-0.24.2/fixtures/TEST_GITHUB_404.md --- old/lychee-0.24.1/fixtures/TEST_GITHUB_404.md 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/fixtures/TEST_GITHUB_404.md 1970-01-01 01:00:00.000000000 +0100 @@ -1,3 +0,0 @@ -Test file: contains a single **invalid** (e.g. 404) GitHub URL. - -Lychee: https://github.com/mre/idiomatic-rust-doesnt-exist-man diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-bin/CHANGELOG.md new/lychee-0.24.2/lychee-bin/CHANGELOG.md --- old/lychee-0.24.1/lychee-bin/CHANGELOG.md 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-bin/CHANGELOG.md 2026-05-01 17:38:40.000000000 +0200 @@ -7,6 +7,21 @@ ## [Unreleased] +## [0.24.2](https://github.com/lycheeverse/lychee/compare/lychee-v0.24.1...lychee-v0.24.2) - 2026-04-30 + +### Added + +- user hints ([#2021](https://github.com/lycheeverse/lychee/pull/2021)) + +### Fixed + +- typo in README.md ([#2173](https://github.com/lycheeverse/lychee/pull/2173)) +- update binstall metadata after 0.24.0 restructure ([#2165](https://github.com/lycheeverse/lychee/pull/2165)) + +### Other + +- *(deps)* bump the dependencies group with 8 updates + ## [0.24.1](https://github.com/lycheeverse/lychee/compare/lychee-v0.24.0...lychee-v0.24.1) - 2026-04-24 ### Other diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-bin/Cargo.toml new/lychee-0.24.2/lychee-bin/Cargo.toml --- old/lychee-0.24.1/lychee-bin/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-bin/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -15,15 +15,15 @@ [dependencies] # NOTE: We need to specify the version of lychee-lib here because crates.io # requires all dependencies to have a version number. -lychee-lib = { path = "../lychee-lib", version = "0.24.1", default-features = false } +lychee-lib = { path = "../lychee-lib", version = "0.24.2", default-features = false } anyhow = "1.0.102" assert-json-diff = "2.0.2" -clap = { version = "4.6.0", features = ["env", "derive", "cargo", "string"] } -clap_complete = "4.6.1" +clap = { version = "4.6.1", features = ["env", "derive", "cargo", "string"] } +clap_complete = "4.6.3" clap_mangen = "0.3.0" console = "0.16.3" -const_format = "0.2.35" +const_format = "0.2.36" csv = "1.4.0" dashmap = { version = "6.1.0", features = ["serde"] } env_logger = "0.11.10" @@ -37,7 +37,7 @@ indicatif = "0.18.4" log = "0.4.28" regex = "1.12.2" -reqwest = "0.13.2" +reqwest = "0.13.3" reqwest_cookie_store = { version = "0.10.0", features = ["serde"] } rlimit = "0.11.0" # Make build work on Apple Silicon. @@ -51,14 +51,14 @@ strum = { version = "0.28.0", features = ["derive"] } supports-color = "3.0.2" tabled = "0.20.0" -tokio = { version = "1.51.1", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } tokio-stream = "0.1.18" toml = "1.1.2" url = "2.5.8" quick-junit = "0.6.0" [dev-dependencies] -assert_cmd = "2.2.0" +assert_cmd = "2.2.1" cookie_store = "0.22.1" predicates = "3.1.4" pretty_assertions = "1.4.1" @@ -69,7 +69,7 @@ "registry", "env-filter", ] } -uuid = { version = "1.23.0", features = ["v4"] } +uuid = { version = "1.23.1", features = ["v4"] } wiremock = "0.6.5" [features] @@ -93,5 +93,3 @@ # Metadata for cargo-binstall to get the right artifacts [package.metadata.binstall] pkg-url = "{ repo }/releases/download/{ name }-v{ version }/{ name }-{ target }{ archive-suffix }" -bin-dir = "{ bin }{ binary-ext }" -pkg-fmt = "tgz" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-bin/src/formatters/response/color.rs new/lychee-0.24.2/lychee-bin/src/formatters/response/color.rs --- old/lychee-0.24.1/lychee-bin/src/formatters/response/color.rs 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-bin/src/formatters/response/color.rs 2026-05-01 17:38:40.000000000 +0200 @@ -1,3 +1,5 @@ +use std::sync::LazyLock; + use lychee_lib::{CacheStatus, ResponseBody, Status}; use crate::formatters::color::{DIM, GREEN, PINK, YELLOW}; @@ -13,7 +15,7 @@ impl ColorFormatter { /// Determine the color for formatted output based on the status of the /// response. - fn status_color(status: &Status) -> &'static std::sync::LazyLock<console::Style> { + fn status_color(status: &Status) -> &'static LazyLock<console::Style> { match status { Status::Ok(_) | Status::Cached(CacheStatus::Ok(_)) => &GREEN, Status::Excluded diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-bin/src/formatters/stats/mod.rs new/lychee-0.24.2/lychee-bin/src/formatters/stats/mod.rs --- old/lychee-0.24.1/lychee-bin/src/formatters/stats/mod.rs 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-bin/src/formatters/stats/mod.rs 2026-05-01 17:38:40.000000000 +0200 @@ -23,7 +23,10 @@ io::{Write, stdout}, }; -use crate::{config::Config, formatters::get_stats_formatter}; +use crate::{ + config::{Config, OutputMode}, + formatters::{color::YELLOW, get_stats_formatter}, +}; use anyhow::{Context, Result}; use lychee_lib::{InputSource, ratelimit::HostStatsMap}; @@ -40,7 +43,7 @@ fn format(&self, stats: OutputStats) -> Result<String>; } -/// If configured to do so, output response statistics to stdout or the specified output file. +/// Output response statistics to stdout or the specified output file. pub(crate) fn output_statistics(stats: OutputStats, config: &Config) -> Result<()> { let formatter = get_stats_formatter(&config.format(), &config.mode()); let formatted_stats = formatter.format(stats)?; @@ -54,6 +57,25 @@ Ok(()) } +/// Output hints to stderr. +pub(crate) fn output_hints(config: &Config) { + let hints = lychee_lib::get_hints(); + + if config.verbose().log_level() <= log::Level::Error { + return; // log level is too low + } + + let prefix = match &config.mode() { + OutputMode::Emoji => "💡".into(), + OutputMode::Color => YELLOW.apply_to("Hint").to_string() + ":", + OutputMode::Plain | OutputMode::Task => "Hint:".into(), + }; + + for hint in hints { + eprintln!("{prefix} {hint}"); + } +} + /// Convert a `ResponseStats` `HashMap` to a sorted Vec of key-value pairs. /// The returned keys and values are both sorted in natural, case-insensitive order. /// Additionally, the returned keys are deduplicated and their values are merged. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-bin/src/hints.rs new/lychee-0.24.2/lychee-bin/src/hints.rs --- old/lychee-0.24.1/lychee-bin/src/hints.rs 1970-01-01 01:00:00.000000000 +0100 +++ new/lychee-0.24.2/lychee-bin/src/hints.rs 2026-05-01 17:38:40.000000000 +0200 @@ -0,0 +1,111 @@ +use http::StatusCode; +use lychee_lib::{ErrorKind, Status, StatusCodeSelector, hint}; + +use crate::{config::Config, formatters::stats::ResponseStats}; + +/// Collect [`lychee_lib::Hint`]s based on the resulting statistics. +pub(crate) fn handle_stats(stats: &ResponseStats, config: &Config) { + rate_limit(stats, config); + github_rate_limit(stats, config); + any_redirects(stats, config); + rejected_status_codes(stats, config); + unfollowed_redirects(stats, config); +} + +fn rate_limit(stats: &ResponseStats, config: &Config) { + let default_host_config = config.hosts.is_empty(); + let first_rate_limited_domain = stats + .error_map + .values() + .flatten() + .find(|r| { + matches!( + r.status, + Status::Error(ErrorKind::RejectedStatusCode(StatusCode::TOO_MANY_REQUESTS)) + ) + }) + .and_then(|b| b.uri.domain()); + + if default_host_config && let Some(domain) = first_rate_limited_domain { + hint!( + "Encountered rate limit responses. \ + You might be able to work around this by adding `[hosts.\"{domain}\"]` to the TOML config \ + to adjust the `concurrency` and `request_interval` values." + ); + } +} + +/// Github rate limits can be circumvented by specifying a token. +/// According to the [docs]: +/// +/// > If you exceed your primary rate limit, you will receive a 403 or 429 response +/// +/// [docs]: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2026-03-10#exceeding-the-rate-limit +fn github_rate_limit(stats: &ResponseStats, config: &Config) { + let any_github_errors = stats.error_map.values().flatten().any(|body| { + let is_github = body + .uri + .domain() + .is_some_and(|domain| domain.ends_with("github.com")); + + let is_rate_limited = matches!( + body.status.code(), + Some(StatusCode::FORBIDDEN | StatusCode::TOO_MANY_REQUESTS) + ); + + is_github && is_rate_limited + }); + + if config.github_token.is_none() && any_github_errors { + hint!( + "GitHub seems to be rate limiting us. \ + You could try setting a GitHub token with `--github-token`" + ); + } +} + +fn any_redirects(stats: &ResponseStats, config: &Config) { + let count = stats.redirects; + let has_redirects = count > 0; + let hides_redirects = config.verbose().log_level() < log::Level::Info; + + if has_redirects && hides_redirects { + let noun = if count == 1 { "redirect" } else { "redirects" }; + hint!( + "Followed {count} {noun}. \ + You might want to consider replacing redirecting URLs with the resolved URLs. \ + Use verbose mode (`-v`/`-vv`) to see redirection details." + ); + } +} + +fn rejected_status_codes(stats: &ResponseStats, config: &Config) { + let is_default = config.accept() == StatusCodeSelector::default_accepted(); + let any_rejected_codes = stats + .error_map + .values() + .flatten() + .any(|r| matches!(r.status, Status::Error(ErrorKind::RejectedStatusCode(_)))); + + if is_default && any_rejected_codes { + hint!("You can configure accepted/rejected response codes with `-a` or `--accept`"); + } +} + +fn unfollowed_redirects(stats: &ResponseStats, config: &Config) { + let is_small_limit = config.max_redirects() <= lychee_lib::DEFAULT_MAX_REDIRECTS; + let any_rejected_redirection_codes = stats.error_map.values().flatten().any(|r| { + matches!( + r.status, + Status::Error(ErrorKind::RejectedStatusCode(s)) if s.is_redirection() + ) + }); + + if is_small_limit && any_rejected_redirection_codes { + hint!( + "Rejected redirectional status codes. \ + This means some redirects were not followed. \ + You might want to increase the limit for `-m`/`--max-redirects`." + ); + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-bin/src/main.rs new/lychee-0.24.2/lychee-bin/src/main.rs --- old/lychee-0.24.1/lychee-bin/src/main.rs 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-bin/src/main.rs 2026-05-01 17:38:40.000000000 +0200 @@ -84,12 +84,13 @@ mod config; mod files_from; mod formatters; +mod hints; mod parse; mod progress; mod time; mod verbosity; -use crate::formatters::stats::{OutputStats, ResponseStats, output_statistics}; +use crate::formatters::stats::{OutputStats, output_hints, output_statistics}; use crate::{ cache::Cache, config::{Config, LYCHEE_CACHE_FILE, LYCHEE_IGNORE_FILE, LycheeOptions}, @@ -445,8 +446,7 @@ commands::dump(params).await? } else { let (response_stats, cache, exit_code, host_pool) = commands::check(params).await?; - github_warning(&response_stats, &opts.config); - redirect_warning(&response_stats, &opts.config); + hints::handle_stats(&response_stats, &opts.config); let stats = OutputStats { response_stats, @@ -466,40 +466,10 @@ cookie_jar.save().context("Cannot save cookie jar")?; } + output_hints(&opts.config); + exit_code }; Ok(exit_code as i32) } - -/// Display user-friendly message if there were any issues with GitHub URLs -fn github_warning(stats: &ResponseStats, config: &Config) { - let github_errors = stats - .error_map - .values() - .flatten() - .any(|body| body.uri.domain() == Some("github.com")); - - if github_errors && config.github_token.is_none() { - warn!( - "There were issues with GitHub URLs. You could try setting a GitHub token and running lychee again.", - ); - } -} - -/// Display user-friendly message if there were any redirects -/// in non-verbose mode. -fn redirect_warning(stats: &ResponseStats, config: &Config) { - let redirects = stats.redirects; - if redirects > 0 && config.verbose().log_level() < log::Level::Info { - let noun = if redirects == 1 { - "redirect" - } else { - "redirects" - }; - - warn!( - "lychee detected {redirects} {noun}. You might want to consider replacing redirecting URLs with the resolved URLs. Run lychee in verbose mode (-v/--verbose) to see details about the redirections.", - ); - } -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-bin/tests/cli.rs new/lychee-0.24.2/lychee-bin/tests/cli.rs --- old/lychee-0.24.1/lychee-bin/tests/cli.rs 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-bin/tests/cli.rs 2026-05-01 17:38:40.000000000 +0200 @@ -268,12 +268,11 @@ /// See https://github.com/lycheeverse/lychee/issues/1355 #[test] fn test_valid_json_output_to_stdout_on_error() -> Result<()> { - let test_path = fixtures_path!().join("TEST_GITHUB_404.md"); - let mut cmd = cargo_bin_cmd!(); cmd.arg("--format") .arg("json") - .arg(test_path) + .arg("-") + .write_stdin("https://github.com/mre/idiomatic-rust-doesnt-exist-man") .assert() .failure() .code(2); @@ -652,25 +651,6 @@ .stdout(contains("1 Excluded")); } - #[test] - fn test_failure_github_404_no_token() { - let test_github_404_path = fixtures_path!().join("TEST_GITHUB_404.md"); - - cargo_bin_cmd!() - .arg(test_github_404_path) - .arg("--no-progress") - .env_clear() - .assert() - .failure() - .code(2) - .stdout(contains( - r#"[404] https://github.com/mre/idiomatic-rust-doesnt-exist-man (at 3:9) | Rejected status code: 404 Not Found (configurable with "accept" option)"# - )) - .stderr(contains( - "There were issues with GitHub URLs. You could try setting a GitHub token and running lychee again.", - )); - } - #[tokio::test] async fn test_stdin_input() { let mock_server = mock_server!(StatusCode::OK); @@ -1413,7 +1393,7 @@ mock_server_ok.uri() ))) .stderr(contains(format!( - "[404] {}/ (at 2:1) | Rejected status code: 404 Not Found (configurable with \"accept\" option)\n", + "[404] {}/ (at 2:1) | Rejected status code: 404 Not Found\n", mock_server_err.uri() ))); @@ -1465,13 +1445,16 @@ // Run first without cache to generate the cache file test_cmd .assert() - .stderr(contains(format!("[200] {}/ (at 1:1)\n", mock_server_ok.uri()))) + .stderr(contains(format!( + "[200] {}/ (at 1:1)\n", + mock_server_ok.uri() + ))) .stderr(contains(format!( "[204] {}/ (at 2:1) | 204 No Content\n", mock_server_no_content.uri() ))) .stderr(contains(format!( - "[429] {}/ (at 3:1) | Rejected status code: 429 Too Many Requests (configurable with \"accept\" option)", + "[429] {}/ (at 3:1) | Rejected status code: 429 Too Many Requests\n", mock_server_too_many_requests.uri() ))); @@ -1529,11 +1512,11 @@ .failure() .code(2) .stdout(contains(format!( - r#"[418] {}/ (at 2:1) | Rejected status code: 418 I'm a teapot (configurable with "accept" option)"#, + r#"[418] {}/ (at 2:1) | Rejected status code: 418 I'm a teapot"#, mock_server_teapot.uri() ))) .stdout(contains(format!( - r#"[500] {}/ (at 3:1) | Rejected status code: 500 Internal Server Error (configurable with "accept" option)"#, + r#"[500] {}/ (at 3:1) | Rejected status code: 500 Internal Server Error"#, mock_server_server_error.uri() ))); @@ -1581,7 +1564,7 @@ .failure() .code(2) .stdout(contains(format!( - r#"[200] {}/ (at 1:1) | Rejected status code: 200 OK (configurable with "accept" option)"#, + r#"[200] {}/ (at 1:1) | Rejected status code: 200 OK"#, mock_server_200.uri() ))); @@ -2015,7 +1998,7 @@ .assert() .failure() .stdout(contains(r#" -[404] http://rust-lang.org/lycheeverse (at 1:1) | Rejected status code: 404 Not Found (configurable with "accept" option) | Remaps: http://github.com/lycheeverse --> http://rust-lang.org/lycheeverse | Followed 1 redirect. Redirects: http://rust-lang.org/lycheeverse --[301]--> https://rust-lang.org/lycheeverse +[404] http://rust-lang.org/lycheeverse (at 1:1) | Rejected status code: 404 Not Found | Remaps: http://github.com/lycheeverse --> http://rust-lang.org/lycheeverse | Followed 1 redirect. Redirects: http://rust-lang.org/lycheeverse --[301]--> https://rust-lang.org/lycheeverse "#)) // It is debugged when URIs are remapped .stderr(contains("[DEBUG] Remapping http://github.com/lycheeverse --> http://rust-lang.org/lycheeverse")) @@ -2705,7 +2688,7 @@ // Non-verbose mode redirecting_mock_server!(async |redirect_url: Url, _| { let (json, stderr) = run(&redirect_url, false); - assert!(stderr.contains("[WARN] lychee detected 1 redirect. You might want to consider replacing redirecting URLs")); + assert!(stderr.contains("Hint: Followed 1 redirect. You might want to")); assert_eq!(json["total"], 1); assert_eq!(json["redirects"], 1); // there was one redirect assert_eq!(json["successful"], 1); // which resolved to a success @@ -4162,6 +4145,7 @@ .assert() .success(); } + /// Verifies that loading an older, legacy `.lycheecache` file containing a cached error /// correctly drops the error and successfully retries the link. /// This ensures we don't break existing user CI workflows that have older cache files @@ -4535,8 +4519,7 @@ std::fs::write(dir.path().join("exclude_workspace.txt"), "")?; std::fs::write(dir.path().join("exclude_package.txt"), "")?; - let mut cmd = cargo_bin_cmd!(); - let assert = cmd + let assert = cargo_bin_cmd!() .current_dir(dir.path()) .arg("--dump-inputs") .arg(".") @@ -4549,6 +4532,58 @@ assert!(output.contains("exclude_workspace.txt")); Ok(()) } + + /// Verify that lychee will fail before all checks run if the parent of the given output path does not exist + /// See https://github.com/lycheeverse/lychee/issues/2147 + #[test] + fn test_output_invalid_path() { + cargo_bin_cmd!() + .arg("--output") + .arg("does/not/exist") + .arg("-") + .assert().failure().stderr(contains( + "Output path `does/not/exist` is not writable: parent directory `does/not` does not exist", + )); + } + + #[tokio::test] + async fn test_user_hints() { + cargo_bin_cmd!() + .arg("-") + .write_stdin("http://wikipedia.org/this/does/not/exist") + .assert() + .stderr(contains("Hint: Followed 1 redirect. You might want to consider replacing redirecting URLs with the resolved URLs. Use verbose mode (`-v`/`-vv`) to see redirection details.\n")) + .stderr(contains("Hint: You can configure accepted/rejected response codes with `-a` or `--accept`\n")); + + cargo_bin_cmd!() + .arg("-") + .arg("--max-redirects=0") + .write_stdin("http://rust-lang.org") + .assert() + .stderr(contains("Hint: Rejected redirectional status codes. This means some redirects were not followed. You might want to increase the limit for `-m`/`--max-redirects`.")); + + cargo_bin_cmd!() + .arg("-") + .arg("--max-retries=0") + .write_stdin("https://http.codes/429") + .assert() + .stderr(contains(r#"Hint: Encountered rate limit responses. You might be able to work around this by adding `[hosts."http.codes"]` to the TOML config to adjust the `concurrency` and `request_interval` values."#)); + + cargo_bin_cmd!() + .arg("-") + .arg("--accept=200..201,300..301") + .assert() + .stderr(contains(r#"Hint: Accept range `200..201` only matches the single status code 200. Did you mean `200..=201` or `200,201`?"#)) + .stderr(contains(r#"Hint: Accept range `300..301` only matches the single status code 300. Did you mean `300..=301` or `300,301`?"#)); + + cargo_bin_cmd!() + .arg("-") + .arg("--max-redirects=0") + .write_stdin("http://rust-lang.org") + .arg("-q") // hide user hints + .assert() + .stderr(contains("Hint").not()); + } } #[cfg(unix)] @@ -4575,16 +4610,3 @@ "System file descriptor limit is 64 which is too low for the requested concurrency of 128. Lowering `max_concurrency` to 44", )); } - -// Verify that lychee will fail before all checks run if the parent of the given output path does not exist -// See https://github.com/lycheeverse/lychee/issues/2147 -#[test] -fn test_output_invalid_path() { - let mut cmd = assert_cmd::Command::cargo_bin("lychee").unwrap(); - cmd.arg("--output") - .arg("does/not/exist") - .arg("https://example.com"); - cmd.assert().failure().stderr(predicates::str::contains( - "Output path `does/not/exist` is not writable: parent directory `does/not` does not exist", - )); -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-lib/CHANGELOG.md new/lychee-0.24.2/lychee-lib/CHANGELOG.md --- old/lychee-0.24.1/lychee-lib/CHANGELOG.md 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-lib/CHANGELOG.md 2026-05-01 17:38:40.000000000 +0200 @@ -7,6 +7,20 @@ ## [Unreleased] +## [0.24.2](https://github.com/lycheeverse/lychee/compare/lychee-lib-v0.24.1...lychee-lib-v0.24.2) - 2026-04-30 + +### Added + +- user hints ([#2021](https://github.com/lycheeverse/lychee/pull/2021)) + +### Fixed + +- typo in README.md ([#2173](https://github.com/lycheeverse/lychee/pull/2173)) + +### Other + +- *(deps)* bump the dependencies group with 8 updates + ## [0.24.1](https://github.com/lycheeverse/lychee/compare/lychee-lib-v0.24.0...lychee-lib-v0.24.1) - 2026-04-24 ### Other diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-lib/Cargo.toml new/lychee-0.24.2/lychee-lib/Cargo.toml --- old/lychee-0.24.1/lychee-lib/Cargo.toml 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-lib/Cargo.toml 2026-05-01 17:38:40.000000000 +0200 @@ -33,7 +33,7 @@ linkify = "0.11.0" log = "0.4.28" mailify-lib = { version = "0.2.0", optional = true } -octocrab = { version = "0.49.7", default-features = false, features = [ +octocrab = { version = "0.49.9", default-features = false, features = [ "default-client", "follow-redirect", "jwt-rust-crypto", @@ -49,7 +49,7 @@ pulldown-cmark = "0.13.3" quick-xml = "0.39.2" regex = "1.12.2" -reqwest = { version = "0.13.2", features = ["json", "gzip"] } +reqwest = { version = "0.13.3", features = ["json", "gzip"] } reqwest_cookie_store = { version = "0.10.0", features = ["serde"] } # Make build work on Apple Silicon. # See https://github.com/briansmith/ring/issues/1163 @@ -62,7 +62,7 @@ shellexpand = "3.1.2" strum = { version = "0.28.0", features = ["derive"] } thiserror = "2.0.18" -tokio = { version = "1.51.1", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } toml = "1.1.2" typed-builder = "0.23.2" url = { version = "2.5.8", features = ["serde"] } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-lib/src/lib.rs new/lychee-0.24.2/lychee-lib/src/lib.rs --- old/lychee-0.24.1/lychee-lib/src/lib.rs 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-lib/src/lib.rs 2026-05-01 17:38:40.000000000 +0200 @@ -103,7 +103,7 @@ BaseInfo, BasicAuthCredentials, BasicAuthSelector, CacheStatus, CookieJar, ErrorKind, FileExtensions, FileType, Input, InputContent, InputResolver, InputSource, LycheeResult, Preprocessor, Redirect, Redirects, Request, RequestError, ResolvedInputSource, Response, - ResponseBody, Result, Status, StatusCodeSelector, StatusRange, StatusRangeError, + ResponseBody, Result, Status, StatusCodeSelector, StatusRange, StatusRangeError, hints::*, uri::raw::RawUri, uri::raw::RawUriSpan, uri::valid::Uri, }, }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-lib/src/ratelimit/config.rs new/lychee-0.24.2/lychee-lib/src/ratelimit/config.rs --- old/lychee-0.24.1/lychee-lib/src/ratelimit/config.rs 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-lib/src/ratelimit/config.rs 2026-05-01 17:38:40.000000000 +0200 @@ -65,11 +65,17 @@ } /// Get the number of [`HostConfig`]s - #[cfg(test)] - pub(crate) fn len(&self) -> usize { + #[must_use] + pub fn len(&self) -> usize { self.0.len() } + /// Returns `true` if if there are no [`HostConfig`]s + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Get the iterator over all elements pub(crate) fn iter(&self) -> Iter<'_, HostKey, HostConfig> { self.0.iter() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-lib/src/types/accept/range.rs new/lychee-0.24.2/lychee-lib/src/types/accept/range.rs --- old/lychee-0.24.1/lychee-lib/src/types/accept/range.rs 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-lib/src/types/accept/range.rs 2026-05-01 17:38:40.000000000 +0200 @@ -7,6 +7,8 @@ use regex::Regex; use thiserror::Error; +use crate::hint; + /// Smallest accepted value const MIN: u16 = 100; @@ -75,6 +77,12 @@ if inclusive { Self::new(start, end) } else { + if end == start + 1 { + hint!( + "Accept range `{s}` only matches the single status code {start}. \ + Did you mean `{start}..={end}` or `{start},{end}`?" + ); + } Self::new(start, end - 1) } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-lib/src/types/error.rs new/lychee-0.24.2/lychee-lib/src/types/error.rs --- old/lychee-0.24.1/lychee-lib/src/types/error.rs 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-lib/src/types/error.rs 2026-05-01 17:38:40.000000000 +0200 @@ -152,7 +152,7 @@ /// The given status code was not accepted (this depends on the `accept` configuration) #[error( - r#"Rejected status code: {code} {reason} (configurable with "accept" option)"#, + "Rejected status code: {code} {reason}", code = .0.as_str(), reason = .0.canonical_reason().unwrap_or("Unknown status code") )] @@ -479,16 +479,7 @@ use crate::ErrorKind; #[test] fn test_error_kind_details() { - // Test rejected status code let status_error = ErrorKind::RejectedStatusCode(http::StatusCode::NOT_FOUND); assert!(status_error.to_string().contains("Not Found")); - - // Test redirected status code - let redir_error = ErrorKind::RejectedStatusCode(http::StatusCode::MOVED_PERMANENTLY); - assert!( - redir_error - .details() - .contains(r#"(configurable with "accept" option)"#) - ); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-lib/src/types/hints.rs new/lychee-0.24.2/lychee-lib/src/types/hints.rs --- old/lychee-0.24.1/lychee-lib/src/types/hints.rs 1970-01-01 01:00:00.000000000 +0100 +++ new/lychee-0.24.2/lychee-lib/src/types/hints.rs 2026-05-01 17:38:40.000000000 +0200 @@ -0,0 +1,63 @@ +//! Provide the means to display practical user-friendly messages, +//! which are collected during runtime. + +use std::{ + collections::{BTreeSet, btree_set::IntoIter}, + fmt::Display, + sync::Mutex, +}; + +/// Hints are accumulated during the whole program invocation. +static HINTS: Mutex<BTreeSet<Hint>> = Mutex::new(BTreeSet::new()); + +/// An informative and friendly message created during the invocation of the program +/// to be displayed before termination, to improve user experience. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Hint(String); + +impl Display for Hint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<String> for Hint { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for Hint { + fn from(value: &str) -> Self { + Self(value.to_owned()) + } +} + +/// Collect a [`Hint`] to optionally be shown to users +/// at a later point in time by using [`get_hints`]. +/// +/// +/// # Panics +/// +/// Panics if the mutex is poisoned +pub fn add_hint(hint: Hint) { + HINTS.lock().unwrap().insert(hint); +} + +/// Format and collect a [`Hint`]. +/// Helper macro for [`add_hint`]. +#[macro_export] +macro_rules! hint { + ($($arg:tt)*) => {{ + $crate::add_hint(format!($($arg)*).into()); + }}; +} + +/// Get [`Hint`]s to report to users +/// +/// # Panics +/// +/// Panics if the mutex is poisoned +pub fn get_hints() -> IntoIter<Hint> { + HINTS.lock().unwrap().clone().into_iter() +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/lychee-lib/src/types/mod.rs new/lychee-0.24.2/lychee-lib/src/types/mod.rs --- old/lychee-0.24.1/lychee-lib/src/types/mod.rs 2026-04-24 17:35:48.000000000 +0200 +++ new/lychee-0.24.2/lychee-lib/src/types/mod.rs 2026-05-01 17:38:40.000000000 +0200 @@ -7,6 +7,7 @@ mod cookies; mod error; mod file; +pub(crate) mod hints; mod input; mod preprocessor; pub(crate) mod redirect_history; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lychee-0.24.1/pyproject.toml new/lychee-0.24.2/pyproject.toml --- old/lychee-0.24.1/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 +++ new/lychee-0.24.2/pyproject.toml 2026-05-01 17:38:40.000000000 +0200 @@ -0,0 +1,24 @@ +[build-system] +requires = ["maturin>=1.10,<2.0"] +build-backend = "maturin" + +[project] +name = "lychee-bin" +requires-python = ">=3" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", +] +dynamic = [ + "version", + "description", + "keywords", + "authors", + "urls", + "readme", + "license", +] + +[tool.maturin] +bindings = "bin" +manifest-path = "lychee-bin/Cargo.toml" ++++++ lychee.obsinfo ++++++ --- /var/tmp/diff_new_pack.dO997S/_old 2026-05-04 12:56:18.372367055 +0200 +++ /var/tmp/diff_new_pack.dO997S/_new 2026-05-04 12:56:18.380367385 +0200 @@ -1,5 +1,5 @@ name: lychee -version: 0.24.1 -mtime: 1777044948 -commit: 1f7e8d5524df4e6b3a9335445886fa54250063e2 +version: 0.24.2 +mtime: 1777649920 +commit: 2bba271688c1abb1503097a064e6c3bc1d1b6a9b ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/lychee/vendor.tar.zst /work/SRC/openSUSE:Factory/.lychee.new.30200/vendor.tar.zst differ: char 7, line 1
