Compare commits

..

1 commit

Author SHA1 Message Date
hugoreleaser
8f3d902ce5 releaser: Bump versions for release of 0.126.2
[ci skip]
2024-05-30 15:19:22 +00:00
2348 changed files with 47014 additions and 64829 deletions

View file

@ -4,7 +4,7 @@ parameters:
defaults: &defaults defaults: &defaults
resource_class: large resource_class: large
docker: docker:
- image: bepsays/ci-hugoreleaser:1.22400.20000 - image: bepsays/ci-hugoreleaser:1.22200.20201
environment: &buildenv environment: &buildenv
GOMODCACHE: /root/project/gomodcache GOMODCACHE: /root/project/gomodcache
version: 2 version: 2
@ -14,7 +14,9 @@ jobs:
environment: &buildenv environment: &buildenv
GOMODCACHE: /root/project/gomodcache GOMODCACHE: /root/project/gomodcache
steps: steps:
- setup_remote_docker - &remote-docker
setup_remote_docker:
version: 20.10.14
- checkout: - checkout:
path: hugo path: hugo
- &git-config - &git-config
@ -58,7 +60,7 @@ jobs:
environment: environment:
<<: [*buildenv] <<: [*buildenv]
docker: docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000 - image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20201
steps: steps:
- *restore-cache - *restore-cache
- &attach-workspace - &attach-workspace

View file

@ -1,49 +0,0 @@
name: Build Docker image
on:
release:
types: [published]
pull_request:
permissions:
packages: write
env:
REGISTRY_IMAGE: ghcr.io/gohugoio/hugo
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Docker meta
id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
- name: Login to GHCR
# Login is only needed when the image is pushed
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
with:
context: .
provenance: mode=max
sbom: true
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: HUGO_BUILD_TAGS=extended,withdeploy

View file

@ -12,7 +12,7 @@ jobs:
pull-requests: write pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@7de207be1d3ce97a9abe6ff1306222982d1ca9f9 # v5.0.1 - uses: dessant/lock-threads@08e671be8ac8944d0e132aa71d0ae8ccfb347675
with: with:
issue-inactive-days: 21 issue-inactive-days: 21
add-issue-labels: 'Outdated' add-issue-labels: 'Outdated'
@ -24,7 +24,7 @@ jobs:
This pull request has been automatically locked since there This pull request has been automatically locked since there
has not been any recent activity after it was closed. has not been any recent activity after it was closed.
Please open a new issue for related bugs. Please open a new issue for related bugs.
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 - uses: actions/stale@04a1828bc18ada028d85a0252a47cd2963a91abe
with: with:
operations-per-run: 999 operations-per-run: 999
days-before-issue-stale: 365 days-before-issue-stale: 365

View file

@ -6,23 +6,23 @@ name: Test
env: env:
GOPROXY: https://proxy.golang.org GOPROXY: https://proxy.golang.org
GO111MODULE: on GO111MODULE: on
SASS_VERSION: 1.80.3 SASS_VERSION: 1.63.2
DART_SASS_SHA_LINUX: 7c933edbad0a7d389192c5b79393485c088bd2c4398e32f5754c32af006a9ffd DART_SASS_SHA_LINUX: 3ea33c95ad5c35fda6e9a0956199eef38a398f496cfb8750e02479d7d1dd42af
DART_SASS_SHA_MACOS: 79e060b0e131c3bb3c16926bafc371dc33feab122bfa8c01aa337a072097967b DART_SASS_SHA_MACOS: 11c70f259836b250b44a9cb57fed70e030f21f45069b467d371685855f1eb4f0
DART_SASS_SHA_WINDOWS: 0bc4708b37cd1bac4740e83ac5e3176e66b774f77fd5dd364da5b5cfc9bfb469 DART_SASS_SHA_WINDOWS: cd8cd36a619dd8e27f93d3186c52d70eb7d69472aa6c85f5094b29693e773f64
permissions: permissions:
contents: read contents: read
jobs: jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.23.x, 1.24.x] go-version: [1.21.x, 1.22.x]
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues. os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- if: matrix.os == 'ubuntu-latest' - if: matrix.os == 'ubuntu-latest'
name: Free Disk Space (Ubuntu) name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with: with:
# this might remove tools that are actually needed, # this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB # if set to "true" but frees about 6 GB
@ -34,9 +34,9 @@ jobs:
docker-images: true docker-images: true
swap-storage: true swap-storage: true
- name: Checkout code - name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Install Go - name: Install Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
check-latest: true check-latest: true
@ -45,18 +45,18 @@ jobs:
**/go.sum **/go.sum
**/go.mod **/go.mod
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 uses: ruby/setup-ruby@5f19ec79cedfadb78ab837f95b87734d0003c899
with: with:
ruby-version: "2.7" ruby-version: "2.7"
bundler-cache: true # bundler-cache: true #
- name: Install Python - name: Install Python
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install Mage - name: Install Mage
run: go install github.com/magefile/mage@v1.15.0 run: go install github.com/magefile/mage@v1.15.0
- name: Install asciidoctor - name: Install asciidoctor
uses: reitzig/actions-asciidoctor@c642db5eedd1d729bb8c92034770d0b2f769eda6 # v2.0.2 uses: reitzig/actions-asciidoctor@03fcc74cd74880b697950c4930c9ec8a67c69ecc
- name: Install docutils - name: Install docutils
run: | run: |
pip install docutils pip install docutils
@ -112,17 +112,17 @@ jobs:
sass --version; sass --version;
mage -v check; mage -v check;
env: env:
HUGO_BUILD_TAGS: extended,withdeploy HUGO_BUILD_TAGS: extended
- if: matrix.os == 'windows-latest' - if: matrix.os == 'windows-latest'
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now. # See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
name: Test name: Test
run: | run: |
mage -v test; mage -v test;
env: env:
HUGO_BUILD_TAGS: extended,withdeploy HUGO_BUILD_TAGS: extended
- name: Build tags - name: Build tags
run: | run: |
go install -tags extended go install -tags extended,nodeploy
- if: matrix.os == 'ubuntu-latest' - if: matrix.os == 'ubuntu-latest'
name: Build for dragonfly name: Build for dragonfly
run: | run: |

3
.gitignore vendored
View file

@ -1,6 +1,3 @@
*.test *.test
imports.* imports.*
dist/
public/
.DS_Store

View file

@ -1,4 +1,4 @@
>**Note:** We would appreciate if you hold on with any big refactoring (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future. >**Note:** We would apprecitate if you hold on with any big refactorings (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future.
# Contributing to Hugo # Contributing to Hugo
@ -93,7 +93,6 @@ Most title/subjects should have a lower-cased prefix with a colon and one whites
* If this commit touches many packages without a common functional topic, prefix with `all:` (e.g. `all: Reformat Go code`) * If this commit touches many packages without a common functional topic, prefix with `all:` (e.g. `all: Reformat Go code`)
* If this is a documentation update, prefix with `docs:`. * If this is a documentation update, prefix with `docs:`.
* If nothing of the above applies, just leave the prefix out. * If nothing of the above applies, just leave the prefix out.
* Note that the above excludes nouns seen in other repositories, e.g. "chore:".
Also, if your commit references one or more GitHub issues, always end your commit message body with *See #1234* or *Fixes #1234*. Also, if your commit references one or more GitHub issues, always end your commit message body with *See #1234* or *Fixes #1234*.
Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*. Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*.

View file

@ -2,98 +2,44 @@
# Twitter: https://twitter.com/gohugoio # Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/ # Website: https://gohugo.io/
ARG GO_VERSION="1.24" FROM golang:1.21-alpine AS build
ARG ALPINE_VERSION="3.22"
ARG DART_SASS_VERSION="1.79.3"
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx # Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS gobuild # docker build --build-arg HUGO_BUILD_TAGS=extended .
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS gorun ARG HUGO_BUILD_TAGS
ARG CGO=1
FROM gobuild AS build ENV CGO_ENABLED=${CGO}
ENV GOOS=linux
RUN apk add clang lld ENV GO111MODULE=on
# Set up cross-compilation helpers
COPY --from=xx / /
ARG TARGETPLATFORM
RUN xx-apk add musl-dev gcc g++
# Optionally set HUGO_BUILD_TAGS to "none" or "withdeploy" when building like so:
# docker build --build-arg HUGO_BUILD_TAGS=withdeploy .
#
# We build the extended version by default.
ARG HUGO_BUILD_TAGS="extended"
ENV CGO_ENABLED=1
ENV GOPROXY=https://proxy.golang.org
ENV GOCACHE=/root/.cache/go-build
ENV GOMODCACHE=/go/pkg/mod
ARG TARGETPLATFORM
WORKDIR /go/src/github.com/gohugoio/hugo WORKDIR /go/src/github.com/gohugoio/hugo
# For --mount=type=cache the value of target is the default cache id, so COPY . /go/src/github.com/gohugoio/hugo/
# for the go mod cache it would be good if we could share it with other Go images using the same setup,
# but the go build cache needs to be per platform.
# See this comment: https://github.com/moby/buildkit/issues/1706#issuecomment-702238282
RUN --mount=target=. \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-$TARGETPLATFORM <<EOT
set -ex
xx-go build -tags "$HUGO_BUILD_TAGS" -ldflags "-s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=docker" -o /usr/bin/hugo
xx-verify /usr/bin/hugo
EOT
# dart-sass downloads the dart-sass runtime dependency # gcc/g++ are required to build SASS libraries for extended version
FROM alpine:${ALPINE_VERSION} AS dart-sass RUN apk update && \
ARG TARGETARCH apk add --no-cache gcc g++ musl-dev git && \
ARG DART_SASS_VERSION go install github.com/magefile/mage
ARG DART_ARCH=${TARGETARCH/amd64/x64}
WORKDIR /out
ADD https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-${DART_ARCH}.tar.gz .
RUN tar -xf dart-sass-${DART_SASS_VERSION}-linux-${DART_ARCH}.tar.gz
FROM gorun AS final RUN mage hugo && mage install
COPY --from=build /usr/bin/hugo /usr/bin/hugo # ---
# libc6-compat are required for extended libraries (libsass, libwebp). FROM alpine:3.18
RUN apk add --no-cache \
libc6-compat \
git \
runuser \
nodejs \
npm
RUN mkdir -p /var/hugo/bin /cache && \ COPY --from=build /go/bin/hugo /usr/bin/hugo
addgroup -Sg 1000 hugo && \
adduser -Sg hugo -u 1000 -h /var/hugo hugo && \
chown -R hugo: /var/hugo /cache && \
# For the Hugo's Git integration to work.
runuser -u hugo -- git config --global --add safe.directory /project && \
# See https://github.com/gohugoio/hugo/issues/9810
runuser -u hugo -- git config --global core.quotepath false
USER hugo:hugo # libc6-compat & libstdc++ are required for extended SASS libraries
VOLUME /project # ca-certificates are required to fetch outside resources (like Twitter oEmbeds)
WORKDIR /project RUN apk update && \
ENV HUGO_CACHEDIR=/cache apk add --no-cache ca-certificates libc6-compat libstdc++ git
ENV PATH="/var/hugo/bin:$PATH"
COPY scripts/docker/entrypoint.sh /entrypoint.sh VOLUME /site
COPY --from=dart-sass /out/dart-sass /var/hugo/bin/dart-sass WORKDIR /site
# Update PATH to reflect the new dependencies.
# For more complex setups, we should probably find a way to
# delegate this to the script itself, but this will have to do for now.
# Also, the dart-sass binary is a little special, other binaries can be put/linked
# directly in /var/hugo/bin.
ENV PATH="/var/hugo/bin/dart-sass:$PATH"
# Expose port for live server # Expose port for live server
EXPOSE 1313 EXPOSE 1313
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["hugo"]
CMD ["--help"] CMD ["--help"]

237
README.md
View file

@ -5,7 +5,6 @@
[documentation repository]: https://github.com/gohugoio/hugoDocs [documentation repository]: https://github.com/gohugoio/hugoDocs
[documentation]: https://gohugo.io/documentation [documentation]: https://gohugo.io/documentation
[dragonfly bsd, freebsd, netbsd, and openbsd]: https://gohugo.io/installation/bsd [dragonfly bsd, freebsd, netbsd, and openbsd]: https://gohugo.io/installation/bsd
[features]: https://gohugo.io/about/features/
[forum]: https://discourse.gohugo.io [forum]: https://discourse.gohugo.io
[friends]: https://github.com/gohugoio/hugo/graphs/contributors [friends]: https://github.com/gohugoio/hugo/graphs/contributors
[go]: https://go.dev/ [go]: https://go.dev/
@ -20,6 +19,7 @@
[static site generator]: https://en.wikipedia.org/wiki/Static_site_generator [static site generator]: https://en.wikipedia.org/wiki/Static_site_generator
[support]: https://discourse.gohugo.io [support]: https://discourse.gohugo.io
[themes]: https://themes.gohugo.io/ [themes]: https://themes.gohugo.io/
[twitter]: https://twitter.com/gohugoio
[website]: https://gohugo.io [website]: https://gohugo.io
[windows]: https://gohugo.io/installation/windows [windows]: https://gohugo.io/installation/windows
@ -52,42 +52,18 @@ Use Hugo's embedded web server during development to instantly see changes to co
Hugo's fast asset pipelines include: Hugo's fast asset pipelines include:
- Image processing &ndash; Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data - CSS bundling &ndash; transpilation (Sass), tree shaking, minification, source maps, SRI hashing, and PostCSS integration
- JavaScript bundling &ndash; Transpile TypeScript and JSX to JavaScript, bundle, tree shake, minify, create source maps, and perform SRI hashing. - JavaScript bundling &ndash; transpilation (TypeScript, JSX), tree shaking, minification, source maps, and SRI hashing
- Sass processing &ndash; Transpile Sass to CSS, bundle, tree shake, minify, create source maps, perform SRI hashing, and integrate with PostCSS - Image processing &ndash; convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
- Tailwind CSS processing &ndash; Compile Tailwind CSS utility classes into standard CSS, bundle, tree shake, optimize, minify, perform SRI hashing, and integrate with PostCSS
And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories. And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories.
See the [features] section of the documentation for a comprehensive summary of Hugo's capabilities.
## Sponsors ## Sponsors
<p>&nbsp;</p> <p>&nbsp;</p>
<p float="left"> <p float="left">
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a> <a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
&nbsp;&nbsp;&nbsp; <p>&nbsp;</p>
<a href="https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
&nbsp;&nbsp;&nbsp;
<a href="https://pinme.eth.limo/?s=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/logo-pinme.svg" width="200" alt="PinMe."></a>
</p>
## Editions
Hugo is available in three editions: standard, extended, and extended/deploy. While the standard edition provides core functionality, the extended and extended/deploy editions offer advanced features.
Feature|extended edition|extended/deploy edition
:--|:-:|:-:
Encode to the WebP format when [processing images]. You can decode WebP images with any edition.|:heavy_check_mark:|:heavy_check_mark:
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See&nbsp;[details].|:x:|:heavy_check_mark:
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
[processing images]: https://gohugo.io/content-management/image-processing/
[transpile sass to css]: https://gohugo.io/functions/css/sass/
[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
## Installation ## Installation
@ -100,11 +76,15 @@ Install Hugo from a [prebuilt binary], package manager, or package repository. P
## Build from source ## Build from source
Hugo is available in two editions: standard and extended. With the extended edition you can:
- Encode to the WebP format when processing images. You can decode WebP images with either edition.
- Transpile Sass to CSS using the embedded LibSass transpiler. The extended edition is not required to use the Dart Sass transpiler.
Prerequisites to build Hugo from source: Prerequisites to build Hugo from source:
- Standard edition: Go 1.23.0 or later - Standard edition: Go 1.20 or later
- Extended edition: Go 1.23.0 or later, and GCC - Extended edition: Go 1.20 or later, and GCC
- Extended/deploy edition: Go 1.23.0 or later, and GCC
Build the standard edition: Build the standard edition:
@ -118,16 +98,6 @@ Build the extended edition:
CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest
``` ```
Build the extended/deploy edition:
```text
CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest
```
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=gohugoio/hugo&type=Timeline)](https://star-history.com/#gohugoio/hugo&Timeline)
## Documentation ## Documentation
Hugo's [documentation] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples. Hugo's [documentation] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
@ -170,113 +140,152 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
<summary>See current dependencies</summary> <summary>See current dependencies</summary>
```text ```text
cloud.google.com/go/compute/metadata="v0.2.3"
cloud.google.com/go/iam="v1.1.3"
cloud.google.com/go/storage="v1.31.0"
cloud.google.com/go="v0.110.8"
github.com/Azure/azure-sdk-for-go/sdk/azcore="v1.7.0"
github.com/Azure/azure-sdk-for-go/sdk/azidentity="v1.3.0"
github.com/Azure/azure-sdk-for-go/sdk/internal="v1.3.0"
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob="v1.1.0"
github.com/Azure/go-autorest/autorest/to="v0.4.0"
github.com/AzureAD/microsoft-authentication-library-for-go="v1.0.0"
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69" github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
github.com/PuerkitoBio/goquery="v1.10.1" github.com/PuerkitoBio/purell="v1.1.1"
github.com/alecthomas/chroma/v2="v2.15.0" github.com/PuerkitoBio/urlesc="v0.0.0-20170810143723-de5bf2ad4578"
github.com/andybalholm/cascadia="v1.3.3" github.com/alecthomas/chroma/v2="v2.11.1"
github.com/armon/go-radix="v1.0.1-0.20221118154546-54df44f2176c" github.com/armon/go-radix="v1.0.0"
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream="v1.4.11"
github.com/aws/aws-sdk-go-v2/config="v1.18.32"
github.com/aws/aws-sdk-go-v2/credentials="v1.13.31"
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.13.7"
github.com/aws/aws-sdk-go-v2/feature/s3/manager="v1.11.76"
github.com/aws/aws-sdk-go-v2/internal/configsources="v1.1.37"
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2="v2.4.31"
github.com/aws/aws-sdk-go-v2/internal/ini="v1.3.38"
github.com/aws/aws-sdk-go-v2/internal/v4a="v1.1.0"
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding="v1.9.12"
github.com/aws/aws-sdk-go-v2/service/internal/checksum="v1.1.32"
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.9.31"
github.com/aws/aws-sdk-go-v2/service/internal/s3shared="v1.15.0"
github.com/aws/aws-sdk-go-v2/service/s3="v1.38.1"
github.com/aws/aws-sdk-go-v2/service/sso="v1.13.1"
github.com/aws/aws-sdk-go-v2/service/ssooidc="v1.15.1"
github.com/aws/aws-sdk-go-v2/service/sts="v1.21.1"
github.com/aws/aws-sdk-go-v2="v1.20.0"
github.com/aws/aws-sdk-go="v1.48.2"
github.com/aws/smithy-go="v1.14.0"
github.com/bep/clocks="v0.5.0" github.com/bep/clocks="v0.5.0"
github.com/bep/debounce="v1.2.0" github.com/bep/debounce="v1.2.0"
github.com/bep/gitmap="v1.6.0" github.com/bep/gitmap="v1.1.2"
github.com/bep/goat="v0.5.0" github.com/bep/goat="v0.5.0"
github.com/bep/godartsass/v2="v2.3.2" github.com/bep/godartsass/v2="v2.0.0"
github.com/bep/golibsass="v1.2.0" github.com/bep/godartsass="v1.2.0"
github.com/bep/golibsass="v1.1.1"
github.com/bep/gowebp="v0.3.0" github.com/bep/gowebp="v0.3.0"
github.com/bep/imagemeta="v0.8.4" github.com/bep/lazycache="v0.2.0"
github.com/bep/lazycache="v0.7.0" github.com/bep/logg="v0.3.0"
github.com/bep/logg="v0.4.0"
github.com/bep/mclib="v1.20400.20402" github.com/bep/mclib="v1.20400.20402"
github.com/bep/overlayfs="v0.9.2" github.com/bep/overlayfs="v0.6.0"
github.com/bep/simplecobra="v0.5.0" github.com/bep/simplecobra="v0.3.2"
github.com/bep/tmc="v0.5.1" github.com/bep/tmc="v0.5.1"
github.com/cespare/xxhash/v2="v2.3.0"
github.com/clbanning/mxj/v2="v2.7.0" github.com/clbanning/mxj/v2="v2.7.0"
github.com/cpuguy83/go-md2man/v2="v2.0.4" github.com/cli/safeexec="v1.0.1"
github.com/cpuguy83/go-md2man/v2="v2.0.2"
github.com/disintegration/gift="v1.2.1" github.com/disintegration/gift="v1.2.1"
github.com/dlclark/regexp2="v1.11.5" github.com/dlclark/regexp2="v1.10.0"
github.com/dop251/goja="v0.0.0-20250125213203-5ef83b82af17" github.com/dustin/go-humanize="v1.0.1"
github.com/evanw/esbuild="v0.24.2" github.com/evanw/esbuild="v0.19.7"
github.com/fatih/color="v1.18.0" github.com/fatih/color="v1.16.0"
github.com/frankban/quicktest="v1.14.6" github.com/frankban/quicktest="v1.14.6"
github.com/fsnotify/fsnotify="v1.8.0" github.com/fsnotify/fsnotify="v1.7.0"
github.com/getkin/kin-openapi="v0.129.0" github.com/getkin/kin-openapi="v0.120.0"
github.com/ghodss/yaml="v1.0.0" github.com/ghodss/yaml="v1.0.0"
github.com/go-openapi/jsonpointer="v0.21.0" github.com/go-openapi/jsonpointer="v0.19.6"
github.com/go-openapi/swag="v0.23.0" github.com/go-openapi/swag="v0.22.4"
github.com/go-sourcemap/sourcemap="v2.1.4+incompatible" github.com/gobuffalo/flect="v1.0.2"
github.com/gobuffalo/flect="v1.0.3"
github.com/gobwas/glob="v0.2.3" github.com/gobwas/glob="v0.2.3"
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e" github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
github.com/gohugoio/hashstructure="v0.5.0"
github.com/gohugoio/httpcache="v0.7.0"
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0"
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0"
github.com/gohugoio/locales="v0.14.0" github.com/gohugoio/locales="v0.14.0"
github.com/gohugoio/localescompressed="v1.0.1" github.com/gohugoio/localescompressed="v1.0.1"
github.com/golang/freetype="v0.0.0-20170609003504-e2365dfdc4a0" github.com/golang-jwt/jwt/v4="v4.5.0"
github.com/golang/groupcache="v0.0.0-20210331224755-41bb18bfe9da"
github.com/golang/protobuf="v1.5.3"
github.com/google/go-cmp="v0.6.0" github.com/google/go-cmp="v0.6.0"
github.com/google/pprof="v0.0.0-20250208200701-d0013a598941" github.com/google/s2a-go="v0.1.7"
github.com/gorilla/websocket="v1.5.3" github.com/google/uuid="v1.4.0"
github.com/hairyhenderson/go-codeowners="v0.7.0" github.com/google/wire="v0.5.0"
github.com/hashicorp/golang-lru/v2="v2.0.7" github.com/googleapis/enterprise-certificate-proxy="v0.3.2"
github.com/googleapis/gax-go/v2="v2.12.0"
github.com/gorilla/websocket="v1.5.1"
github.com/hairyhenderson/go-codeowners="v0.4.0"
github.com/hashicorp/golang-lru/v2="v2.0.1"
github.com/invopop/yaml="v0.2.0"
github.com/jdkato/prose="v1.2.1" github.com/jdkato/prose="v1.2.1"
github.com/jmespath/go-jmespath="v0.4.0"
github.com/josharian/intern="v1.0.0" github.com/josharian/intern="v1.0.0"
github.com/kr/pretty="v0.3.1" github.com/kr/pretty="v0.3.1"
github.com/kr/text="v0.2.0" github.com/kr/text="v0.2.0"
github.com/kyokomi/emoji/v2="v2.2.13" github.com/kylelemons/godebug="v1.1.0"
github.com/lucasb-eyer/go-colorful="v1.2.0" github.com/kyokomi/emoji/v2="v2.2.12"
github.com/mailru/easyjson="v0.7.7" github.com/mailru/easyjson="v0.7.7"
github.com/makeworld-the-better-one/dither/v2="v2.4.0"
github.com/marekm4/color-extractor="v1.2.1" github.com/marekm4/color-extractor="v1.2.1"
github.com/mattn/go-colorable="v0.1.13" github.com/mattn/go-colorable="v0.1.13"
github.com/mattn/go-isatty="v0.0.20" github.com/mattn/go-isatty="v0.0.20"
github.com/mattn/go-runewidth="v0.0.9" github.com/mattn/go-runewidth="v0.0.9"
github.com/mazznoer/csscolorparser="v0.1.5" github.com/mitchellh/hashstructure="v1.1.0"
github.com/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c" github.com/mitchellh/mapstructure="v1.5.0"
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826" github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
github.com/muesli/smartcrop="v0.3.0" github.com/muesli/smartcrop="v0.3.0"
github.com/niklasfasching/go-org="v1.7.0" github.com/niklasfasching/go-org="v1.7.0"
github.com/oasdiff/yaml3="v0.0.0-20241210130736-a94c01f36349"
github.com/oasdiff/yaml="v0.0.0-20241210131133-6b86fb107d80"
github.com/olekukonko/tablewriter="v0.0.5" github.com/olekukonko/tablewriter="v0.0.5"
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58" github.com/pelletier/go-toml/v2="v2.1.0"
github.com/pelletier/go-toml/v2="v2.2.3"
github.com/perimeterx/marshmallow="v1.1.5" github.com/perimeterx/marshmallow="v1.1.5"
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c" github.com/pkg/browser="v0.0.0-20210911075715-681adbf594b8"
github.com/pkg/errors="v0.9.1" github.com/pkg/errors="v0.9.1"
github.com/rivo/uniseg="v0.4.7" github.com/rogpeppe/go-internal="v1.11.0"
github.com/rogpeppe/go-internal="v1.13.1"
github.com/russross/blackfriday/v2="v2.1.0" github.com/russross/blackfriday/v2="v2.1.0"
github.com/sass/libsass="3.6.6" github.com/rwcarlsen/goexif="v0.0.0-20190401172101-9e8deecbddbd"
github.com/spf13/afero="v1.11.0" github.com/sanity-io/litter="v1.5.5"
github.com/spf13/cast="v1.7.1" github.com/sass/dart-sass/compiler="1.63.2"
github.com/spf13/cobra="v1.8.1" github.com/sass/dart-sass/implementation="1.63.2"
github.com/spf13/fsync="v0.10.1" github.com/sass/dart-sass/protocol="2.0.0"
github.com/spf13/pflag="v1.0.6" github.com/sass/libsass="3.6.5"
github.com/tdewolff/minify/v2="v2.20.37" github.com/spf13/afero="v1.10.0"
github.com/tdewolff/parse/v2="v2.7.15" github.com/spf13/cast="v1.5.1"
github.com/tetratelabs/wazero="v1.8.2" github.com/spf13/cobra="v1.7.0"
github.com/spf13/fsync="v0.9.0"
github.com/spf13/pflag="v1.0.5"
github.com/tdewolff/minify/v2="v2.20.7"
github.com/tdewolff/parse/v2="v2.7.5"
github.com/webmproject/libwebp="v1.3.2" github.com/webmproject/libwebp="v1.3.2"
github.com/yuin/goldmark-emoji="v1.0.4" github.com/yuin/goldmark-emoji="v1.0.2"
github.com/yuin/goldmark="v1.7.8" github.com/yuin/goldmark="v1.6.0"
go.opencensus.io="v0.24.0"
go.uber.org/atomic="v1.11.0"
go.uber.org/automaxprocs="v1.5.3" go.uber.org/automaxprocs="v1.5.3"
golang.org/x/crypto="v0.33.0" gocloud.dev="v0.34.0"
golang.org/x/exp="v0.0.0-20250210185358-939b2ce775ac" golang.org/x/crypto="v0.15.0"
golang.org/x/image="v0.24.0" golang.org/x/exp="v0.0.0-20221031165847-c99f073a8326"
golang.org/x/mod="v0.23.0" golang.org/x/image="v0.13.0"
golang.org/x/net="v0.35.0" golang.org/x/mod="v0.14.0"
golang.org/x/sync="v0.11.0" golang.org/x/net="v0.18.0"
golang.org/x/sys="v0.30.0" golang.org/x/oauth2="v0.13.0"
golang.org/x/text="v0.22.0" golang.org/x/sync="v0.5.0"
golang.org/x/tools="v0.30.0" golang.org/x/sys="v0.14.0"
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da" golang.org/x/text="v0.14.0"
gonum.org/v1/plot="v0.15.0" golang.org/x/time="v0.3.0"
google.golang.org/protobuf="v1.36.5" golang.org/x/tools="v0.15.0"
golang.org/x/xerrors="v0.0.0-20220907171357-04be3eba64a2"
google.golang.org/api="v0.151.0"
google.golang.org/genproto/googleapis/api="v0.0.0-20231016165738-49dd2c1f3d0b"
google.golang.org/genproto/googleapis/rpc="v0.0.0-20231030173426-d783a09b4405"
google.golang.org/genproto="v0.0.0-20231016165738-49dd2c1f3d0b"
google.golang.org/grpc="v1.59.0"
google.golang.org/protobuf="v1.31.0"
gopkg.in/yaml.v2="v2.4.0" gopkg.in/yaml.v2="v2.4.0"
gopkg.in/yaml.v3="v3.0.1" gopkg.in/yaml.v3="v3.0.1"
oss.terrastruct.com/d2="v0.6.9" howett.net/plist="v1.0.0"
oss.terrastruct.com/util-go="v0.0.0-20241005222610-44c011a04896"
rsc.io/qr="v0.2.0"
software.sslmate.com/src/go-pkcs12="v0.2.0" software.sslmate.com/src/go-pkcs12="v0.2.0"
``` ```
</details> </details>

37
bench.sh Executable file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# allow user to override go executable by running as GOEXE=xxx make ...
GOEXE="${GOEXE-go}"
# Convenience script to
# - For a given branch
# - Run benchmark tests for a given package
# - Do the same for master
# - then compare the two runs with benchcmp
benchFilter=".*"
if (( $# < 2 ));
then
echo "USAGE: ./bench.sh <git-branch> <package-to-bench> (and <benchmark filter> (regexp, optional))"
exit 1
fi
if [ $# -eq 3 ]; then
benchFilter=$3
fi
BRANCH=$1
PACKAGE=$2
git checkout $BRANCH
"${GOEXE}" test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-$BRANCH.txt
git checkout master
"${GOEXE}" test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-master.txt
benchcmp /tmp/bench-$PACKAGE-master.txt /tmp/bench-$PACKAGE-$BRANCH.txt

12
benchSite.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
# allow user to override go executable by running as GOEXE=xxx make ...
GOEXE="${GOEXE-go}"
# Send in a regexp matching the benchmarks you want to run, i.e. './benchSite.sh "YAML"'.
# Note the quotes, which will be needed for more complex expressions.
# The above will run all variations, but only for front matter YAML.
echo "Running with BenchmarkSiteBuilding/${1}"
"${GOEXE}" test -run="NONE" -bench="BenchmarkSiteBuilding/${1}" -test.benchmem=true ./hugolib -memprofile mem.prof -count 3 -cpuprofile cpu.prof

1
benchbep.sh Executable file
View file

@ -0,0 +1 @@
gobench -package=./hugolib -bench="BenchmarkSiteNew/Deep_content_tree"

1
bepdock.sh Executable file
View file

@ -0,0 +1 @@
docker run --rm --mount type=bind,source="$(pwd)",target=/hugo -w /hugo -i -t bepsays/ci-goreleaser:1.11-2 /bin/bash

View file

@ -38,11 +38,6 @@ import (
const minMaxSize = 10 const minMaxSize = 10
type KeyIdentity struct {
Key any
Identity identity.Identity
}
// New creates a new cache. // New creates a new cache.
func New(opts Options) *Cache { func New(opts Options) *Cache {
if opts.CheckInterval == 0 { if opts.CheckInterval == 0 {
@ -69,14 +64,14 @@ func New(opts Options) *Cache {
infol := opts.Log.InfoCommand("dynacache") infol := opts.Log.InfoCommand("dynacache")
evictedIdentities := collections.NewStack[KeyIdentity]() evictedIdentities := collections.NewStack[identity.Identity]()
onEvict := func(k, v any) { onEvict := func(k, v any) {
if !opts.Watching { if !opts.Watching {
return return
} }
identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool { identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool {
evictedIdentities.Push(KeyIdentity{Key: k, Identity: id}) evictedIdentities.Push(id)
return false return false
}) })
resource.MarkStale(v) resource.MarkStale(v)
@ -129,7 +124,7 @@ type Cache struct {
partitions map[string]PartitionManager partitions map[string]PartitionManager
onEvict func(k, v any) onEvict func(k, v any)
evictedIdentities *collections.Stack[KeyIdentity] evictedIdentities *collections.Stack[identity.Identity]
opts Options opts Options
infol logg.LevelLogger infol logg.LevelLogger
@ -140,15 +135,10 @@ type Cache struct {
} }
// DrainEvictedIdentities drains the evicted identities from the cache. // DrainEvictedIdentities drains the evicted identities from the cache.
func (c *Cache) DrainEvictedIdentities() []KeyIdentity { func (c *Cache) DrainEvictedIdentities() []identity.Identity {
return c.evictedIdentities.Drain() return c.evictedIdentities.Drain()
} }
// DrainEvictedIdentitiesMatching drains the evicted identities from the cache that match the given predicate.
func (c *Cache) DrainEvictedIdentitiesMatching(predicate func(KeyIdentity) bool) []KeyIdentity {
return c.evictedIdentities.DrainMatching(predicate)
}
// ClearMatching clears all partition for which the predicate returns true. // ClearMatching clears all partition for which the predicate returns true.
func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManager) bool, predicateValue func(k, v any) bool) { func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManager) bool, predicateValue func(k, v any) bool) {
if predicatePartition == nil { if predicatePartition == nil {
@ -176,12 +166,11 @@ func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManag
} }
// ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account. // ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account.
// predicate is optional and will clear any entry for which it returns true. func (c *Cache) ClearOnRebuild(changeset ...identity.Identity) {
func (c *Cache) ClearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) {
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{ g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
NumWorkers: len(c.partitions), NumWorkers: len(c.partitions),
Handle: func(ctx context.Context, partition PartitionManager) error { Handle: func(ctx context.Context, partition PartitionManager) error {
partition.clearOnRebuild(predicate, changeset...) partition.clearOnRebuild(changeset...)
return nil return nil
}, },
}) })
@ -431,25 +420,12 @@ func (p *Partition[K, V]) doGetOrCreateWitTimeout(key K, duration time.Duration,
errch := make(chan error, 1) errch := make(chan error, 1)
go func() { go func() {
var ( v, _, err := p.c.GetOrCreate(key, create)
v V
err error
)
defer func() {
if r := recover(); r != nil {
if rerr, ok := r.(error); ok {
err = rerr
} else {
err = fmt.Errorf("panic: %v", r)
}
}
if err != nil { if err != nil {
errch <- err errch <- err
} else { return
resultch <- v
} }
}() resultch <- v
v, _, err = p.c.GetOrCreate(key, create)
}() }()
select { select {
@ -480,12 +456,7 @@ func (p *Partition[K, V]) clearMatching(predicate func(k, v any) bool) {
}) })
} }
func (p *Partition[K, V]) clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) { func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
if predicate == nil {
predicate = func(k, v any) bool {
return false
}
}
opts := p.getOptions() opts := p.getOptions()
if opts.ClearWhen == ClearNever { if opts.ClearWhen == ClearNever {
return return
@ -531,7 +502,7 @@ func (p *Partition[K, V]) clearOnRebuild(predicate func(k, v any) bool, changese
// Second pass needs to be done in a separate loop to catch any // Second pass needs to be done in a separate loop to catch any
// elements marked as stale in the other partitions. // elements marked as stale in the other partitions.
p.c.DeleteFunc(func(key K, v V) bool { p.c.DeleteFunc(func(key K, v V) bool {
if predicate(key, v) || shouldDelete(key, v) { if shouldDelete(key, v) {
p.trace.Log( p.trace.Log(
logg.StringFunc( logg.StringFunc(
func() string { func() string {
@ -607,7 +578,7 @@ type PartitionManager interface {
adjustMaxSize(addend int) int adjustMaxSize(addend int) int
getMaxSize() int getMaxSize() int
getOptions() OptionsPartition getOptions() OptionsPartition
clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) clearOnRebuild(changeset ...identity.Identity)
clearMatching(predicate func(k, v any) bool) clearMatching(predicate func(k, v any) bool)
clearStale() clearStale()
} }

View file

@ -14,11 +14,8 @@
package dynacache package dynacache
import ( import (
"errors"
"fmt"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
@ -147,13 +144,13 @@ func TestClear(t *testing.T) {
c.Assert(cache.Keys(predicateAll), qt.HasLen, 4) c.Assert(cache.Keys(predicateAll), qt.HasLen, 4)
cache.ClearOnRebuild(nil) cache.ClearOnRebuild()
// Stale items are always cleared. // Stale items are always cleared.
c.Assert(cache.Keys(predicateAll), qt.HasLen, 2) c.Assert(cache.Keys(predicateAll), qt.HasLen, 2)
cache = newTestCache(t) cache = newTestCache(t)
cache.ClearOnRebuild(nil, identity.StringIdentity("changed")) cache.ClearOnRebuild(identity.StringIdentity("changed"))
c.Assert(cache.Keys(nil), qt.HasLen, 1) c.Assert(cache.Keys(nil), qt.HasLen, 1)
@ -168,58 +165,6 @@ func TestClear(t *testing.T) {
cache.adjustCurrentMaxSize() cache.adjustCurrentMaxSize()
} }
func TestPanicInCreate(t *testing.T) {
t.Parallel()
c := qt.New(t)
cache := newTestCache(t)
p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", OptionsPartition{Weight: 30, ClearWhen: ClearOnRebuild})
willPanic := func(i int) func() {
return func() {
p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) {
panic(errors.New(key))
})
}
}
// GetOrCreateWitTimeout needs to recover from panics in the create func.
willErr := func(i int) error {
_, err := p1.GetOrCreateWitTimeout(fmt.Sprintf("error-%d", i), 10*time.Second, func(key string) (testItem, error) {
return testItem{}, errors.New(key)
})
return err
}
for i := range 3 {
for range 3 {
c.Assert(willPanic(i), qt.PanicMatches, fmt.Sprintf("panic-%d", i))
c.Assert(willErr(i), qt.ErrorMatches, fmt.Sprintf("error-%d", i))
}
}
// Test the same keys again without the panic.
for i := range 3 {
for range 3 {
v, err := p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) {
return testItem{
name: key,
}, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v.name, qt.Equals, fmt.Sprintf("panic-%d", i))
v, err = p1.GetOrCreateWitTimeout(fmt.Sprintf("error-%d", i), 10*time.Second, func(key string) (testItem, error) {
return testItem{
name: key,
}, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v.name, qt.Equals, fmt.Sprintf("error-%d", i))
}
}
}
func TestAdjustCurrentMaxSize(t *testing.T) { func TestAdjustCurrentMaxSize(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved. // Copyright 2018 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -23,7 +23,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/gohugoio/httpcache"
"github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
@ -183,15 +182,6 @@ func (c *Cache) ReadOrCreate(id string,
return return
} }
// NamedLock locks the given id. The lock is released when the returned function is called.
func (c *Cache) NamedLock(id string) func() {
id = cleanID(id)
c.nlocker.Lock(id)
return func() {
c.nlocker.Unlock(id)
}
}
// GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will // GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will
// be invoked and the result cached. // be invoked and the result cached.
// This method is protected by a named lock using the given id as identifier. // This method is protected by a named lock using the given id as identifier.
@ -228,23 +218,7 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
var buff bytes.Buffer var buff bytes.Buffer
return info, return info,
hugio.ToReadCloser(&buff), hugio.ToReadCloser(&buff),
c.writeReader(id, io.TeeReader(r, &buff)) afero.WriteReader(c.Fs, id, io.TeeReader(r, &buff))
}
func (c *Cache) writeReader(id string, r io.Reader) error {
dir := filepath.Dir(id)
if dir != "" {
_ = c.Fs.MkdirAll(dir, 0o777)
}
f, err := c.Fs.Create(id)
if err != nil {
return err
}
defer f.Close()
_, _ = io.Copy(f, r)
return nil
} }
// GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice. // GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice.
@ -279,10 +253,9 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
return info, b, nil return info, b, nil
} }
if err := c.writeReader(id, bytes.NewReader(b)); err != nil { if err := afero.WriteReader(c.Fs, id, bytes.NewReader(b)); err != nil {
return info, nil, err return info, nil, err
} }
return info, b, nil return info, b, nil
} }
@ -332,10 +305,18 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
return nil return nil
} }
if removed, err := c.removeIfExpired(id); err != nil || removed { if c.maxAge > 0 {
fi, err := c.Fs.Stat(id)
if err != nil {
return nil return nil
} }
if c.isExpired(fi.ModTime()) {
c.Fs.Remove(id)
return nil
}
}
f, err := c.Fs.Open(id) f, err := c.Fs.Open(id)
if err != nil { if err != nil {
return nil return nil
@ -344,49 +325,6 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
return f return f
} }
func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
if c.maxAge == 0 {
// No caching.
return nil, false
}
f, err := c.Fs.Open(id)
if err != nil {
return nil, false
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil, false
}
removed, err := c.removeIfExpired(id)
if err != nil {
return nil, false
}
return b, removed
}
func (c *Cache) removeIfExpired(id string) (bool, error) {
if c.maxAge <= 0 {
return false, nil
}
fi, err := c.Fs.Stat(id)
if err != nil {
return false, err
}
if c.isExpired(fi.ModTime()) {
c.Fs.Remove(id)
return true, nil
}
return false, nil
}
func (c *Cache) isExpired(modTime time.Time) bool { func (c *Cache) isExpired(modTime time.Time) bool {
if c.maxAge < 0 { if c.maxAge < 0 {
return false return false
@ -460,37 +398,3 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) {
func cleanID(name string) string { func cleanID(name string) string {
return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator) return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator)
} }
// AsHTTPCache returns an httpcache.Cache implementation for this file cache.
// Note that none of the methods are protected by named locks, so you need to make sure
// to do that in your own code.
func (c *Cache) AsHTTPCache() httpcache.Cache {
return &httpCache{c: c}
}
type httpCache struct {
c *Cache
}
func (h *httpCache) Get(id string) (resp []byte, ok bool) {
id = cleanID(id)
b, removed := h.c.getBytesAndRemoveIfExpired(id)
return b, !removed
}
func (h *httpCache) Set(id string, resp []byte) {
if h.c.maxAge == 0 {
return
}
id = cleanID(id)
if err := h.c.writeReader(id, bytes.NewReader(resp)); err != nil {
panic(err)
}
}
func (h *httpCache) Delete(key string) {
h.c.Fs.Remove(key)
}

View file

@ -46,7 +46,6 @@ const (
CacheKeyAssets = "assets" CacheKeyAssets = "assets"
CacheKeyModules = "modules" CacheKeyModules = "modules"
CacheKeyGetResource = "getresource" CacheKeyGetResource = "getresource"
CacheKeyMisc = "misc"
) )
type Configs map[string]FileCacheConfig type Configs map[string]FileCacheConfig
@ -71,14 +70,10 @@ var defaultCacheConfigs = Configs{
MaxAge: -1, MaxAge: -1,
Dir: resourcesGenDir, Dir: resourcesGenDir,
}, },
CacheKeyGetResource: { CacheKeyGetResource: FileCacheConfig{
MaxAge: -1, // Never expire MaxAge: -1, // Never expire
Dir: cacheDirProject, Dir: cacheDirProject,
}, },
CacheKeyMisc: {
MaxAge: -1,
Dir: cacheDirProject,
},
} }
type FileCacheConfig struct { type FileCacheConfig struct {
@ -125,11 +120,6 @@ func (f Caches) AssetsCache() *Cache {
return f[CacheKeyAssets] return f[CacheKeyAssets]
} }
// MiscCache gets the file cache for miscellaneous stuff.
func (f Caches) MiscCache() *Cache {
return f[CacheKeyMisc]
}
// GetResourceCache gets the file cache for remote resources. // GetResourceCache gets the file cache for remote resources.
func (f Caches) GetResourceCache() *Cache { func (f Caches) GetResourceCache() *Cache {
return f[CacheKeyGetResource] return f[CacheKeyGetResource]

View file

@ -59,7 +59,7 @@ dir = "/path/to/c4"
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7) c.Assert(len(decoded), qt.Equals, 6)
c2 := decoded["getcsv"] c2 := decoded["getcsv"]
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s") c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
@ -106,7 +106,7 @@ dir = "/path/to/c4"
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7) c.Assert(len(decoded), qt.Equals, 6)
for _, v := range decoded { for _, v := range decoded {
c.Assert(v.MaxAge, qt.Equals, time.Duration(0)) c.Assert(v.MaxAge, qt.Equals, time.Duration(0))
@ -129,7 +129,7 @@ func TestDecodeConfigDefault(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7) c.Assert(len(decoded), qt.Equals, 6)
imgConfig := decoded[filecache.CacheKeyImages] imgConfig := decoded[filecache.CacheKeyImages]
jsonConfig := decoded[filecache.CacheKeyGetJSON] jsonConfig := decoded[filecache.CacheKeyGetJSON]

View file

@ -59,7 +59,7 @@ dir = ":resourceDir/_gen"
caches, err := filecache.NewCaches(p) caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
cache := caches[name] cache := caches[name]
for i := range 10 { for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i) id := fmt.Sprintf("i%d", i)
cache.GetOrCreateBytes(id, func() ([]byte, error) { cache.GetOrCreateBytes(id, func() ([]byte, error) {
return []byte("abc"), nil return []byte("abc"), nil
@ -74,7 +74,7 @@ dir = ":resourceDir/_gen"
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
c.Assert(count, qt.Equals, 5, msg) c.Assert(count, qt.Equals, 5, msg)
for i := range 10 { for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i) id := fmt.Sprintf("i%d", i)
v := cache.GetString(id) v := cache.GetString(id)
if i < 5 { if i < 5 {
@ -97,7 +97,7 @@ dir = ":resourceDir/_gen"
c.Assert(count, qt.Equals, 4) c.Assert(count, qt.Equals, 4)
// Now only the i5 should be left. // Now only the i5 should be left.
for i := range 10 { for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i) id := fmt.Sprintf("i%d", i)
v := cache.GetString(id) v := cache.GetString(id)
if i != 5 { if i != 5 {

View file

@ -105,7 +105,7 @@ dir = ":cacheDir/c"
} }
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} { for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
for range 2 { for i := 0; i < 2; i++ {
info, r, err := ca.GetOrCreate("a", rf("abc")) info, r, err := ca.GetOrCreate("a", rf("abc"))
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
c.Assert(r, qt.Not(qt.IsNil)) c.Assert(r, qt.Not(qt.IsNil))
@ -193,11 +193,11 @@ dir = "/cache/c"
var wg sync.WaitGroup var wg sync.WaitGroup
for i := range 50 { for i := 0; i < 50; i++ {
wg.Add(1) wg.Add(1)
go func(i int) { go func(i int) {
defer wg.Done() defer wg.Done()
for range 20 { for j := 0; j < 20; j++ {
ca := caches.Get(cacheName) ca := caches.Get(cacheName)
c.Assert(ca, qt.Not(qt.IsNil)) c.Assert(ca, qt.Not(qt.IsNil))
filename, data := filenameData(i) filename, data := filenameData(i)

View file

@ -1,229 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcache
import (
"encoding/json"
"time"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/predicate"
"github.com/gohugoio/hugo/config"
"github.com/mitchellh/mapstructure"
)
// DefaultConfig holds the default configuration for the HTTP cache.
var DefaultConfig = Config{
Cache: Cache{
For: GlobMatcher{
Excludes: []string{"**"},
},
},
Polls: []PollConfig{
{
For: GlobMatcher{
Includes: []string{"**"},
},
Disable: true,
},
},
}
// Config holds the configuration for the HTTP cache.
type Config struct {
// Configures the HTTP cache behavior (RFC 9111).
// When this is not enabled for a resource, Hugo will go straight to the file cache.
Cache Cache
// Polls holds a list of configurations for polling remote resources to detect changes in watch mode.
// This can be disabled for some resources, typically if they are known to not change.
Polls []PollConfig
}
type Cache struct {
// Enable HTTP cache behavior (RFC 9111) for these resources.
For GlobMatcher
}
func (c *Config) Compile() (ConfigCompiled, error) {
var cc ConfigCompiled
p, err := c.Cache.For.CompilePredicate()
if err != nil {
return cc, err
}
cc.For = p
for _, pc := range c.Polls {
p, err := pc.For.CompilePredicate()
if err != nil {
return cc, err
}
cc.PollConfigs = append(cc.PollConfigs, PollConfigCompiled{
For: p,
Config: pc,
})
}
return cc, nil
}
// PollConfig holds the configuration for polling remote resources to detect changes in watch mode.
type PollConfig struct {
// What remote resources to apply this configuration to.
For GlobMatcher
// Disable polling for this configuration.
Disable bool
// Low is the lower bound for the polling interval.
// This is the starting point when the resource has recently changed,
// if that resource stops changing, the polling interval will gradually increase towards High.
Low time.Duration
// High is the upper bound for the polling interval.
// This is the interval used when the resource is stable.
High time.Duration
}
func (c PollConfig) MarshalJSON() (b []byte, err error) {
// Marshal the durations as strings.
type Alias PollConfig
return json.Marshal(&struct {
Low string
High string
Alias
}{
Low: c.Low.String(),
High: c.High.String(),
Alias: (Alias)(c),
})
}
type GlobMatcher struct {
// Excludes holds a list of glob patterns that will be excluded.
Excludes []string
// Includes holds a list of glob patterns that will be included.
Includes []string
}
func (gm GlobMatcher) IsZero() bool {
return len(gm.Includes) == 0 && len(gm.Excludes) == 0
}
type ConfigCompiled struct {
For predicate.P[string]
PollConfigs []PollConfigCompiled
}
func (c *ConfigCompiled) PollConfigFor(s string) PollConfigCompiled {
for _, pc := range c.PollConfigs {
if pc.For(s) {
return pc
}
}
return PollConfigCompiled{}
}
func (c *ConfigCompiled) IsPollingDisabled() bool {
for _, pc := range c.PollConfigs {
if !pc.Config.Disable {
return false
}
}
return true
}
type PollConfigCompiled struct {
For predicate.P[string]
Config PollConfig
}
func (p PollConfigCompiled) IsZero() bool {
return p.For == nil
}
func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if gm.IsZero() {
panic("no includes or excludes")
}
var p predicate.P[string]
for _, include := range gm.Includes {
g, err := glob.Compile(include, '/')
if err != nil {
return nil, err
}
fn := func(s string) bool {
return g.Match(s)
}
p = p.Or(fn)
}
for _, exclude := range gm.Excludes {
g, err := glob.Compile(exclude, '/')
if err != nil {
return nil, err
}
fn := func(s string) bool {
return !g.Match(s)
}
p = p.And(fn)
}
return p, nil
}
func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
if len(m) == 0 {
return DefaultConfig, nil
}
var c Config
dc := &mapstructure.DecoderConfig{
Result: &c,
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
}
decoder, err := mapstructure.NewDecoder(dc)
if err != nil {
return c, err
}
if err := decoder.Decode(m); err != nil {
return c, err
}
if c.Cache.For.IsZero() {
c.Cache.For = DefaultConfig.Cache.For
}
for pci := range c.Polls {
if c.Polls[pci].For.IsZero() {
c.Polls[pci].For = DefaultConfig.Cache.For
c.Polls[pci].Disable = true
}
}
if len(c.Polls) == 0 {
c.Polls = DefaultConfig.Polls
}
return c, nil
}

View file

@ -1,95 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcache_test
import (
"testing"
"time"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
)
func TestConfigCustom(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
[httpcache]
[httpcache.cache.for]
includes = ["**gohugo.io**"]
[[httpcache.polls]]
low = "5s"
high = "32s"
[httpcache.polls.for]
includes = ["**gohugo.io**"]
`
b := hugolib.Test(t, files)
httpcacheConf := b.H.Configs.Base.HTTPCache
compiled := b.H.Configs.Base.C.HTTPCache
b.Assert(httpcacheConf.Cache.For.Includes, qt.DeepEquals, []string{"**gohugo.io**"})
b.Assert(httpcacheConf.Cache.For.Excludes, qt.IsNil)
pc := compiled.PollConfigFor("https://gohugo.io/foo.jpg")
b.Assert(pc.Config.Low, qt.Equals, 5*time.Second)
b.Assert(pc.Config.High, qt.Equals, 32*time.Second)
b.Assert(compiled.PollConfigFor("https://example.com/foo.jpg").IsZero(), qt.IsTrue)
}
func TestConfigDefault(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
`
b := hugolib.Test(t, files)
compiled := b.H.Configs.Base.C.HTTPCache
b.Assert(compiled.For("https://gohugo.io/posts.json"), qt.IsFalse)
b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse)
b.Assert(compiled.PollConfigFor("https://gohugo.io/foo.jpg").Config.Disable, qt.IsTrue)
}
func TestConfigPollsOnly(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
[httpcache]
[[httpcache.polls]]
low = "5s"
high = "32s"
[httpcache.polls.for]
includes = ["**gohugo.io**"]
`
b := hugolib.Test(t, files)
compiled := b.H.Configs.Base.C.HTTPCache
b.Assert(compiled.For("https://gohugo.io/posts.json"), qt.IsFalse)
b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse)
pc := compiled.PollConfigFor("https://gohugo.io/foo.jpg")
b.Assert(pc.Config.Low, qt.Equals, 5*time.Second)
b.Assert(pc.Config.High, qt.Equals, 32*time.Second)
b.Assert(compiled.PollConfigFor("https://example.com/foo.jpg").IsZero(), qt.IsTrue)
}

View file

@ -1,73 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpcache
import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
)
func TestGlobMatcher(t *testing.T) {
c := qt.New(t)
g := GlobMatcher{
Includes: []string{"**/*.jpg", "**.png", "**/bar/**"},
Excludes: []string{"**/foo.jpg", "**.css"},
}
p, err := g.CompilePredicate()
c.Assert(err, qt.IsNil)
c.Assert(p("foo.jpg"), qt.IsFalse)
c.Assert(p("foo.png"), qt.IsTrue)
c.Assert(p("foo/bar.jpg"), qt.IsTrue)
c.Assert(p("foo/bar.png"), qt.IsTrue)
c.Assert(p("foo/bar/foo.jpg"), qt.IsFalse)
c.Assert(p("foo/bar/foo.css"), qt.IsFalse)
c.Assert(p("foo.css"), qt.IsFalse)
c.Assert(p("foo/bar/foo.css"), qt.IsFalse)
c.Assert(p("foo/bar/foo.xml"), qt.IsTrue)
}
func TestDefaultConfig(t *testing.T) {
c := qt.New(t)
_, err := DefaultConfig.Compile()
c.Assert(err, qt.IsNil)
}
func TestDecodeConfigInjectsDefaultAndCompiles(t *testing.T) {
c := qt.New(t)
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]interface{}{})
c.Assert(err, qt.IsNil)
c.Assert(cfg, qt.DeepEquals, DefaultConfig)
_, err = cfg.Compile()
c.Assert(err, qt.IsNil)
cfg, err = DecodeConfig(config.BaseConfig{}, map[string]any{
"cache": map[string]any{
"polls": []map[string]any{
{"disable": true},
},
},
})
c.Assert(err, qt.IsNil)
_, err = cfg.Compile()
c.Assert(err, qt.IsNil)
}

View file

@ -26,7 +26,6 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"slices"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -103,7 +102,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
} }
for _, t := range include { for _, t := range include {
for i := range t.NumMethod() { for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i) m := t.Method(i)
if excludes[m.Name] || seen[m.Name] { if excludes[m.Name] || seen[m.Name] {
@ -123,7 +122,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
method := Method{Owner: t, OwnerName: ownerName, Name: m.Name} method := Method{Owner: t, OwnerName: ownerName, Name: m.Name}
for i := range numIn { for i := 0; i < numIn; i++ {
in := m.Type.In(i) in := m.Type.In(i)
name, pkg := nameAndPackage(in) name, pkg := nameAndPackage(in)
@ -138,7 +137,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
numOut := m.Type.NumOut() numOut := m.Type.NumOut()
if numOut > 0 { if numOut > 0 {
for i := range numOut { for i := 0; i < numOut; i++ {
out := m.Type.Out(i) out := m.Type.Out(i)
name, pkg := nameAndPackage(out) name, pkg := nameAndPackage(out)
@ -305,7 +304,7 @@ func (m Method) inOutStr() string {
} }
args := make([]string, len(m.In)) args := make([]string, len(m.In))
for i := range args { for i := 0; i < len(args); i++ {
args[i] = fmt.Sprintf("arg%d", i) args[i] = fmt.Sprintf("arg%d", i)
} }
return "(" + strings.Join(args, ", ") + ")" return "(" + strings.Join(args, ", ") + ")"
@ -317,7 +316,7 @@ func (m Method) inStr() string {
} }
args := make([]string, len(m.In)) args := make([]string, len(m.In))
for i := range args { for i := 0; i < len(args); i++ {
args[i] = fmt.Sprintf("arg%d %s", i, m.In[i]) args[i] = fmt.Sprintf("arg%d %s", i, m.In[i])
} }
return "(" + strings.Join(args, ", ") + ")" return "(" + strings.Join(args, ", ") + ")"
@ -340,7 +339,7 @@ func (m Method) outStrNamed() string {
} }
outs := make([]string, len(m.Out)) outs := make([]string, len(m.Out))
for i := range outs { for i := 0; i < len(outs); i++ {
outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i]) outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i])
} }
@ -436,7 +435,7 @@ func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (st
// Exclude self // Exclude self
for i, pkgImp := range pkgImports { for i, pkgImp := range pkgImports {
if pkgImp == pkgPath { if pkgImp == pkgPath {
pkgImports = slices.Delete(pkgImports, i, i+1) pkgImports = append(pkgImports[:i], pkgImports[i+1:]...)
} }
} }
} }

View file

@ -39,16 +39,15 @@ import (
"github.com/gohugoio/hugo/common/hstrings" "github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds" "github.com/gohugoio/hugo/resources/kinds"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -66,12 +65,6 @@ func Execute(args []string) error {
} }
args = mapLegacyArgs(args) args = mapLegacyArgs(args)
cd, err := x.Execute(context.Background(), args) cd, err := x.Execute(context.Background(), args)
if cd != nil {
if closer, ok := cd.Root.Command.(types.Closer); ok {
closer.Close()
}
}
if err != nil { if err != nil {
if err == errHelp { if err == errHelp {
cd.CobraCommand.Help() cd.CobraCommand.Help()
@ -94,17 +87,11 @@ type commonConfig struct {
fs *hugofs.Fs fs *hugofs.Fs
} }
type configKey struct {
counter int32
ignoreModulesDoesNotExists bool
}
// This is the root command. // This is the root command.
type rootCommand struct { type rootCommand struct {
Printf func(format string, v ...any) Printf func(format string, v ...interface{})
Println func(a ...any) Println func(a ...interface{})
StdOut io.Writer Out io.Writer
StdErr io.Writer
logger loggers.Logger logger loggers.Logger
@ -113,11 +100,8 @@ type rootCommand struct {
// Some, but not all commands need access to these. // Some, but not all commands need access to these.
// Some needs more than one, so keep them in a small cache. // Some needs more than one, so keep them in a small cache.
commonConfigs *lazycache.Cache[configKey, *commonConfig] commonConfigs *lazycache.Cache[int32, *commonConfig]
hugoSites *lazycache.Cache[configKey, *hugolib.HugoSites] hugoSites *lazycache.Cache[int32, *hugolib.HugoSites]
// changesFromBuild received from Hugo in watch mode.
changesFromBuild chan []identity.Identity
commands []simplecobra.Commander commands []simplecobra.Commander
@ -141,6 +125,8 @@ type rootCommand struct {
logLevel string logLevel string
verbose bool
debug bool
quiet bool quiet bool
devMode bool // Hidden flag. devMode bool // Hidden flag.
@ -154,18 +140,6 @@ func (r *rootCommand) isVerbose() bool {
return r.logger.Level() <= logg.LevelInfo return r.logger.Level() <= logg.LevelInfo
} }
func (r *rootCommand) Close() error {
if r.hugoSites != nil {
r.hugoSites.DeleteFunc(func(key configKey, value *hugolib.HugoSites) bool {
if value != nil {
value.Close()
}
return false
})
}
return nil
}
func (r *rootCommand) Build(cd *simplecobra.Commandeer, bcfg hugolib.BuildCfg, cfg config.Provider) (*hugolib.HugoSites, error) { func (r *rootCommand) Build(cd *simplecobra.Commandeer, bcfg hugolib.BuildCfg, cfg config.Provider) (*hugolib.HugoSites, error) {
h, err := r.Hugo(cfg) h, err := r.Hugo(cfg)
if err != nil { if err != nil {
@ -182,8 +156,8 @@ func (r *rootCommand) Commands() []simplecobra.Commander {
return r.commands return r.commands
} }
func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*commonConfig, error) { func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commonConfig, error) {
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) { cc, _, err := r.commonConfigs.GetOrCreate(key, func(key int32) (*commonConfig, error) {
fs := oldConf.fs fs := oldConf.fs
configs, err := allconfig.LoadConfig( configs, err := allconfig.LoadConfig(
allconfig.ConfigSourceDescriptor{ allconfig.ConfigSourceDescriptor{
@ -193,7 +167,6 @@ func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*c
ConfigDir: r.cfgDir, ConfigDir: r.cfgDir,
Logger: r.logger, Logger: r.logger,
Environment: r.environment, Environment: r.environment,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
}, },
) )
if err != nil { if err != nil {
@ -216,11 +189,11 @@ func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*c
return cc, err return cc, err
} }
func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*commonConfig, error) { func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commonConfig, error) {
if cfg == nil { if cfg == nil {
panic("cfg must be set") panic("cfg must be set")
} }
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) { cc, _, err := r.commonConfigs.GetOrCreate(key, func(key int32) (*commonConfig, error) {
var dir string var dir string
if r.source != "" { if r.source != "" {
dir, _ = filepath.Abs(r.source) dir, _ = filepath.Abs(r.source)
@ -249,7 +222,6 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
ConfigDir: r.cfgDir, ConfigDir: r.cfgDir,
Environment: r.environment, Environment: r.environment,
Logger: r.logger, Logger: r.logger,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
}, },
) )
if err != nil { if err != nil {
@ -331,35 +303,25 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
} }
func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) { func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
k := configKey{counter: r.configVersionID.Load()} h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) {
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) { depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, LogOut: r.logger.Out(), LogLevel: r.logger.Level()}
depsCfg := r.newDepsConfig(conf)
return hugolib.NewHugoSites(depsCfg) return hugolib.NewHugoSites(depsCfg)
}) })
return h, err return h, err
} }
func (r *rootCommand) Hugo(cfg config.Provider) (*hugolib.HugoSites, error) { func (r *rootCommand) Hugo(cfg config.Provider) (*hugolib.HugoSites, error) {
return r.getOrCreateHugo(cfg, false) h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) {
}
func (r *rootCommand) getOrCreateHugo(cfg config.Provider, ignoreModuleDoesNotExist bool) (*hugolib.HugoSites, error) {
k := configKey{counter: r.configVersionID.Load(), ignoreModulesDoesNotExists: ignoreModuleDoesNotExist}
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
conf, err := r.ConfigFromProvider(key, cfg) conf, err := r.ConfigFromProvider(key, cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
depsCfg := r.newDepsConfig(conf) depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, LogOut: r.logger.Out(), LogLevel: r.logger.Level()}
return hugolib.NewHugoSites(depsCfg) return hugolib.NewHugoSites(depsCfg)
}) })
return h, err return h, err
} }
func (r *rootCommand) newDepsConfig(conf *commonConfig) deps.DepsCfg {
return deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, StdOut: r.logger.StdOut(), StdErr: r.logger.StdErr(), LogLevel: r.logger.Level(), ChangesFromBuild: r.changesFromBuild}
}
func (r *rootCommand) Name() string { func (r *rootCommand) Name() string {
return "hugo" return "hugo"
} }
@ -422,23 +384,21 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
} }
func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error { func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
r.StdOut = os.Stdout r.Out = os.Stdout
r.StdErr = os.Stderr
if r.quiet { if r.quiet {
r.StdOut = io.Discard r.Out = io.Discard
r.StdErr = io.Discard
} }
// Used by mkcert (server). // Used by mkcert (server).
log.SetOutput(r.StdOut) log.SetOutput(r.Out)
r.Printf = func(format string, v ...any) { r.Printf = func(format string, v ...interface{}) {
if !r.quiet { if !r.quiet {
fmt.Fprintf(r.StdOut, format, v...) fmt.Fprintf(r.Out, format, v...)
} }
} }
r.Println = func(a ...any) { r.Println = func(a ...interface{}) {
if !r.quiet { if !r.quiet {
fmt.Fprintln(r.StdOut, a...) fmt.Fprintln(r.Out, a...)
} }
} }
_, running := runner.Command.(*serverCommand) _, running := runner.Command.(*serverCommand)
@ -447,16 +407,12 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
if err != nil { if err != nil {
return err return err
} }
// Set up the global logger early to allow info deprecations during config load.
loggers.SetGlobalLogger(r.logger)
r.changesFromBuild = make(chan []identity.Identity, 10) r.commonConfigs = lazycache.New(lazycache.Options[int32, *commonConfig]{MaxEntries: 5})
r.commonConfigs = lazycache.New(lazycache.Options[configKey, *commonConfig]{MaxEntries: 5})
// We don't want to keep stale HugoSites in memory longer than needed. // We don't want to keep stale HugoSites in memory longer than needed.
r.hugoSites = lazycache.New(lazycache.Options[configKey, *hugolib.HugoSites]{ r.hugoSites = lazycache.New(lazycache.Options[int32, *hugolib.HugoSites]{
MaxEntries: 1, MaxEntries: 1,
OnEvict: func(key configKey, value *hugolib.HugoSites) { OnEvict: func(key int32, value *hugolib.HugoSites) {
value.Close() value.Close()
runtime.GC() runtime.GC()
}, },
@ -484,21 +440,32 @@ func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
default: default:
return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel) return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel)
} }
} else {
if r.verbose {
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
level = logg.LevelInfo
}
if r.debug {
hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
level = logg.LevelDebug
}
} }
} }
optsLogger := loggers.Options{ optsLogger := loggers.Options{
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
Level: level, Level: level,
StdOut: r.StdOut, Stdout: r.Out,
StdErr: r.StdErr, Stderr: r.Out,
StoreErrors: running, StoreErrors: running,
} }
return loggers.New(optsLogger), nil return loggers.New(optsLogger), nil
} }
func (r *rootCommand) resetLogs() { func (r *rootCommand) Reset() {
r.logger.Reset() r.logger.Reset()
loggers.Log().Reset() loggers.Log().Reset()
} }
@ -509,26 +476,16 @@ func (r *rootCommand) IsTestRun() bool {
} }
func (r *rootCommand) Init(cd *simplecobra.Commandeer) error { func (r *rootCommand) Init(cd *simplecobra.Commandeer) error {
return r.initRootCommand("", cd)
}
func (r *rootCommand) initRootCommand(subCommandName string, cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand cmd := cd.CobraCommand
commandName := "hugo" cmd.Use = "hugo [flags]"
if subCommandName != "" { cmd.Short = "hugo builds your site"
commandName = subCommandName cmd.Long = `hugo is the main command, used to build your Hugo site.
}
cmd.Use = fmt.Sprintf("%s [flags]", commandName)
cmd.Short = "Build your site"
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo site.
Hugo is a Fast and Flexible Static Site Generator Hugo is a Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go. built with love by spf13 and friends in Go.
Complete documentation is available at https://gohugo.io/.` Complete documentation is available at https://gohugo.io/.`
cmd.Long = strings.ReplaceAll(cmd.Long, "COMMAND_NAME", commandName)
// Configure persistent flags // Configure persistent flags
cmd.PersistentFlags().StringVarP(&r.source, "source", "s", "", "filesystem path to read files relative from") cmd.PersistentFlags().StringVarP(&r.source, "source", "s", "", "filesystem path to read files relative from")
_ = cmd.MarkFlagDirname("source") _ = cmd.MarkFlagDirname("source")
@ -540,7 +497,6 @@ Complete documentation is available at https://gohugo.io/.`
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory") cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
_ = cmd.MarkFlagDirname("themesDir") _ = cmd.MarkFlagDirname("themesDir")
cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern") cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
cmd.PersistentFlags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
_ = cmd.RegisterFlagCompletionFunc("ignoreVendorPaths", cobra.NoFileCompletions) _ = cmd.RegisterFlagCompletionFunc("ignoreVendorPaths", cobra.NoFileCompletions)
cmd.PersistentFlags().String("clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00") cmd.PersistentFlags().String("clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00")
_ = cmd.RegisterFlagCompletionFunc("clock", cobra.NoFileCompletions) _ = cmd.RegisterFlagCompletionFunc("clock", cobra.NoFileCompletions)
@ -552,6 +508,8 @@ Complete documentation is available at https://gohugo.io/.`
cmd.PersistentFlags().BoolVar(&r.quiet, "quiet", false, "build in quiet mode") cmd.PersistentFlags().BoolVar(&r.quiet, "quiet", false, "build in quiet mode")
cmd.PersistentFlags().BoolVarP(&r.renderToMemory, "renderToMemory", "M", false, "render to memory (mostly useful when running the server)") cmd.PersistentFlags().BoolVarP(&r.renderToMemory, "renderToMemory", "M", false, "render to memory (mostly useful when running the server)")
cmd.PersistentFlags().BoolVarP(&r.verbose, "verbose", "v", false, "verbose output")
cmd.PersistentFlags().BoolVarP(&r.debug, "debug", "", false, "debug output")
cmd.PersistentFlags().BoolVarP(&r.devMode, "devMode", "", false, "only used for internal testing, flag hidden.") cmd.PersistentFlags().BoolVarP(&r.devMode, "devMode", "", false, "only used for internal testing, flag hidden.")
cmd.PersistentFlags().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)") cmd.PersistentFlags().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)")
_ = cmd.RegisterFlagCompletionFunc("logLevel", cobra.FixedCompletions([]string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp)) _ = cmd.RegisterFlagCompletionFunc("logLevel", cobra.FixedCompletions([]string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp))
@ -596,6 +554,7 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.") cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.")
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files") cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files") cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
cmd.Flags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations") cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations")
cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.") cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.") cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.")

View file

@ -14,8 +14,6 @@
package commands package commands
import ( import (
"context"
"github.com/bep/simplecobra" "github.com/bep/simplecobra"
) )
@ -23,7 +21,6 @@ import (
func newExec() (*simplecobra.Exec, error) { func newExec() (*simplecobra.Exec, error) {
rootCmd := &rootCommand{ rootCmd := &rootCommand{
commands: []simplecobra.Commander{ commands: []simplecobra.Commander{
newHugoBuildCmd(),
newVersionCmd(), newVersionCmd(),
newEnvCommand(), newEnvCommand(),
newServerCommand(), newServerCommand(),
@ -41,33 +38,3 @@ func newExec() (*simplecobra.Exec, error) {
return simplecobra.New(rootCmd) return simplecobra.New(rootCmd)
} }
func newHugoBuildCmd() simplecobra.Commander {
return &hugoBuildCommand{}
}
// hugoBuildCommand just delegates to the rootCommand.
type hugoBuildCommand struct {
rootCmd *rootCommand
}
func (c *hugoBuildCommand) Commands() []simplecobra.Commander {
return nil
}
func (c *hugoBuildCommand) Name() string {
return "build"
}
func (c *hugoBuildCommand) Init(cd *simplecobra.Commandeer) error {
c.rootCmd = cd.Root.Command.(*rootCommand)
return c.rootCmd.initRootCommand("build", cd)
}
func (c *hugoBuildCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
return c.rootCmd.PreRun(cd, runner)
}
func (c *hugoBuildCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return c.rootCmd.Run(ctx, cd, args)
}

View file

@ -45,7 +45,6 @@ type configCommand struct {
format string format string
lang string lang string
printZero bool
commands []simplecobra.Commander commands []simplecobra.Commander
} }
@ -59,7 +58,7 @@ func (c *configCommand) Name() string {
} }
func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil)) conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil { if err != nil {
return err return err
} }
@ -79,7 +78,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
dec.SetIndent("", " ") dec.SetIndent("", " ")
dec.SetEscapeHTML(false) dec.SetEscapeHTML(false)
if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: !c.printZero}); err != nil { if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: true}); err != nil {
return err return err
} }
@ -90,7 +89,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
os.Stdout.Write(buf.Bytes()) os.Stdout.Write(buf.Bytes())
default: default:
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format. // Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
var m map[string]any var m map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &m); err != nil { if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err return err
} }
@ -111,12 +110,11 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
func (c *configCommand) Init(cd *simplecobra.Commandeer) error { func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand) c.r = cd.Root.Command.(*rootCommand)
cmd := cd.CobraCommand cmd := cd.CobraCommand
cmd.Short = "Display site configuration" cmd.Short = "Print the site configuration"
cmd.Long = `Display site configuration, both default and custom settings.` cmd.Long = `Print the site configuration, both default and custom settings.`
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)") cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp)) _ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.") cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
cmd.Flags().BoolVar(&c.printZero, "printZero", false, `include config options with zero values (e.g. false, 0, "") in the output`)
_ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions) _ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions)
applyLocalFlagsBuildConfig(cmd, c.r) applyLocalFlagsBuildConfig(cmd, c.r)
@ -211,7 +209,7 @@ func (c *configMountsCommand) Name() string {
func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
r := c.configCmd.r r := c.configCmd.r
conf, err := r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil)) conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil { if err != nil {
return err return err
} }

View file

@ -105,8 +105,8 @@ func (c *convertCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, ar
func (c *convertCommand) Init(cd *simplecobra.Commandeer) error { func (c *convertCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand cmd := cd.CobraCommand
cmd.Short = "Convert front matter to another format" cmd.Short = "Convert your content to different formats"
cmd.Long = `Convert front matter to another format. cmd.Long = `Convert your content (e.g. front matter) to different formats.
See convert's subcommands toJSON, toTOML and toYAML for more information.` See convert's subcommands toJSON, toTOML and toYAML for more information.`

View file

@ -11,7 +11,21 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build withdeploy //go:build !nodeploy
// +build !nodeploy
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands package commands
@ -19,6 +33,7 @@ import (
"context" "context"
"github.com/gohugoio/hugo/deploy" "github.com/gohugoio/hugo/deploy"
"github.com/gohugoio/hugo/deploy/deployconfig"
"github.com/bep/simplecobra" "github.com/bep/simplecobra"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -27,8 +42,8 @@ import (
func newDeployCommand() simplecobra.Commander { func newDeployCommand() simplecobra.Commander {
return &simpleCommand{ return &simpleCommand{
name: "deploy", name: "deploy",
short: "Deploy your site to a cloud provider", short: "Deploy your site to a Cloud provider.",
long: `Deploy your site to a cloud provider long: `Deploy your site to a Cloud provider.
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
documentation. documentation.
@ -45,7 +60,17 @@ documentation.
return deployer.Deploy(ctx) return deployer.Deploy(ctx)
}, },
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
applyDeployFlags(cmd, r) cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
_ = cmd.RegisterFlagCompletionFunc("target", cobra.NoFileCompletions)
cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
cmd.Flags().Bool("dryRun", false, "dry run")
cmd.Flags().Bool("force", false, "force upload of all files")
cmd.Flags().Bool("invalidateCDN", deployconfig.DefaultConfig.InvalidateCDN, "invalidate the CDN cache listed in the deployment target")
cmd.Flags().Int("maxDeletes", deployconfig.DefaultConfig.MaxDeletes, "maximum # of files to delete, or -1 to disable")
_ = cmd.RegisterFlagCompletionFunc("maxDeletes", cobra.NoFileCompletions)
cmd.Flags().Int("workers", deployconfig.DefaultConfig.Workers, "number of workers to transfer files. defaults to 10")
_ = cmd.RegisterFlagCompletionFunc("workers", cobra.NoFileCompletions)
}, },
} }
} }

View file

@ -1,33 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"github.com/gohugoio/hugo/deploy/deployconfig"
"github.com/spf13/cobra"
)
func applyDeployFlags(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
_ = cmd.RegisterFlagCompletionFunc("target", cobra.NoFileCompletions)
cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
cmd.Flags().Bool("dryRun", false, "dry run")
cmd.Flags().Bool("force", false, "force upload of all files")
cmd.Flags().Bool("invalidateCDN", deployconfig.DefaultConfig.InvalidateCDN, "invalidate the CDN cache listed in the deployment target")
cmd.Flags().Int("maxDeletes", deployconfig.DefaultConfig.MaxDeletes, "maximum # of files to delete, or -1 to disable")
_ = cmd.RegisterFlagCompletionFunc("maxDeletes", cobra.NoFileCompletions)
cmd.Flags().Int("workers", deployconfig.DefaultConfig.Workers, "number of workers to transfer files. defaults to 10")
_ = cmd.RegisterFlagCompletionFunc("workers", cobra.NoFileCompletions)
}

View file

@ -11,7 +11,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build !withdeploy //go:build nodeploy
// +build nodeploy
// Copyright 2024 The Hugo Authors. All rights reserved. // Copyright 2024 The Hugo Authors. All rights reserved.
// //
@ -30,7 +31,6 @@ package commands
import ( import (
"context" "context"
"errors"
"github.com/bep/simplecobra" "github.com/bep/simplecobra"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -40,10 +40,9 @@ func newDeployCommand() simplecobra.Commander {
return &simpleCommand{ return &simpleCommand{
name: "deploy", name: "deploy",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return errors.New("deploy not supported in this version of Hugo; install a release with 'withdeploy' in the archive filename or build yourself with the 'withdeploy' build tag. Also see https://github.com/gohugoio/hugo/pull/12995") return nil
}, },
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
applyDeployFlags(cmd, r)
cmd.Hidden = true cmd.Hidden = true
}, },
} }

View file

@ -25,8 +25,8 @@ import (
func newEnvCommand() simplecobra.Commander { func newEnvCommand() simplecobra.Commander {
return &simpleCommand{ return &simpleCommand{
name: "env", name: "env",
short: "Display version and environment info", short: "Print Hugo version and environment info",
long: "Display version and environment info. This is useful in Hugo bug reports", long: "Print Hugo version and environment info. This is useful in Hugo bug reports",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Printf("%s\n", hugo.BuildVersionString()) r.Printf("%s\n", hugo.BuildVersionString())
r.Printf("GOOS=%q\n", runtime.GOOS) r.Printf("GOOS=%q\n", runtime.GOOS)
@ -61,8 +61,8 @@ func newVersionCmd() simplecobra.Commander {
r.Println(hugo.BuildVersionString()) r.Println(hugo.BuildVersionString())
return nil return nil
}, },
short: "Display version", short: "Print Hugo version and environment info",
long: "Display version and environment info. This is useful in Hugo bug reports.", long: "Print Hugo version and environment info. This is useful in Hugo bug reports.",
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.ValidArgsFunction = cobra.NoFileCompletions
}, },

View file

@ -21,7 +21,6 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2"
@ -50,7 +49,6 @@ func newGenCommand() *genCommand {
highlightStyle string highlightStyle string
lineNumbersInlineStyle string lineNumbersInlineStyle string
lineNumbersTableStyle string lineNumbersTableStyle string
omitEmpty bool
) )
newChromaStyles := func() simplecobra.Commander { newChromaStyles := func() simplecobra.Commander {
@ -62,10 +60,6 @@ func newGenCommand() *genCommand {
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`, See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
style = strings.ToLower(style)
if !slices.Contains(styles.Names(), style) {
return fmt.Errorf("invalid style: %s", style)
}
builder := styles.Get(style).Builder() builder := styles.Get(style).Builder()
if highlightStyle != "" { if highlightStyle != "" {
builder.Add(chroma.LineHighlight, highlightStyle) builder.Add(chroma.LineHighlight, highlightStyle)
@ -80,17 +74,8 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
if err != nil { if err != nil {
return err return err
} }
formatter := html.New(html.WithAllClasses(true))
var formatter *html.Formatter formatter.WriteCSS(os.Stdout, style)
if omitEmpty {
formatter = html.New(html.WithClasses(true))
} else {
formatter = html.New(html.WithAllClasses(true))
}
w := os.Stdout
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
formatter.WriteCSS(w, style)
return nil return nil
}, },
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
@ -103,8 +88,6 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
_ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions) _ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`) cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions) _ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules`)
_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
}, },
} }
} }
@ -159,7 +142,7 @@ url: %s
return &simpleCommand{ return &simpleCommand{
name: "doc", name: "doc",
short: "Generate Markdown documentation for the Hugo CLI", short: "Generate Markdown documentation for the Hugo CLI.",
long: `Generate Markdown documentation for the Hugo CLI. long: `Generate Markdown documentation for the Hugo CLI.
This command is, mostly, used to create up-to-date documentation This command is, mostly, used to create up-to-date documentation
of Hugo's command-line interface for https://gohugo.io/. of Hugo's command-line interface for https://gohugo.io/.
@ -184,13 +167,13 @@ url: %s
prepender := func(filename string) string { prepender := func(filename string) string {
name := filepath.Base(filename) name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name)) base := strings.TrimSuffix(name, path.Ext(name))
url := "/docs/reference/commands/" + strings.ToLower(base) + "/" url := "/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url) return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
} }
linkHandler := func(name string) string { linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name)) base := strings.TrimSuffix(name, path.Ext(name))
return "/docs/reference/commands/" + strings.ToLower(base) + "/" return "/commands/" + strings.ToLower(base) + "/"
} }
r.Println("Generating Hugo command-line documentation in", gendocdir, "...") r.Println("Generating Hugo command-line documentation in", gendocdir, "...")
doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler) doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler)
@ -211,7 +194,7 @@ url: %s
newDocsHelper := func() simplecobra.Commander { newDocsHelper := func() simplecobra.Commander {
return &simpleCommand{ return &simpleCommand{
name: "docshelper", name: "docshelper",
short: "Generate some data files for the Hugo docs", short: "Generate some data files for the Hugo docs.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Println("Generate docs data to", docsHelperTarget) r.Println("Generate docs data to", docsHelperTarget)
@ -232,7 +215,7 @@ url: %s
} }
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format. // Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
var m map[string]any var m map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &m); err != nil { if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err return err
} }
@ -290,8 +273,7 @@ func (c *genCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args [
func (c *genCommand) Init(cd *simplecobra.Commandeer) error { func (c *genCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand cmd := cd.CobraCommand
cmd.Short = "Generate documentation and syntax highlighting styles" cmd.Short = "A collection of several useful generators."
cmd.Long = "Generate documentation for your project using Hugo's documentation engine, including syntax highlighting for various programming languages."
cmd.RunE = nil cmd.RunE = nil
return nil return nil

View file

@ -27,6 +27,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/bep/logg"
"github.com/bep/simplecobra" "github.com/bep/simplecobra"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
@ -42,7 +43,6 @@ import (
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems" "github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/livereload" "github.com/gohugoio/hugo/livereload"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/watcher" "github.com/gohugoio/hugo/watcher"
@ -62,7 +62,7 @@ type hugoBuilder struct {
// Currently only set when in "fast render mode". // Currently only set when in "fast render mode".
changeDetector *fileChangeDetector changeDetector *fileChangeDetector
visitedURLs *types.EvictingQueue[string] visitedURLs *types.EvictingStringQueue
fullRebuildSem *semaphore.Weighted fullRebuildSem *semaphore.Weighted
debounce func(f func()) debounce func(f func())
@ -135,6 +135,10 @@ func (e *hugoBuilderErrState) wasErr() bool {
return e.waserr return e.waserr
} }
func (c *hugoBuilder) errCount() int {
return c.r.logger.LoggCount(logg.LevelError) + loggers.Log().LoggCount(logg.LevelError)
}
// getDirList provides NewWatcher() with a list of directories to watch for changes. // getDirList provides NewWatcher() with a list of directories to watch for changes.
func (c *hugoBuilder) getDirList() ([]string, error) { func (c *hugoBuilder) getDirList() ([]string, error) {
h, err := c.hugo() h, err := c.hugo()
@ -339,26 +343,6 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa
go func() { go func() {
for { for {
select { select {
case changes := <-c.r.changesFromBuild:
unlock, err := h.LockBuild()
if err != nil {
c.r.logger.Errorln("Failed to acquire a build lock: %s", err)
return
}
c.changeDetector.PrepareNew()
err = c.rebuildSitesForChanges(changes)
if err != nil {
c.r.logger.Errorln("Error while watching:", err)
}
if c.s != nil && c.s.doLiveReload {
doReload := c.changeDetector == nil || len(c.changeDetector.changed()) > 0
doReload = doReload || c.showErrorInBrowser && c.errState.buildErr() != nil
if doReload {
livereload.ForceRefresh()
}
}
unlock()
case evs := <-watcher.Events: case evs := <-watcher.Events:
unlock, err := h.LockBuild() unlock, err := h.LockBuild()
if err != nil { if err != nil {
@ -366,7 +350,7 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa
return return
} }
c.handleEvents(watcher, staticSyncer, evs, configSet) c.handleEvents(watcher, staticSyncer, evs, configSet)
if c.showErrorInBrowser && c.errState.buildErr() != nil { if c.showErrorInBrowser && c.errCount() > 0 {
// Need to reload browser to show the error // Need to reload browser to show the error
livereload.ForceRefresh() livereload.ForceRefresh()
} }
@ -413,17 +397,11 @@ func (c *hugoBuilder) build() error {
} }
func (c *hugoBuilder) buildSites(noBuildLock bool) (err error) { func (c *hugoBuilder) buildSites(noBuildLock bool) (err error) {
defer func() { h, err := c.hugo()
c.errState.setBuildErr(err)
}()
var h *hugolib.HugoSites
h, err = c.hugo()
if err != nil { if err != nil {
return return err
} }
err = h.Build(hugolib.BuildCfg{NoBuildLock: noBuildLock}) return h.Build(hugolib.BuildCfg{NoBuildLock: noBuildLock})
return
} }
func (c *hugoBuilder) copyStatic() (map[string]uint64, error) { func (c *hugoBuilder) copyStatic() (map[string]uint64, error) {
@ -619,9 +597,6 @@ func (c *hugoBuilder) fullRebuild(changeType string) {
// Set the processing on pause until the state is recovered. // Set the processing on pause until the state is recovered.
c.errState.setPaused(true) c.errState.setPaused(true)
c.handleBuildErr(err, "Failed to reload config") c.handleBuildErr(err, "Failed to reload config")
if c.s.doLiveReload {
livereload.ForceRefresh()
}
} else { } else {
c.errState.setPaused(false) c.errState.setPaused(false)
} }
@ -663,20 +638,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
var n int var n int
for _, ev := range evs { for _, ev := range evs {
keep := true keep := true
// Write and rename operations are often followed by CHMOD. if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
// There may be valid use cases for rebuilding the site on CHMOD,
// but that will require more complex logic than this simple conditional.
// On OS X this seems to be related to Spotlight, see:
// https://github.com/go-fsnotify/fsnotify/issues/15
// A workaround is to put your site(s) on the Spotlight exception list,
// but that may be a little mysterious for most end users.
// So, for now, we skip reload on CHMOD.
// We do have to check for WRITE though. On slower laptops a Chmod
// could be aggregated with other important events, and we still want
// to rebuild on those
if ev.Op == fsnotify.Chmod {
keep = false
} else if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
if _, err := os.Stat(ev.Name); err != nil { if _, err := os.Stat(ev.Name); err != nil {
keep = false keep = false
} }
@ -760,20 +722,6 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
staticEvents := []fsnotify.Event{} staticEvents := []fsnotify.Event{}
dynamicEvents := []fsnotify.Event{} dynamicEvents := []fsnotify.Event{}
filterDuplicateEvents := func(evs []fsnotify.Event) []fsnotify.Event {
seen := make(map[string]bool)
var n int
for _, ev := range evs {
if seen[ev.Name] {
continue
}
seen[ev.Name] = true
evs[n] = ev
n++
}
return evs[:n]
}
h, err := c.hugo() h, err := c.hugo()
if err != nil { if err != nil {
c.r.logger.Errorln("Error getting the Hugo object:", err) c.r.logger.Errorln("Error getting the Hugo object:", err)
@ -795,7 +743,6 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
istemp := strings.HasSuffix(ext, "~") || istemp := strings.HasSuffix(ext, "~") ||
(ext == ".swp") || // vim (ext == ".swp") || // vim
(ext == ".swx") || // vim (ext == ".swx") || // vim
(ext == ".bck") || // helix
(ext == ".tmp") || // generic temp file (ext == ".tmp") || // generic temp file
(ext == ".DS_Store") || // OSX Thumbnail (ext == ".DS_Store") || // OSX Thumbnail
baseName == "4913" || // vim baseName == "4913" || // vim
@ -818,6 +765,21 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
continue continue
} }
// Write and rename operations are often followed by CHMOD.
// There may be valid use cases for rebuilding the site on CHMOD,
// but that will require more complex logic than this simple conditional.
// On OS X this seems to be related to Spotlight, see:
// https://github.com/go-fsnotify/fsnotify/issues/15
// A workaround is to put your site(s) on the Spotlight exception list,
// but that may be a little mysterious for most end users.
// So, for now, we skip reload on CHMOD.
// We do have to check for WRITE though. On slower laptops a Chmod
// could be aggregated with other important events, and we still want
// to rebuild on those
if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
continue
}
walkAdder := func(path string, f hugofs.FileMetaInfo) error { walkAdder := func(path string, f hugofs.FileMetaInfo) error {
if f.IsDir() { if f.IsDir() {
c.r.logger.Println("adding created directory to watchlist", path) c.r.logger.Println("adding created directory to watchlist", path)
@ -849,11 +811,6 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
} }
} }
lrl := c.r.logger.InfoCommand("livereload")
staticEvents = filterDuplicateEvents(staticEvents)
dynamicEvents = filterDuplicateEvents(dynamicEvents)
if len(staticEvents) > 0 { if len(staticEvents) > 0 {
c.printChangeDetected("Static files") c.printChangeDetected("Static files")
@ -874,20 +831,19 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
if c.s != nil && c.s.doLiveReload { if c.s != nil && c.s.doLiveReload {
// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
// force refresh when more than one file
if !c.errState.wasErr() && len(staticEvents) == 1 { if !c.errState.wasErr() && len(staticEvents) == 1 {
ev := staticEvents[0]
h, err := c.hugo() h, err := c.hugo()
if err != nil { if err != nil {
c.r.logger.Errorln("Error getting the Hugo object:", err) c.r.logger.Errorln("Error getting the Hugo object:", err)
return return
} }
path := h.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
path := h.BaseFs.SourceFilesystems.MakeStaticPathRelative(staticEvents[0].Name)
path = h.RelURL(paths.ToSlashTrimLeading(path), false) path = h.RelURL(paths.ToSlashTrimLeading(path), false)
lrl.Logf("refreshing static file %q", path)
livereload.RefreshPath(path) livereload.RefreshPath(path)
} else { } else {
lrl.Logf("got %d static file change events, force refresh", len(staticEvents))
livereload.ForceRefresh() livereload.ForceRefresh()
} }
} }
@ -911,34 +867,20 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
}() }()
if c.s != nil && c.s.doLiveReload { if c.s != nil && c.s.doLiveReload {
if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
if c.errState.wasErr() { if c.errState.wasErr() {
livereload.ForceRefresh() livereload.ForceRefresh()
return return
} }
changed := c.changeDetector.changed() changed := c.changeDetector.changed()
if c.changeDetector != nil { if c.changeDetector != nil && len(changed) == 0 {
if len(changed) >= 10 {
lrl.Logf("build changed %d files", len(changed))
} else {
lrl.Logf("build changed %d files: %q", len(changed), changed)
}
if len(changed) == 0 {
// Nothing has changed. // Nothing has changed.
return return
} } else if len(changed) == 1 {
} pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(changed[0]), false)
livereload.RefreshPath(pathToRefresh)
// If this change set also contains one or more CSS files, we need to
// refresh these as well.
var cssChanges []string
var otherChanges []string
for _, ev := range changed {
if strings.HasSuffix(ev, ".css") {
cssChanges = append(cssChanges, ev)
} else { } else {
otherChanges = append(otherChanges, ev) livereload.ForceRefresh()
} }
} }
@ -954,39 +896,11 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
} }
} }
if p != nil && p.RelPermalink() != "" { if p != nil {
link, port := p.RelPermalink(), p.Site().ServerPort() livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort())
lrl.Logf("navigating to %q using port %d", link, port)
livereload.NavigateToPathForPort(link, port)
} else { } else {
lrl.Logf("no page to navigate to, force refresh")
livereload.ForceRefresh() livereload.ForceRefresh()
} }
} else if len(otherChanges) > 0 {
if len(otherChanges) == 1 {
// Allow single changes to be refreshed without a full page reload.
pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(otherChanges[0]), false)
lrl.Logf("refreshing %q", pathToRefresh)
livereload.RefreshPath(pathToRefresh)
} else if len(cssChanges) == 0 || len(otherChanges) > 1 {
lrl.Logf("force refresh")
livereload.ForceRefresh()
}
} else {
lrl.Logf("force refresh")
livereload.ForceRefresh()
}
if len(cssChanges) > 0 {
// Allow some time for the live reload script to get reconnected.
if len(otherChanges) > 0 {
time.Sleep(200 * time.Millisecond)
}
for _, ev := range cssChanges {
pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(ev), false)
lrl.Logf("refreshing CSS %q", pathToRefresh)
livereload.RefreshPath(pathToRefresh)
}
} }
} }
} }
@ -1055,7 +969,7 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
"fastRenderMode": c.fastRenderMode, "fastRenderMode": c.fastRenderMode,
}) })
conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, cfg)) conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, cfg))
if err != nil { if err != nil {
return err return err
} }
@ -1089,49 +1003,29 @@ func (c *hugoBuilder) printChangeDetected(typ string) {
c.r.logger.Println(htime.Now().Format(layout)) c.r.logger.Println(htime.Now().Format(layout))
} }
func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) (err error) { func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) error {
defer func() {
c.errState.setBuildErr(err)
}()
if err := c.errState.buildErr(); err != nil { if err := c.errState.buildErr(); err != nil {
ferrs := herrors.UnwrapFileErrorsWithErrorContext(err) ferrs := herrors.UnwrapFileErrorsWithErrorContext(err)
for _, err := range ferrs { for _, err := range ferrs {
events = append(events, fsnotify.Event{Name: err.Position().Filename, Op: fsnotify.Write}) events = append(events, fsnotify.Event{Name: err.Position().Filename, Op: fsnotify.Write})
} }
} }
var h *hugolib.HugoSites c.errState.setBuildErr(nil)
h, err = c.hugo() h, err := c.hugo()
if err != nil { if err != nil {
return return err
}
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...)
return
} }
func (c *hugoBuilder) rebuildSitesForChanges(ids []identity.Identity) (err error) { return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...)
defer func() {
c.errState.setBuildErr(err)
}()
var h *hugolib.HugoSites
h, err = c.hugo()
if err != nil {
return
}
whatChanged := &hugolib.WhatChanged{}
whatChanged.Add(ids...)
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, WhatChanged: whatChanged, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()})
return
} }
func (c *hugoBuilder) reloadConfig() error { func (c *hugoBuilder) reloadConfig() error {
c.r.resetLogs() c.r.Reset()
c.r.configVersionID.Add(1) c.r.configVersionID.Add(1)
if err := c.withConfE(func(conf *commonConfig) error { if err := c.withConfE(func(conf *commonConfig) error {
oldConf := conf oldConf := conf
newConf, err := c.r.ConfigFromConfig(configKey{counter: c.r.configVersionID.Load()}, conf) newConf, err := c.r.ConfigFromConfig(c.r.configVersionID.Load(), conf)
if err != nil { if err != nil {
return err return err
} }

View file

@ -90,8 +90,8 @@ func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
func (c *importCommand) Init(cd *simplecobra.Commandeer) error { func (c *importCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand cmd := cd.CobraCommand
cmd.Short = "Import a site from another system" cmd.Short = "Import your site from others."
cmd.Long = `Import a site from another system. cmd.Long = `Import your site from other web site generators like Jekyll.
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`." Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."

View file

@ -57,7 +57,7 @@ func newListCommand() *listCommand {
return err return err
} }
writer := csv.NewWriter(r.StdOut) writer := csv.NewWriter(r.Out)
defer writer.Flush() defer writer.Flush()
writer.Write([]string{ writer.Write([]string{
@ -199,8 +199,8 @@ func (c *listCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
func (c *listCommand) Init(cd *simplecobra.Commandeer) error { func (c *listCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand cmd := cd.CobraCommand
cmd.Short = "List content" cmd.Short = "Listing out various types of content"
cmd.Long = `List content. cmd.Long = `Listing out various types of content.
List requires a subcommand, e.g. hugo list drafts` List requires a subcommand, e.g. hugo list drafts`

View file

@ -44,12 +44,12 @@ func newModCommands() *modCommands {
npmCommand := &simpleCommand{ npmCommand := &simpleCommand{
name: "npm", name: "npm",
short: "Various npm helpers", short: "Various npm helpers.",
long: `Various npm (Node package manager) helpers.`, long: `Various npm (Node package manager) helpers.`,
commands: []simplecobra.Commander{ commands: []simplecobra.Commander{
&simpleCommand{ &simpleCommand{
name: "pack", name: "pack",
short: "Experimental: Prepares and writes a composite package.json file for your project", short: "Experimental: Prepares and writes a composite package.json file for your project.",
long: `Prepares and writes a composite package.json file for your project. long: `Prepares and writes a composite package.json file for your project.
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
@ -80,7 +80,7 @@ so this may/will change in future versions of Hugo.
commands: []simplecobra.Commander{ commands: []simplecobra.Commander{
&simpleCommand{ &simpleCommand{
name: "init", name: "init",
short: "Initialize this project as a Hugo Module", short: "Initialize this project as a Hugo Module.",
long: `Initialize this project as a Hugo Module. long: `Initialize this project as a Hugo Module.
It will try to guess the module path, but you may help by passing it as an argument, e.g: It will try to guess the module path, but you may help by passing it as an argument, e.g:
@ -94,7 +94,7 @@ so this may/will change in future versions of Hugo.
applyLocalFlagsBuildConfig(cmd, r) applyLocalFlagsBuildConfig(cmd, r)
}, },
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.getOrCreateHugo(flagsToCfg(cd, nil), true) h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil { if err != nil {
return err return err
} }
@ -102,16 +102,12 @@ so this may/will change in future versions of Hugo.
if len(args) >= 1 { if len(args) >= 1 {
initPath = args[0] initPath = args[0]
} }
c := h.Configs.ModulesClient return h.Configs.ModulesClient.Init(initPath)
if err := c.Init(initPath); err != nil {
return err
}
return nil
}, },
}, },
&simpleCommand{ &simpleCommand{
name: "verify", name: "verify",
short: "Verify dependencies", short: "Verify dependencies.",
long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`, long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`,
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.ValidArgsFunction = cobra.NoFileCompletions
@ -119,7 +115,7 @@ so this may/will change in future versions of Hugo.
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification") cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
}, },
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil)) conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil { if err != nil {
return err return err
} }
@ -129,7 +125,7 @@ so this may/will change in future versions of Hugo.
}, },
&simpleCommand{ &simpleCommand{
name: "graph", name: "graph",
short: "Print a module dependency graph", short: "Print a module dependency graph.",
long: `Print a module dependency graph with information about module status (disabled, vendored). long: `Print a module dependency graph with information about module status (disabled, vendored).
Note that for vendored modules, that is the version listed and not the one from go.mod. Note that for vendored modules, that is the version listed and not the one from go.mod.
`, `,
@ -139,7 +135,7 @@ Note that for vendored modules, that is the version listed and not the one from
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification") cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
}, },
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil)) conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil { if err != nil {
return err return err
} }
@ -149,7 +145,7 @@ Note that for vendored modules, that is the version listed and not the one from
}, },
&simpleCommand{ &simpleCommand{
name: "clean", name: "clean",
short: "Delete the Hugo Module cache for the current project", short: "Delete the Hugo Module cache for the current project.",
long: `Delete the Hugo Module cache for the current project.`, long: `Delete the Hugo Module cache for the current project.`,
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.ValidArgsFunction = cobra.NoFileCompletions
@ -175,7 +171,7 @@ Note that for vendored modules, that is the version listed and not the one from
}, },
&simpleCommand{ &simpleCommand{
name: "tidy", name: "tidy",
short: "Remove unused entries in go.mod and go.sum", short: "Remove unused entries in go.mod and go.sum.",
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r) applyLocalFlagsBuildConfig(cmd, r)
@ -190,7 +186,7 @@ Note that for vendored modules, that is the version listed and not the one from
}, },
&simpleCommand{ &simpleCommand{
name: "vendor", name: "vendor",
short: "Vendor all module dependencies into the _vendor directory", short: "Vendor all module dependencies into the _vendor directory.",
long: `Vendor all module dependencies into the _vendor directory. long: `Vendor all module dependencies into the _vendor directory.
If a module is vendored, that is where Hugo will look for it's dependencies. If a module is vendored, that is where Hugo will look for it's dependencies.
`, `,
@ -209,9 +205,9 @@ Note that for vendored modules, that is the version listed and not the one from
&simpleCommand{ &simpleCommand{
name: "get", name: "get",
short: "Resolves dependencies in your current Hugo project", short: "Resolves dependencies in your current Hugo Project.",
long: ` long: `
Resolves dependencies in your current Hugo project. Resolves dependencies in your current Hugo Project.
Some examples: Some examples:
@ -272,14 +268,13 @@ Run "go help get" for more information. All flags available for "go get" is also
if info.Name() == "go.mod" { if info.Name() == "go.mod" {
// Found a module. // Found a module.
dir := filepath.Dir(path) dir := filepath.Dir(path)
r.Println("Update module in", dir)
cfg := config.New() cfg := config.New()
cfg.Set("workingDir", dir) cfg.Set("workingDir", dir)
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Add(1)}, flagsToCfg(cd, cfg)) conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
if err != nil { if err != nil {
return err return err
} }
r.Println("Update module in", conf.configs.Base.WorkingDir)
client := conf.configs.ModulesClient client := conf.configs.ModulesClient
return client.Get(args...) return client.Get(args...)
@ -288,7 +283,7 @@ Run "go help get" for more information. All flags available for "go get" is also
}) })
return nil return nil
} else { } else {
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil)) conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil { if err != nil {
return err return err
} }
@ -317,7 +312,7 @@ func (c *modCommands) Name() string {
} }
func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
_, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, nil) _, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), nil)
if err != nil { if err != nil {
return err return err
} }
@ -328,7 +323,7 @@ func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args
func (c *modCommands) Init(cd *simplecobra.Commandeer) error { func (c *modCommands) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand cmd := cd.CobraCommand
cmd.Short = "Manage modules" cmd.Short = "Various Hugo Modules helpers."
cmd.Long = `Various helpers to help manage the modules in your project's dependency graph. cmd.Long = `Various helpers to help manage the modules in your project's dependency graph.
Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git). Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git).
This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor". This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor".

View file

@ -40,7 +40,7 @@ func newNewCommand() *newCommand {
&simpleCommand{ &simpleCommand{
name: "content", name: "content",
use: "content [path]", use: "content [path]",
short: "Create new content", short: "Create new content for your site",
long: `Create a new content file and automatically set the date and title. long: `Create a new content file and automatically set the date and title.
It will guess which kind of file to create based on the path provided. It will guess which kind of file to create based on the path provided.
@ -76,8 +76,10 @@ Ensure you run this within the root directory of your site.`,
&simpleCommand{ &simpleCommand{
name: "site", name: "site",
use: "site [path]", use: "site [path]",
short: "Create a new site", short: "Create a new site (skeleton)",
long: `Create a new site at the specified path.`, long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 { if len(args) < 1 {
return newUserError("path needs to be provided") return newUserError("path needs to be provided")
@ -91,7 +93,7 @@ Ensure you run this within the root directory of your site.`,
cfg.Set("workingDir", createpath) cfg.Set("workingDir", createpath)
cfg.Set("publishDir", "public") cfg.Set("publishDir", "public")
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg)) conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
if err != nil { if err != nil {
return err return err
} }
@ -122,9 +124,11 @@ Ensure you run this within the root directory of your site.`,
&simpleCommand{ &simpleCommand{
name: "theme", name: "theme",
use: "theme [name]", use: "theme [name]",
short: "Create a new theme", short: "Create a new theme (skeleton)",
long: `Create a new theme with the specified name in the ./themes directory. long: `Create a new theme (skeleton) called [name] in ./themes.
This generates a functional theme including template examples and sample content.`, New theme is a skeleton. Please add content to the touched files. Add your
name to the copyright line in the license and adjust the theme.toml file
according to your needs.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 { if len(args) < 1 {
return newUserError("theme name needs to be provided") return newUserError("theme name needs to be provided")
@ -132,7 +136,7 @@ This generates a functional theme including template examples and sample content
cfg := config.New() cfg := config.New()
cfg.Set("publishDir", "public") cfg.Set("publishDir", "public")
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg)) conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
if err != nil { if err != nil {
return err return err
} }
@ -140,7 +144,7 @@ This generates a functional theme including template examples and sample content
createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0])) createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0]))
r.Println("Creating new theme in", createpath) r.Println("Creating new theme in", createpath)
err = skeletons.CreateTheme(createpath, sourceFs, format) err = skeletons.CreateTheme(createpath, sourceFs)
if err != nil { if err != nil {
return err return err
} }
@ -148,14 +152,7 @@ This generates a functional theme including template examples and sample content
return nil return nil
}, },
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { cmd.ValidArgsFunction = cobra.NoFileCompletions
if len(args) != 0 {
return []string{}, cobra.ShellCompDirectiveNoFileComp
}
return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs
}
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
}, },
}, },
}, },
@ -184,7 +181,7 @@ func (c *newCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args [
func (c *newCommand) Init(cd *simplecobra.Commandeer) error { func (c *newCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand cmd := cd.CobraCommand
cmd.Short = "Create new content" cmd.Short = "Create new content for your site"
cmd.Long = `Create a new content file and automatically set the date and title. cmd.Long = `Create a new content file and automatically set the date and title.
It will guess which kind of file to create based on the path provided. It will guess which kind of file to create based on the path provided.

View file

@ -32,7 +32,7 @@ func newReleaseCommand() simplecobra.Commander {
return &simpleCommand{ return &simpleCommand{
name: "release", name: "release",
short: "Release a new version of Hugo", short: "Release a new version of Hugo.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
rel, err := releaser.New(skipPush, try, step) rel, err := releaser.New(skipPush, try, step)
if err != nil { if err != nil {

View file

@ -23,7 +23,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"maps"
"net" "net"
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
@ -33,7 +32,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -42,14 +40,12 @@ import (
"time" "time"
"github.com/bep/mclib" "github.com/bep/mclib"
"github.com/pkg/browser"
"github.com/bep/debounce" "github.com/bep/debounce"
"github.com/bep/simplecobra" "github.com/bep/simplecobra"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/common/urls"
@ -59,6 +55,7 @@ import (
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems" "github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload" "github.com/gohugoio/hugo/livereload"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/transform" "github.com/gohugoio/hugo/transform"
"github.com/gohugoio/hugo/transform/livereloadinject" "github.com/gohugoio/hugo/transform/livereloadinject"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -85,14 +82,10 @@ const (
configChangeGoWork = "go work file" configChangeGoWork = "go work file"
) )
const (
hugoHeaderRedirect = "X-Hugo-Redirect"
)
func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder { func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder {
var visitedURLs *types.EvictingQueue[string] var visitedURLs *types.EvictingStringQueue
if s != nil && !s.disableFastRender { if s != nil && !s.disableFastRender {
visitedURLs = types.NewEvictingQueue[string](20) visitedURLs = types.NewEvictingStringQueue(20)
} }
return &hugoBuilder{ return &hugoBuilder{
r: r, r: r,
@ -120,7 +113,7 @@ func newServerCommand() *serverCommand {
commands: []simplecobra.Commander{ commands: []simplecobra.Commander{
&simpleCommand{ &simpleCommand{
name: "trust", name: "trust",
short: "Install the local CA in the system trust store", short: "Install the local CA in the system trust store.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
action := "-install" action := "-install"
if uninstall { if uninstall {
@ -169,16 +162,16 @@ type dynamicEvents struct {
type fileChangeDetector struct { type fileChangeDetector struct {
sync.Mutex sync.Mutex
current map[string]uint64 current map[string]string
prev map[string]uint64 prev map[string]string
irrelevantRe *regexp.Regexp irrelevantRe *regexp.Regexp
} }
func (f *fileChangeDetector) OnFileClose(name string, checksum uint64) { func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
f.current[name] = checksum f.current[name] = md5sum
} }
func (f *fileChangeDetector) PrepareNew() { func (f *fileChangeDetector) PrepareNew() {
@ -190,14 +183,16 @@ func (f *fileChangeDetector) PrepareNew() {
defer f.Unlock() defer f.Unlock()
if f.current == nil { if f.current == nil {
f.current = make(map[string]uint64) f.current = make(map[string]string)
f.prev = make(map[string]uint64) f.prev = make(map[string]string)
return return
} }
f.prev = make(map[string]uint64) f.prev = make(map[string]string)
maps.Copy(f.prev, f.current) for k, v := range f.current {
f.current = make(map[string]uint64) f.prev[k] = v
}
f.current = make(map[string]string)
} }
func (f *fileChangeDetector) changed() []string { func (f *fileChangeDetector) changed() []string {
@ -214,17 +209,16 @@ func (f *fileChangeDetector) changed() []string {
} }
} }
return f.filterIrrelevantAndSort(c) return f.filterIrrelevant(c)
} }
func (f *fileChangeDetector) filterIrrelevantAndSort(in []string) []string { func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
var filtered []string var filtered []string
for _, v := range in { for _, v := range in {
if !f.irrelevantRe.MatchString(v) { if !f.irrelevantRe.MatchString(v) {
filtered = append(filtered, v) filtered = append(filtered, v)
} }
} }
sort.Strings(filtered)
return filtered return filtered
} }
@ -310,8 +304,8 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
w.Header().Set(header.Key, header.Value) w.Header().Set(header.Key, header.Value)
} }
if canRedirect(requestURI, r) { if redirect := serverConfig.MatchRedirect(requestURI); !redirect.IsZero() {
if redirect := serverConfig.MatchRedirect(requestURI, r.Header); !redirect.IsZero() { // fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
doRedirect := true doRedirect := true
// This matches Netlify's behavior and is needed for SPA behavior. // This matches Netlify's behavior and is needed for SPA behavior.
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/ // See https://docs.netlify.com/routing/redirects/rewrites-proxies/
@ -340,7 +334,6 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
} }
if doRedirect { if doRedirect {
w.Header().Set(hugoHeaderRedirect, "true")
switch redirect.Status { switch redirect.Status {
case 404: case 404:
w.WriteHeader(404) w.WriteHeader(404)
@ -364,11 +357,11 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
} }
} }
}
} }
if f.c.fastRenderMode && f.c.errState.buildErr() == nil { if f.c.fastRenderMode && f.c.errState.buildErr() == nil {
if isNavigation(requestURI, r) { if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") {
if !f.c.visitedURLs.Contains(requestURI) { if !f.c.visitedURLs.Contains(requestURI) {
// If not already on stack, re-render that single page. // If not already on stack, re-render that single page.
if err := f.c.partialReRender(requestURI); err != nil { if err := f.c.partialReRender(requestURI); err != nil {
@ -455,7 +448,6 @@ type serverCommand struct {
// Flags. // Flags.
renderStaticToDisk bool renderStaticToDisk bool
navigateToChanged bool navigateToChanged bool
openBrowser bool
serverAppend bool serverAppend bool
serverInterface string serverInterface string
tlsCertFile string tlsCertFile string
@ -516,7 +508,7 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
func (c *serverCommand) Init(cd *simplecobra.Commandeer) error { func (c *serverCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand cmd := cd.CobraCommand
cmd.Short = "Start the embedded web server" cmd.Short = "A high performance webserver"
cmd.Long = `Hugo provides its own webserver which builds and serves the site. cmd.Long = `Hugo provides its own webserver which builds and serves the site.
While hugo server is high performance, it is a webserver with limited options. While hugo server is high performance, it is a webserver with limited options.
@ -547,7 +539,6 @@ of a second, you will be able to save and see your changes nearly instantly.`
cmd.Flags().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL") cmd.Flags().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL")
cmd.Flags().BoolVar(&c.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild") cmd.Flags().BoolVar(&c.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
cmd.Flags().BoolVarP(&c.navigateToChanged, "navigateToChanged", "N", false, "navigate to changed content file on live browser reload") cmd.Flags().BoolVarP(&c.navigateToChanged, "navigateToChanged", "N", false, "navigate to changed content file on live browser reload")
cmd.Flags().BoolVarP(&c.openBrowser, "openBrowser", "O", false, "open the site in a browser after server startup")
cmd.Flags().BoolVar(&c.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory") cmd.Flags().BoolVar(&c.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
cmd.Flags().BoolVar(&c.disableFastRender, "disableFastRender", false, "enables full re-renders on changes") cmd.Flags().BoolVar(&c.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser") cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
@ -627,7 +618,7 @@ func (c *serverCommand) setServerInfoInConfig() error {
panic("no server ports set") panic("no server ports set")
} }
return c.withConfE(func(conf *commonConfig) error { return c.withConfE(func(conf *commonConfig) error {
for i, language := range conf.configs.LanguagesDefaultFirst { for i, language := range conf.configs.Languages {
isMultihost := conf.configs.IsMultihost isMultihost := conf.configs.IsMultihost
var serverPort int var serverPort int
if isMultihost { if isMultihost {
@ -657,8 +648,9 @@ func (c *serverCommand) setServerInfoInConfig() error {
} }
func (c *serverCommand) getErrorWithContext() any { func (c *serverCommand) getErrorWithContext() any {
buildErr := c.errState.buildErr() errCount := c.errCount()
if buildErr == nil {
if errCount == 0 {
return nil return nil
} }
@ -667,7 +659,7 @@ func (c *serverCommand) getErrorWithContext() any {
m["Error"] = cleanErrorLog(c.r.logger.Errors()) m["Error"] = cleanErrorLog(c.r.logger.Errors())
m["Version"] = hugo.BuildVersionString() m["Version"] = hugo.BuildVersionString()
ferrors := herrors.UnwrapFileErrorsWithErrorContext(buildErr) ferrors := herrors.UnwrapFileErrorsWithErrorContext(c.errState.buildErr())
m["Files"] = ferrors m["Files"] = ferrors
return m return m
@ -758,7 +750,7 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
c.serverPorts = make([]serverPortListener, len(conf.configs.Languages)) c.serverPorts = make([]serverPortListener, len(conf.configs.Languages))
} }
currentServerPort := c.serverPort currentServerPort := c.serverPort
for i := range c.serverPorts { for i := 0; i < len(c.serverPorts); i++ {
l, err := net.Listen("tcp", net.JoinHostPort(c.serverInterface, strconv.Itoa(currentServerPort))) l, err := net.Listen("tcp", net.JoinHostPort(c.serverInterface, strconv.Itoa(currentServerPort)))
if err == nil { if err == nil {
c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort} c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
@ -838,25 +830,22 @@ func (c *serverCommand) fixURL(baseURLFromConfig, baseURLFromFlag string, port i
return u.String(), nil return u.String(), nil
} }
func (c *serverCommand) partialReRender(urls ...string) (err error) { func (c *serverCommand) partialReRender(urls ...string) error {
defer func() { defer func() {
c.errState.setWasErr(false) c.errState.setWasErr(false)
}() }()
visited := types.NewEvictingQueue[string](len(urls)) c.errState.setBuildErr(nil)
visited := types.NewEvictingStringQueue(len(urls))
for _, url := range urls { for _, url := range urls {
visited.Add(url) visited.Add(url)
} }
var h *hugolib.HugoSites h, err := c.hugo()
h, err = c.hugo()
if err != nil { if err != nil {
return return err
} }
// Note: We do not set NoBuildLock as the file lock is not acquired at this stage. // Note: We do not set NoBuildLock as the file lock is not acquired at this stage.
err = h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyTouched: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()}) return h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()})
return
} }
func (c *serverCommand) serve() error { func (c *serverCommand) serve() error {
@ -897,16 +886,16 @@ func (c *serverCommand) serve() error {
// To allow the en user to change the error template while the server is running, we use // To allow the en user to change the error template while the server is running, we use
// the freshest template we can provide. // the freshest template we can provide.
var ( var (
errTempl *tplimpl.TemplInfo errTempl tpl.Template
templHandler *tplimpl.TemplateStore templHandler tpl.TemplateHandler
) )
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) { getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
if h == nil { if h == nil {
return errTempl, templHandler return errTempl, templHandler
} }
templHandler := h.GetTemplateStore() templHandler := h.Tmpl()
errTempl := templHandler.LookupByPath("/_server/error.html") errTempl, found := templHandler.Lookup("_server/error.html")
if errTempl == nil { if !found {
panic("template server/error.html not found") panic("template server/error.html not found")
} }
return errTempl, templHandler return errTempl, templHandler
@ -1007,13 +996,6 @@ func (c *serverCommand) serve() error {
c.r.Println("Press Ctrl+C to stop") c.r.Println("Press Ctrl+C to stop")
if c.openBrowser {
// There may be more than one baseURL in multihost mode, open the first.
if err := browser.OpenURL(baseURLs[0].String()); err != nil {
c.r.logger.Warnf("Failed to open browser: %s", err)
}
}
err = func() error { err = func() error {
for { for {
select { select {
@ -1030,6 +1012,10 @@ func (c *serverCommand) serve() error {
c.r.Println("Error:", err) c.r.Println("Error:", err)
} }
if h := c.hugoTry(); h != nil {
h.Close()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
wg2, ctx := errgroup.WithContext(ctx) wg2, ctx := errgroup.WithContext(ctx)
@ -1234,24 +1220,3 @@ func formatByteCount(b uint64) string {
return fmt.Sprintf("%.1f %cB", return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp]) float64(b)/float64(div), "kMGTPE"[exp])
} }
func canRedirect(requestURIWithoutQuery string, r *http.Request) bool {
if r.Header.Get(hugoHeaderRedirect) != "" {
return false
}
return isNavigation(requestURIWithoutQuery, r)
}
// Sec-Fetch-Mode should be sent by all recent browser versions, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Mode#navigate
// Fall back to the file extension if not set.
// The main take here is that we don't want to have CSS/JS files etc. partake in this logic.
func isNavigation(requestURIWithoutQuery string, r *http.Request) bool {
return r.Header.Get("Sec-Fetch-Mode") == "navigate" || isPropablyHTMLRequest(requestURIWithoutQuery)
}
func isPropablyHTMLRequest(requestURIWithoutQuery string) bool {
if strings.HasSuffix(requestURIWithoutQuery, "/") || strings.HasSuffix(requestURIWithoutQuery, "html") || strings.HasSuffix(requestURIWithoutQuery, "htm") {
return true
}
return !strings.Contains(requestURIWithoutQuery, ".")
}

View file

@ -117,7 +117,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
tos = append(tos, nil) tos = append(tos, nil)
continue continue
} }
for i := range slice.Len() { for i := 0; i < slice.Len(); i++ {
tos = append(tos, slice.Index(i).Interface()) tos = append(tos, slice.Index(i).Interface())
} }
} }
@ -128,7 +128,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) { func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
var tos []any var tos []any
for i := range tov.Len() { for i := 0; i < tov.Len(); i++ {
tos = append(tos, tov.Index(i).Interface()) tos = append(tos, tov.Index(i).Interface())
} }

View file

@ -15,7 +15,6 @@ package collections
import ( import (
"html/template" "html/template"
"reflect"
"testing" "testing"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
@ -78,7 +77,6 @@ func TestAppend(t *testing.T) {
{[]string{"a", "b"}, []any{nil}, []any{"a", "b", nil}}, {[]string{"a", "b"}, []any{nil}, []any{"a", "b", nil}},
{[]string{"a", "b"}, []any{nil, "d", nil}, []any{"a", "b", nil, "d", nil}}, {[]string{"a", "b"}, []any{nil, "d", nil}, []any{"a", "b", nil, "d", nil}},
{[]any{"a", nil, "c"}, []any{"d", nil, "f"}, []any{"a", nil, "c", "d", nil, "f"}}, {[]any{"a", nil, "c"}, []any{"d", nil, "f"}, []any{"a", nil, "c", "d", nil, "f"}},
{[]string{"a", "b"}, []any{}, []string{"a", "b"}},
} { } {
result, err := Append(test.start, test.addend...) result, err := Append(test.start, test.addend...)
@ -148,66 +146,3 @@ func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) {
c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"}) c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(slice, qt.DeepEquals, []string{"d", "b"}) c.Assert(slice, qt.DeepEquals, []string{"d", "b"})
} }
func TestIndirect(t *testing.T) {
t.Parallel()
c := qt.New(t)
type testStruct struct {
Field string
}
var (
nilPtr *testStruct
nilIface interface{} = nil
nonNilIface interface{} = &testStruct{Field: "hello"}
)
tests := []struct {
name string
input any
wantKind reflect.Kind
wantNil bool
}{
{
name: "nil pointer",
input: nilPtr,
wantKind: reflect.Ptr,
wantNil: true,
},
{
name: "nil interface",
input: nilIface,
wantKind: reflect.Invalid,
wantNil: false,
},
{
name: "non-nil pointer to struct",
input: &testStruct{Field: "abc"},
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "non-nil interface holding pointer",
input: nonNilIface,
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "plain value",
input: testStruct{Field: "xyz"},
wantKind: reflect.Struct,
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := reflect.ValueOf(tt.input)
got, isNil := indirect(v)
c.Assert(got.Kind(), qt.Equals, tt.wantKind)
c.Assert(isNil, qt.Equals, tt.wantNil)
})
}
}

View file

@ -136,37 +136,3 @@ func TestSortedStringSlice(t *testing.T) {
c.Assert(s.Count("z"), qt.Equals, 0) c.Assert(s.Count("z"), qt.Equals, 0)
c.Assert(s.Count("a"), qt.Equals, 1) c.Assert(s.Count("a"), qt.Equals, 1)
} }
func TestStringSliceToInterfaceSlice(t *testing.T) {
t.Parallel()
c := qt.New(t)
tests := []struct {
name string
in []string
want []any
}{
{
name: "empty slice",
in: []string{},
want: []any{},
},
{
name: "single element",
in: []string{"hello"},
want: []any{"hello"},
},
{
name: "multiple elements",
in: []string{"a", "b", "c"},
want: []any{"a", "b", "c"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := StringSliceToInterfaceSlice(tt.in)
c.Assert(got, qt.DeepEquals, tt.want)
})
}
}

View file

@ -13,8 +13,6 @@
package collections package collections
import "slices"
import "sync" import "sync"
// Stack is a simple LIFO stack that is safe for concurrent use. // Stack is a simple LIFO stack that is safe for concurrent use.
@ -67,16 +65,3 @@ func (s *Stack[T]) Drain() []T {
s.items = nil s.items = nil
return items return items
} }
func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
s.mu.Lock()
defer s.mu.Unlock()
var items []T
for i := len(s.items) - 1; i >= 0; i-- {
if predicate(s.items[i]) {
items = append(items, s.items[i])
s.items = slices.Delete(s.items, i, i+1)
}
}
return items
}

View file

@ -1,77 +0,0 @@
package collections
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestNewStack(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
c.Assert(s, qt.IsNotNil)
}
func TestStackBasic(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
c.Assert(s.Len(), qt.Equals, 0)
s.Push(1)
s.Push(2)
s.Push(3)
c.Assert(s.Len(), qt.Equals, 3)
top, ok := s.Peek()
c.Assert(ok, qt.Equals, true)
c.Assert(top, qt.Equals, 3)
popped, ok := s.Pop()
c.Assert(ok, qt.Equals, true)
c.Assert(popped, qt.Equals, 3)
c.Assert(s.Len(), qt.Equals, 2)
_, _ = s.Pop()
_, _ = s.Pop()
_, ok = s.Pop()
c.Assert(ok, qt.Equals, false)
}
func TestStackDrain(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[string]()
s.Push("a")
s.Push("b")
got := s.Drain()
c.Assert(got, qt.DeepEquals, []string{"a", "b"})
c.Assert(s.Len(), qt.Equals, 0)
}
func TestStackDrainMatching(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
s.Push(1)
s.Push(2)
s.Push(3)
s.Push(4)
got := s.DrainMatching(func(v int) bool { return v%2 == 0 })
c.Assert(got, qt.DeepEquals, []int{4, 2})
c.Assert(s.Drain(), qt.DeepEquals, []int{1, 3})
}

View file

@ -21,10 +21,6 @@ const (
ErrRemoteGetCSV = "error-remote-getcsv" ErrRemoteGetCSV = "error-remote-getcsv"
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides" WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle"
) )
// Field/method names with special meaning. // Field/method names with special meaning.
@ -43,7 +39,7 @@ const (
ResourceTransformationFingerprint = "fingerprint" ResourceTransformationFingerprint = "fingerprint"
) )
// IsResourceTransformationPermalinkHash returns whether the given name is a resource transformation that changes the permalink based on the content. // IsResourceTransformationLinkChange returns whether the given name is a resource transformation that changes the permalink based on the content.
func IsResourceTransformationPermalinkHash(name string) bool { func IsResourceTransformationPermalinkHash(name string) bool {
return name == ResourceTransformationFingerprint return name == ResourceTransformationFingerprint
} }

View file

@ -1,194 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package hashing provides common hashing utilities.
package hashing
import (
"crypto/md5"
"encoding/hex"
"io"
"strconv"
"sync"
"github.com/cespare/xxhash/v2"
"github.com/gohugoio/hashstructure"
"github.com/gohugoio/hugo/identity"
)
// XXHashFromReader calculates the xxHash for the given reader.
func XXHashFromReader(r io.Reader) (uint64, int64, error) {
h := getXxHashReadFrom()
defer putXxHashReadFrom(h)
size, err := io.Copy(h, r)
if err != nil {
return 0, 0, err
}
return h.Sum64(), size, nil
}
// XxHashFromReaderHexEncoded calculates the xxHash for the given reader
// and returns the hash as a hex encoded string.
func XxHashFromReaderHexEncoded(r io.Reader) (string, error) {
h := getXxHashReadFrom()
defer putXxHashReadFrom(h)
_, err := io.Copy(h, r)
if err != nil {
return "", err
}
hash := h.Sum(nil)
return hex.EncodeToString(hash), nil
}
// XXHashFromString calculates the xxHash for the given string.
func XXHashFromString(s string) (uint64, error) {
h := xxhash.New()
h.WriteString(s)
return h.Sum64(), nil
}
// XxHashFromStringHexEncoded calculates the xxHash for the given string
// and returns the hash as a hex encoded string.
func XxHashFromStringHexEncoded(f string) string {
h := xxhash.New()
h.WriteString(f)
hash := h.Sum(nil)
return hex.EncodeToString(hash)
}
// MD5FromStringHexEncoded returns the MD5 hash of the given string.
func MD5FromStringHexEncoded(f string) string {
h := md5.New()
h.Write([]byte(f))
return hex.EncodeToString(h.Sum(nil))
}
// HashString returns a hash from the given elements.
// It will panic if the hash cannot be calculated.
// Note that this hash should be used primarily for identity, not for change detection as
// it in the more complex values (e.g. Page) will not hash the full content.
func HashString(vs ...any) string {
hash := HashUint64(vs...)
return strconv.FormatUint(hash, 10)
}
// HashStringHex returns a hash from the given elements as a hex encoded string.
// See HashString for more information.
func HashStringHex(vs ...any) string {
hash := HashUint64(vs...)
return strconv.FormatUint(hash, 16)
}
var hashOptsPool = sync.Pool{
New: func() any {
return &hashstructure.HashOptions{
Hasher: xxhash.New(),
}
},
}
func getHashOpts() *hashstructure.HashOptions {
return hashOptsPool.Get().(*hashstructure.HashOptions)
}
func putHashOpts(opts *hashstructure.HashOptions) {
opts.Hasher.Reset()
hashOptsPool.Put(opts)
}
// HashUint64 returns a hash from the given elements.
// It will panic if the hash cannot be calculated.
// Note that this hash should be used primarily for identity, not for change detection as
// it in the more complex values (e.g. Page) will not hash the full content.
func HashUint64(vs ...any) uint64 {
var o any
if len(vs) == 1 {
o = toHashable(vs[0])
} else {
elements := make([]any, len(vs))
for i, e := range vs {
elements[i] = toHashable(e)
}
o = elements
}
hash, err := Hash(o)
if err != nil {
panic(err)
}
return hash
}
// Hash returns a hash from vs.
func Hash(vs ...any) (uint64, error) {
hashOpts := getHashOpts()
defer putHashOpts(hashOpts)
var v any = vs
if len(vs) == 1 {
v = vs[0]
}
return hashstructure.Hash(v, hashOpts)
}
type keyer interface {
Key() string
}
// For structs, hashstructure.Hash only works on the exported fields,
// so rewrite the input slice for known identity types.
func toHashable(v any) any {
switch t := v.(type) {
case keyer:
return t.Key()
case identity.IdentityProvider:
return t.GetIdentity()
default:
return v
}
}
type xxhashReadFrom struct {
buff []byte
*xxhash.Digest
}
func (x *xxhashReadFrom) ReadFrom(r io.Reader) (int64, error) {
for {
n, err := r.Read(x.buff)
if n > 0 {
x.Digest.Write(x.buff[:n])
}
if err != nil {
if err == io.EOF {
err = nil
}
return int64(n), err
}
}
}
var xXhashReadFromPool = sync.Pool{
New: func() any {
return &xxhashReadFrom{Digest: xxhash.New(), buff: make([]byte, 48*1024)}
},
}
func getXxHashReadFrom() *xxhashReadFrom {
return xXhashReadFromPool.Get().(*xxhashReadFrom)
}
func putXxHashReadFrom(h *xxhashReadFrom) {
h.Reset()
xXhashReadFromPool.Put(h)
}

View file

@ -1,157 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hashing
import (
"fmt"
"math"
"strings"
"sync"
"testing"
qt "github.com/frankban/quicktest"
)
func TestXxHashFromReader(t *testing.T) {
c := qt.New(t)
s := "Hello World"
r := strings.NewReader(s)
got, size, err := XXHashFromReader(r)
c.Assert(err, qt.IsNil)
c.Assert(size, qt.Equals, int64(len(s)))
c.Assert(got, qt.Equals, uint64(7148569436472236994))
}
func TestXxHashFromReaderPara(t *testing.T) {
c := qt.New(t)
var wg sync.WaitGroup
for i := range 10 {
i := i
wg.Add(1)
go func() {
defer wg.Done()
for j := range 100 {
s := strings.Repeat("Hello ", i+j+1*42)
r := strings.NewReader(s)
got, size, err := XXHashFromReader(r)
c.Assert(size, qt.Equals, int64(len(s)))
c.Assert(err, qt.IsNil)
expect, _ := XXHashFromString(s)
c.Assert(got, qt.Equals, expect)
}
}()
}
wg.Wait()
}
func TestXxHashFromString(t *testing.T) {
c := qt.New(t)
s := "Hello World"
got, err := XXHashFromString(s)
c.Assert(err, qt.IsNil)
c.Assert(got, qt.Equals, uint64(7148569436472236994))
}
func TestXxHashFromStringHexEncoded(t *testing.T) {
c := qt.New(t)
s := "The quick brown fox jumps over the lazy dog"
got := XxHashFromStringHexEncoded(s)
// Facit: https://asecuritysite.com/encryption/xxhash?val=The%20quick%20brown%20fox%20jumps%20over%20the%20lazy%20dog
c.Assert(got, qt.Equals, "0b242d361fda71bc")
}
func BenchmarkXXHashFromReader(b *testing.B) {
r := strings.NewReader("Hello World")
b.ResetTimer()
for i := 0; i < b.N; i++ {
XXHashFromReader(r)
r.Seek(0, 0)
}
}
func BenchmarkXXHashFromString(b *testing.B) {
s := "Hello World"
b.ResetTimer()
for i := 0; i < b.N; i++ {
XXHashFromString(s)
}
}
func BenchmarkXXHashFromStringHexEncoded(b *testing.B) {
s := "The quick brown fox jumps over the lazy dog"
b.ResetTimer()
for i := 0; i < b.N; i++ {
XxHashFromStringHexEncoded(s)
}
}
func TestHashString(t *testing.T) {
c := qt.New(t)
c.Assert(HashString("a", "b"), qt.Equals, "3176555414984061461")
c.Assert(HashString("ab"), qt.Equals, "7347350983217793633")
var vals []any = []any{"a", "b", tstKeyer{"c"}}
c.Assert(HashString(vals...), qt.Equals, "4438730547989914315")
c.Assert(vals[2], qt.Equals, tstKeyer{"c"})
}
type tstKeyer struct {
key string
}
func (t tstKeyer) Key() string {
return t.key
}
func (t tstKeyer) String() string {
return "key: " + t.key
}
func BenchmarkHashString(b *testing.B) {
word := " hello "
var tests []string
for i := 1; i <= 5; i++ {
sentence := strings.Repeat(word, int(math.Pow(4, float64(i))))
tests = append(tests, sentence)
}
b.ResetTimer()
for _, test := range tests {
b.Run(fmt.Sprintf("n%d", len(test)), func(b *testing.B) {
for i := 0; i < b.N; i++ {
HashString(test)
}
})
}
}
func BenchmarkHashMap(b *testing.B) {
m := map[string]any{}
for i := range 1000 {
m[fmt.Sprintf("key%d", i)] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
HashString(m)
}
}

View file

@ -152,7 +152,10 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
} }
if ectx.Position.LineNumber > 0 { if ectx.Position.LineNumber > 0 {
low := max(ectx.Position.LineNumber-3, 0) low := ectx.Position.LineNumber - 3
if low < 0 {
low = 0
}
if ectx.Position.LineNumber > 2 { if ectx.Position.LineNumber > 2 {
ectx.LinesPos = 2 ectx.LinesPos = 2
@ -160,7 +163,10 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
ectx.LinesPos = ectx.Position.LineNumber - 1 ectx.LinesPos = ectx.Position.LineNumber - 1
} }
high := min(ectx.Position.LineNumber+2, len(lines)) high := ectx.Position.LineNumber + 2
if high > len(lines) {
high = len(lines)
}
ectx.Lines = lines[low:high] ectx.Lines = lines[low:high]

View file

@ -68,20 +68,6 @@ func (e *TimeoutError) Is(target error) bool {
return ok return ok
} }
// errMessage wraps an error with a message.
type errMessage struct {
msg string
err error
}
func (e *errMessage) Error() string {
return e.msg
}
func (e *errMessage) Unwrap() error {
return e.err
}
// IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError. // IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError.
func IsFeatureNotAvailableError(err error) bool { func IsFeatureNotAvailableError(err error) bool {
return errors.Is(err, &FeatureNotAvailableError{}) return errors.Is(err, &FeatureNotAvailableError{})
@ -133,55 +119,21 @@ func IsNotExist(err error) bool {
return false return false
} }
// IsExist returns true if the error is a file exists error.
// Unlike os.IsExist, this also considers wrapped errors.
func IsExist(err error) bool {
if os.IsExist(err) {
return true
}
// os.IsExist does not consider wrapped errors.
if os.IsExist(errors.Unwrap(err)) {
return true
}
return false
}
var nilPointerErrRe = regexp.MustCompile(`at <(.*)>: error calling (.*?): runtime error: invalid memory address or nil pointer dereference`) var nilPointerErrRe = regexp.MustCompile(`at <(.*)>: error calling (.*?): runtime error: invalid memory address or nil pointer dereference`)
const deferredPrefix = "__hdeferred/" func ImproveIfNilPointer(inErr error) (outErr error) {
var deferredStringToRemove = regexp.MustCompile(`executing "__hdeferred/.*?" `)
// ImproveRenderErr improves the error message for rendering errors.
func ImproveRenderErr(inErr error) (outErr error) {
outErr = inErr outErr = inErr
msg := improveIfNilPointerMsg(inErr)
if msg != "" {
outErr = &errMessage{msg: msg, err: outErr}
}
if strings.Contains(inErr.Error(), deferredPrefix) {
msg := deferredStringToRemove.ReplaceAllString(inErr.Error(), "executing ")
outErr = &errMessage{msg: msg, err: outErr}
}
return
}
func improveIfNilPointerMsg(inErr error) string {
m := nilPointerErrRe.FindStringSubmatch(inErr.Error()) m := nilPointerErrRe.FindStringSubmatch(inErr.Error())
if len(m) == 0 { if len(m) == 0 {
return "" return
} }
call := m[1] call := m[1]
field := m[2] field := m[2]
parts := strings.Split(call, ".") parts := strings.Split(call, ".")
if len(parts) < 2 {
return ""
}
receiverName := parts[len(parts)-2] receiverName := parts[len(parts)-2]
receiver := strings.Join(parts[:len(parts)-1], ".") receiver := strings.Join(parts[:len(parts)-1], ".")
s := fmt.Sprintf(" %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field) s := fmt.Sprintf(" %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field)
return nilPointerErrRe.ReplaceAllString(inErr.Error(), s) outErr = errors.New(nilPointerErrRe.ReplaceAllString(inErr.Error(), s))
return
} }

View file

@ -20,6 +20,8 @@ import (
"io" "io"
"path/filepath" "path/filepath"
godartsassv1 "github.com/bep/godartsass"
"github.com/bep/godartsass/v2" "github.com/bep/godartsass/v2"
"github.com/bep/golibsass/libsass/libsasserrors" "github.com/bep/golibsass/libsass/libsasserrors"
"github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/paths"
@ -151,6 +153,8 @@ func (e *fileError) causeString() string {
// Avoid repeating the file info in the error message. // Avoid repeating the file info in the error message.
case godartsass.SassError: case godartsass.SassError:
return v.Message return v.Message
case godartsassv1.SassError:
return v.Message
case libsasserrors.Error: case libsasserrors.Error:
return v.Message return v.Message
default: default:
@ -258,27 +262,8 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
return f, realFilename, nil return f, realFilename, nil
} }
// Cause returns the underlying error, that is, // Cause returns the underlying error or itself if it does not implement Unwrap.
// it unwraps errors until it finds one that does not implement
// the Unwrap method.
// For a shallow variant, see Unwrap.
func Cause(err error) error { func Cause(err error) error {
type unwrapper interface {
Unwrap() error
}
for err != nil {
cause, ok := err.(unwrapper)
if !ok {
break
}
err = cause.Unwrap()
}
return err
}
// Unwrap returns the underlying error or itself if it does not implement Unwrap.
func Unwrap(err error) error {
if u := errors.Unwrap(err); u != nil { if u := errors.Unwrap(err); u != nil {
return u return u
} }
@ -286,7 +271,7 @@ func Unwrap(err error) error {
} }
func extractFileTypePos(err error) (string, text.Position) { func extractFileTypePos(err error) (string, text.Position) {
err = Unwrap(err) err = Cause(err)
var fileType string var fileType string
@ -403,7 +388,14 @@ func extractPosition(e error) (pos text.Position) {
case godartsass.SassError: case godartsass.SassError:
span := v.Span span := v.Span
start := span.Start start := span.Start
filename, _ := paths.UrlStringToFilename(span.Url) filename, _ := paths.UrlToFilename(span.Url)
pos.Filename = filename
pos.Offset = start.Offset
pos.ColumnNumber = start.Column
case godartsassv1.SassError:
span := v.Span
start := span.Start
filename, _ := paths.UrlToFilename(span.Url)
pos.Filename = filename pos.Filename = filename
pos.Offset = start.Offset pos.Offset = start.Offset
pos.ColumnNumber = start.Column pos.ColumnNumber = start.Column

View file

@ -21,14 +21,10 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"sync"
"github.com/bep/logg" "github.com/cli/safeexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/security" "github.com/gohugoio/hugo/config/security"
) )
@ -88,7 +84,7 @@ var WithEnviron = func(env []string) func(c *commandeer) {
} }
// New creates a new Exec using the provided security config. // New creates a new Exec using the provided security config.
func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec { func New(cfg security.Config) *Exec {
var baseEnviron []string var baseEnviron []string
for _, v := range os.Environ() { for _, v := range os.Environ() {
k, _ := config.SplitEnvVar(v) k, _ := config.SplitEnvVar(v)
@ -99,10 +95,7 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
return &Exec{ return &Exec{
sc: cfg, sc: cfg,
workingDir: workingDir,
infol: log.InfoCommand("exec"),
baseEnviron: baseEnviron, baseEnviron: baseEnviron,
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
} }
} }
@ -112,27 +105,29 @@ func IsNotFound(err error) bool {
return errors.As(err, &notFoundErr) return errors.As(err, &notFoundErr)
} }
// SafeCommand is a wrapper around os/exec Command which uses a LookPath
// implementation that does not search in current directory before looking in PATH.
// See https://github.com/cli/safeexec and the linked issues.
func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
bin, err := safeexec.LookPath(name)
if err != nil {
return nil, err
}
return exec.Command(bin, arg...), nil
}
// Exec enforces a security policy for commands run via os/exec. // Exec enforces a security policy for commands run via os/exec.
type Exec struct { type Exec struct {
sc security.Config sc security.Config
workingDir string
infol logg.LevelLogger
// os.Environ filtered by the Exec.OsEnviron whitelist filter. // os.Environ filtered by the Exec.OsEnviron whitelist filter.
baseEnviron []string baseEnviron []string
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
npxInit sync.Once
npxAvailable bool
}
func (e *Exec) New(name string, arg ...any) (Runner, error) {
return e.new(name, "", arg...)
} }
// New will fail if name is not allowed according to the configured security policy. // New will fail if name is not allowed according to the configured security policy.
// Else a configured Runner will be returned ready to be Run. // Else a configured Runner will be returned ready to be Run.
func (e *Exec) new(name string, fullyQualifiedName string, arg ...any) (Runner, error) { func (e *Exec) New(name string, arg ...any) (Runner, error) {
if err := e.sc.CheckAllowedExec(name); err != nil { if err := e.sc.CheckAllowedExec(name); err != nil {
return nil, err return nil, err
} }
@ -142,111 +137,16 @@ func (e *Exec) new(name string, fullyQualifiedName string, arg ...any) (Runner,
cm := &commandeer{ cm := &commandeer{
name: name, name: name,
fullyQualifiedName: fullyQualifiedName,
env: env, env: env,
} }
return cm.command(arg...) return cm.command(arg...)
} }
type binaryLocation int // Npx is a convenience method to create a Runner running npx --no-install <name> <args.
func (b binaryLocation) String() string {
switch b {
case binaryLocationNodeModules:
return "node_modules/.bin"
case binaryLocationNpx:
return "npx"
case binaryLocationPath:
return "PATH"
}
return "unknown"
}
const (
binaryLocationNodeModules binaryLocation = iota + 1
binaryLocationNpx
binaryLocationPath
)
// Npx will in order:
// 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory.
// 2. If not found, and npx is available, run npx --no-install <name> <args>.
// 3. Fall back to the PATH.
// If name is "tailwindcss", we will try the PATH as the second option.
func (e *Exec) Npx(name string, arg ...any) (Runner, error) { func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
if err := e.sc.CheckAllowedExec(name); err != nil { arg = append(arg[:0], append([]any{"--no-install", name}, arg[0:]...)...)
return nil, err return e.New("npx", arg...)
}
newRunner, err := e.newNPXRunnerCache.GetOrCreate(name, func() (func(...any) (Runner, error), error) {
type tryFunc func() func(...any) (Runner, error)
tryFuncs := map[binaryLocation]tryFunc{
binaryLocationNodeModules: func() func(...any) (Runner, error) {
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
_, err := exec.LookPath(nodeBinFilename)
if err != nil {
return nil
}
return func(arg2 ...any) (Runner, error) {
return e.new(name, nodeBinFilename, arg2...)
}
},
binaryLocationNpx: func() func(...any) (Runner, error) {
e.checkNpx()
if !e.npxAvailable {
return nil
}
return func(arg2 ...any) (Runner, error) {
return e.npx(name, arg2...)
}
},
binaryLocationPath: func() func(...any) (Runner, error) {
if _, err := exec.LookPath(name); err != nil {
return nil
}
return func(arg2 ...any) (Runner, error) {
return e.New(name, arg2...)
}
},
}
locations := []binaryLocation{binaryLocationNodeModules, binaryLocationNpx, binaryLocationPath}
if name == "tailwindcss" {
// See https://github.com/gohugoio/hugo/issues/13221#issuecomment-2574801253
locations = []binaryLocation{binaryLocationNodeModules, binaryLocationPath, binaryLocationNpx}
}
for _, loc := range locations {
if f := tryFuncs[loc](); f != nil {
e.infol.Logf("resolve %q using %s", name, loc)
return f, nil
}
}
return nil, &NotFoundError{name: name, method: fmt.Sprintf("in %s", locations[len(locations)-1])}
})
if err != nil {
return nil, err
}
return newRunner(arg...)
}
const (
npxNoInstall = "--no-install"
npxBinary = "npx"
nodeModulesBinPath = "node_modules/.bin"
)
func (e *Exec) checkNpx() {
e.npxInit.Do(func() {
e.npxAvailable = InPath(npxBinary)
})
}
// npx is a convenience method to create a Runner running npx --no-install <name> <args.
func (e *Exec) npx(name string, arg ...any) (Runner, error) {
arg = append(arg[:0], append([]any{npxNoInstall, name}, arg[0:]...)...)
return e.New(npxBinary, arg...)
} }
// Sec returns the security policies this Exec is configured with. // Sec returns the security policies this Exec is configured with.
@ -256,11 +156,10 @@ func (e *Exec) Sec() security.Config {
type NotFoundError struct { type NotFoundError struct {
name string name string
method string
} }
func (e *NotFoundError) Error() string { func (e *NotFoundError) Error() string {
return fmt.Sprintf("binary with name %q not found %s", e.name, e.method) return fmt.Sprintf("binary with name %q not found", e.name)
} }
// Runner wraps a *os.Cmd. // Runner wraps a *os.Cmd.
@ -283,14 +182,8 @@ func (c *cmdWrapper) Run() error {
if err == nil { if err == nil {
return nil return nil
} }
name := c.name
method := "in PATH"
if name == npxBinary {
name = c.c.Args[2]
method = "using npx"
}
if notFoundRe.MatchString(c.outerr.String()) { if notFoundRe.MatchString(c.outerr.String()) {
return &NotFoundError{name: name, method: method} return &NotFoundError{name: c.name}
} }
return fmt.Errorf("failed to execute binary %q with args %v: %s", c.name, c.c.Args[1:], c.outerr.String()) return fmt.Errorf("failed to execute binary %q with args %v: %s", c.name, c.c.Args[1:], c.outerr.String())
} }
@ -307,7 +200,6 @@ type commandeer struct {
ctx context.Context ctx context.Context
name string name string
fullyQualifiedName string
env []string env []string
} }
@ -328,17 +220,10 @@ func (c *commandeer) command(arg ...any) (*cmdWrapper, error) {
} }
} }
var bin string bin, err := safeexec.LookPath(c.name)
if c.fullyQualifiedName != "" {
bin = c.fullyQualifiedName
} else {
var err error
bin, err = exec.LookPath(c.name)
if err != nil { if err != nil {
return nil, &NotFoundError{ return nil, &NotFoundError{
name: c.name, name: c.name,
method: "in PATH",
}
} }
} }
@ -371,7 +256,7 @@ func InPath(binaryName string) bool {
if strings.Contains(binaryName, "/") { if strings.Contains(binaryName, "/") {
panic("binary name should not contain any slash") panic("binary name should not contain any slash")
} }
_, err := exec.LookPath(binaryName) _, err := safeexec.LookPath(binaryName)
return err == nil return err == nil
} }
@ -381,7 +266,7 @@ func LookPath(binaryName string) string {
if strings.Contains(binaryName, "/") { if strings.Contains(binaryName, "/") {
panic("binary name should not contain any slash") panic("binary name should not contain any slash")
} }
s, err := exec.LookPath(binaryName) s, err := safeexec.LookPath(binaryName)
if err != nil { if err != nil {
return "" return ""
} }

View file

@ -74,16 +74,6 @@ func IsTruthful(in any) bool {
} }
} }
// IsMap reports whether v is a map.
func IsMap(v any) bool {
return reflect.ValueOf(v).Kind() == reflect.Map
}
// IsSlice reports whether v is a slice.
func IsSlice(v any) bool {
return reflect.ValueOf(v).Kind() == reflect.Slice
}
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem() var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
// IsTruthfulValue returns whether the given value has a meaningful truth value. // IsTruthfulValue returns whether the given value has a meaningful truth value.
@ -134,7 +124,12 @@ type methodKey struct {
name string name string
} }
var methodCache sync.Map type methods struct {
sync.RWMutex
cache map[methodKey]int
}
var methodCache = &methods{cache: make(map[methodKey]int)}
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the // GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
// type lookup. // type lookup.
@ -152,16 +147,22 @@ func GetMethodByName(v reflect.Value, name string) reflect.Value {
// -1 if no such method exists. // -1 if no such method exists.
func GetMethodIndexByName(tp reflect.Type, name string) int { func GetMethodIndexByName(tp reflect.Type, name string) int {
k := methodKey{tp, name} k := methodKey{tp, name}
v, found := methodCache.Load(k) methodCache.RLock()
index, found := methodCache.cache[k]
methodCache.RUnlock()
if found { if found {
return v.(int) return index
} }
methodCache.Lock()
defer methodCache.Unlock()
m, ok := tp.MethodByName(name) m, ok := tp.MethodByName(name)
index := m.Index index = m.Index
if !ok { if !ok {
index = -1 index = -1
} }
methodCache.Store(k, index) methodCache.cache[k] = index
if !ok { if !ok {
return -1 return -1
@ -222,27 +223,6 @@ func AsTime(v reflect.Value, loc *time.Location) (time.Time, bool) {
return time.Time{}, false return time.Time{}, false
} }
// ToSliceAny converts the given value to a slice of any if possible.
func ToSliceAny(v any) ([]any, bool) {
if v == nil {
return nil, false
}
switch vv := v.(type) {
case []any:
return vv, true
default:
vvv := reflect.ValueOf(v)
if vvv.Kind() == reflect.Slice {
out := make([]any, vvv.Len())
for i := range vvv.Len() {
out[i] = vvv.Index(i).Interface()
}
return out, true
}
}
return nil, false
}
func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value { func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
fn := v.MethodByName(name) fn := v.MethodByName(name)
var args []reflect.Value var args []reflect.Value
@ -288,8 +268,7 @@ func IsContextType(tp reflect.Type) bool {
return true return true
} }
isContext, _ := isContextCache.GetOrCreate(tp, func() (bool, error) { return isContextCache.GetOrCreate(tp, func() bool {
return tp.Implements(contextInterface), nil return tp.Implements(contextInterface)
}) })
return isContext
} }

View file

@ -50,19 +50,6 @@ func TestIsContextType(t *testing.T) {
c.Assert(IsContextType(reflect.TypeOf(valueCtx)), qt.IsTrue) c.Assert(IsContextType(reflect.TypeOf(valueCtx)), qt.IsTrue)
} }
func TestToSliceAny(t *testing.T) {
c := qt.New(t)
checkOK := func(in any, expected []any) {
out, ok := ToSliceAny(in)
c.Assert(ok, qt.Equals, true)
c.Assert(out, qt.DeepEquals, expected)
}
checkOK([]any{1, 2, 3}, []any{1, 2, 3})
checkOK([]int{1, 2, 3}, []any{1, 2, 3})
}
func BenchmarkIsContextType(b *testing.B) { func BenchmarkIsContextType(b *testing.B) {
type k string type k string
b.Run("value", func(b *testing.B) { b.Run("value", func(b *testing.B) {
@ -134,17 +121,3 @@ func BenchmarkGetMethodByName(b *testing.B) {
} }
} }
} }
func BenchmarkGetMethodByNamePara(b *testing.B) {
v := reflect.ValueOf(&testStruct{})
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
for _, method := range methods {
_ = GetMethodByName(v, method)
}
}
})
}

View file

@ -16,7 +16,6 @@ package hstrings
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"slices"
"strings" "strings"
"sync" "sync"
@ -51,7 +50,12 @@ func (s StringEqualFold) Eq(s2 any) bool {
// EqualAny returns whether a string is equal to any of the given strings. // EqualAny returns whether a string is equal to any of the given strings.
func EqualAny(a string, b ...string) bool { func EqualAny(a string, b ...string) bool {
return slices.Contains(b, a) for _, s := range b {
if a == s {
return true
}
}
return false
} }
// regexpCache represents a cache of regexp objects protected by a mutex. // regexpCache represents a cache of regexp objects protected by a mutex.
@ -99,7 +103,12 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
// InSlice checks if a string is an element of a slice of strings // InSlice checks if a string is an element of a slice of strings
// and returns a boolean value. // and returns a boolean value.
func InSlice(arr []string, el string) bool { func InSlice(arr []string, el string) bool {
return slices.Contains(arr, el) for _, v := range arr {
if v == el {
return true
}
}
return false
} }
// InSlicEqualFold checks if a string is an element of a slice of strings // InSlicEqualFold checks if a string is an element of a slice of strings
@ -128,7 +137,7 @@ func ToString(v any) (string, bool) {
return "", false return "", false
} }
type ( type Tuple struct {
Strings2 [2]string First string
Strings3 [3]string Second string
) }

View file

@ -17,35 +17,24 @@ import (
"bytes" "bytes"
) )
// HasBytesWriter is a writer will match against a slice of patterns. // HasBytesWriter is a writer that will set Match to true if the given pattern
// is found in the stream.
type HasBytesWriter struct { type HasBytesWriter struct {
Patterns []*HasBytesPattern Match bool
Pattern []byte
i int i int
done bool done bool
buff []byte buff []byte
} }
type HasBytesPattern struct {
Match bool
Pattern []byte
}
func (h *HasBytesWriter) patternLen() int {
l := 0
for _, p := range h.Patterns {
l += len(p.Pattern)
}
return l
}
func (h *HasBytesWriter) Write(p []byte) (n int, err error) { func (h *HasBytesWriter) Write(p []byte) (n int, err error) {
if h.done { if h.done {
return len(p), nil return len(p), nil
} }
if len(h.buff) == 0 { if len(h.buff) == 0 {
h.buff = make([]byte, h.patternLen()*2) h.buff = make([]byte, len(h.Pattern)*2)
} }
for i := range p { for i := range p {
@ -57,24 +46,12 @@ func (h *HasBytesWriter) Write(p []byte) (n int, err error) {
h.i = len(h.buff) / 2 h.i = len(h.buff) / 2
} }
for _, pp := range h.Patterns { if bytes.Contains(h.buff, h.Pattern) {
if bytes.Contains(h.buff, pp.Pattern) { h.Match = true
pp.Match = true
done := true
for _, ppp := range h.Patterns {
if !ppp.Match {
done = false
break
}
}
if done {
h.done = true h.done = true
}
return len(p), nil return len(p), nil
} }
} }
}
return len(p), nil return len(p), nil
} }

View file

@ -34,11 +34,8 @@ func TestHasBytesWriter(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
h := &HasBytesWriter{ h := &HasBytesWriter{
Patterns: []*HasBytesPattern{ Pattern: []byte("__foo"),
{Pattern: []byte("__foo")},
},
} }
return h, io.MultiWriter(&b, h) return h, io.MultiWriter(&b, h)
} }
@ -46,22 +43,22 @@ func TestHasBytesWriter(t *testing.T) {
return strings.Repeat("ab cfo", r.Intn(33)) return strings.Repeat("ab cfo", r.Intn(33))
} }
for range 22 { for i := 0; i < 22; i++ {
h, w := neww() h, w := neww()
fmt.Fprint(w, rndStr()+"abc __foobar"+rndStr()) fmt.Fprintf(w, rndStr()+"abc __foobar"+rndStr())
c.Assert(h.Patterns[0].Match, qt.Equals, true) c.Assert(h.Match, qt.Equals, true)
h, w = neww() h, w = neww()
fmt.Fprint(w, rndStr()+"abc __f") fmt.Fprintf(w, rndStr()+"abc __f")
fmt.Fprint(w, "oo bar"+rndStr()) fmt.Fprintf(w, "oo bar"+rndStr())
c.Assert(h.Patterns[0].Match, qt.Equals, true) c.Assert(h.Match, qt.Equals, true)
h, w = neww() h, w = neww()
fmt.Fprint(w, rndStr()+"abc __moo bar") fmt.Fprintf(w, rndStr()+"abc __moo bar")
c.Assert(h.Patterns[0].Match, qt.Equals, false) c.Assert(h.Match, qt.Equals, false)
} }
h, w := neww() h, w := neww()
fmt.Fprintf(w, "__foo") fmt.Fprintf(w, "__foo")
c.Assert(h.Patterns[0].Match, qt.Equals, true) c.Assert(h.Match, qt.Equals, true)
} }

View file

@ -74,13 +74,13 @@ type StringReader interface {
ReadString() string ReadString() string
} }
// NewReadSeekerNoOpCloserFromBytes uses bytes.NewReader to create a new ReadSeekerNoOpCloser // NewReadSeekerNoOpCloserFromString uses strings.NewReader to create a new ReadSeekerNoOpCloser
// from the given bytes slice. // from the given bytes slice.
func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser { func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser {
return readSeekerNopCloser{bytes.NewReader(content)} return readSeekerNopCloser{bytes.NewReader(content)}
} }
// NewOpenReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker. // NewReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker.
// The ReadSeeker will be seeked to the beginning before returned. // The ReadSeeker will be seeked to the beginning before returned.
func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser { func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser {
return func() (ReadSeekCloser, error) { return func() (ReadSeekCloser, error) {

View file

@ -81,33 +81,3 @@ func ToReadCloser(r io.Reader) io.ReadCloser {
io.NopCloser(nil), io.NopCloser(nil),
} }
} }
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
// PipeReadWriteCloser is a convenience type to create a pipe with a ReadCloser and a WriteCloser.
type PipeReadWriteCloser struct {
*io.PipeReader
*io.PipeWriter
}
// NewPipeReadWriteCloser creates a new PipeReadWriteCloser.
func NewPipeReadWriteCloser() PipeReadWriteCloser {
pr, pw := io.Pipe()
return PipeReadWriteCloser{pr, pw}
}
func (c PipeReadWriteCloser) Close() (err error) {
if err = c.PipeReader.Close(); err != nil {
return
}
err = c.PipeWriter.Close()
return
}
func (c PipeReadWriteCloser) WriteString(s string) (int, error) {
return c.PipeWriter.Write([]byte(s))
}

View file

@ -14,7 +14,6 @@
package hugo package hugo
import ( import (
"context"
"fmt" "fmt"
"html/template" "html/template"
"os" "os"
@ -25,13 +24,13 @@ import (
"sync" "sync"
"time" "time"
godartsassv1 "github.com/bep/godartsass"
"github.com/bep/logg" "github.com/bep/logg"
"github.com/mitchellh/mapstructure"
"github.com/bep/godartsass/v2" "github.com/bep/godartsass/v2"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/files"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -54,8 +53,6 @@ var (
vendorInfo string vendorInfo string
) )
var _ maps.StoreProvider = (*HugoInfo)(nil)
// HugoInfo contains information about the current Hugo environment // HugoInfo contains information about the current Hugo environment
type HugoInfo struct { type HugoInfo struct {
CommitHash string CommitHash string
@ -72,11 +69,6 @@ type HugoInfo struct {
conf ConfigProvider conf ConfigProvider
deps []*Dependency deps []*Dependency
store *maps.Scratch
// Context gives access to some of the context scoped variables.
Context Context
} }
// Version returns the current version as a comparable version string. // Version returns the current version as a comparable version string.
@ -119,10 +111,6 @@ func (i HugoInfo) Deps() []*Dependency {
return i.deps return i.deps
} }
func (i HugoInfo) Store() *maps.Scratch {
return i.store
}
// Deprecated: Use hugo.IsMultihost instead. // Deprecated: Use hugo.IsMultihost instead.
func (i HugoInfo) IsMultiHost() bool { func (i HugoInfo) IsMultiHost() bool {
Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0") Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0")
@ -139,30 +127,6 @@ func (i HugoInfo) IsMultilingual() bool {
return i.conf.IsMultilingual() return i.conf.IsMultilingual()
} }
type contextKey uint8
const (
contextKeyMarkupScope contextKey = iota
)
var markupScope = hcontext.NewContextDispatcher[string](contextKeyMarkupScope)
type Context struct{}
func (c Context) MarkupScope(ctx context.Context) string {
return GetMarkupScope(ctx)
}
// SetMarkupScope sets the markup scope in the context.
func SetMarkupScope(ctx context.Context, s string) context.Context {
return markupScope.Set(ctx, s)
}
// GetMarkupScope gets the markup scope from the context.
func GetMarkupScope(ctx context.Context) string {
return markupScope.Get(ctx)
}
// ConfigProvider represents the config options that are relevant for HugoInfo. // ConfigProvider represents the config options that are relevant for HugoInfo.
type ConfigProvider interface { type ConfigProvider interface {
Environment() string Environment() string
@ -196,7 +160,6 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
Environment: conf.Environment(), Environment: conf.Environment(),
conf: conf, conf: conf,
deps: deps, deps: deps,
store: maps.NewScratch(),
GoVersion: goVersion, GoVersion: goVersion,
} }
} }
@ -313,14 +276,14 @@ func GetDependencyListNonGo() []string {
if IsExtended { if IsExtended {
deps = append( deps = append(
deps, deps,
formatDep("github.com/sass/libsass", "3.6.6"), formatDep("github.com/sass/libsass", "3.6.5"),
formatDep("github.com/webmproject/libwebp", "v1.3.2"), formatDep("github.com/webmproject/libwebp", "v1.3.2"),
) )
} }
if dartSass := dartSassVersion(); dartSass.ProtocolVersion != "" { if dartSass := dartSassVersion(); dartSass.ProtocolVersion != "" {
dartSassPath := "github.com/sass/dart-sass-embedded" dartSassPath := "github.com/sass/dart-sass-embedded"
if IsDartSassGeV2() { if IsDartSassV2() {
dartSassPath = "github.com/sass/dart-sass" dartSassPath = "github.com/sass/dart-sass"
} }
deps = append(deps, deps = append(deps,
@ -367,15 +330,22 @@ type Dependency struct {
} }
func dartSassVersion() godartsass.DartSassVersion { func dartSassVersion() godartsass.DartSassVersion {
if DartSassBinaryName == "" || !IsDartSassGeV2() { if DartSassBinaryName == "" {
return godartsass.DartSassVersion{} return godartsass.DartSassVersion{}
} }
if IsDartSassV2() {
v, _ := godartsass.Version(DartSassBinaryName) v, _ := godartsass.Version(DartSassBinaryName)
return v return v
} }
v, _ := godartsassv1.Version(DartSassBinaryName)
var vv godartsass.DartSassVersion
mapstructure.WeakDecode(v, &vv)
return vv
}
// DartSassBinaryName is the name of the Dart Sass binary to use. // DartSassBinaryName is the name of the Dart Sass binary to use.
// TODO(bep) find a better place for this. // TODO(beop) find a better place for this.
var DartSassBinaryName string var DartSassBinaryName string
func init() { func init() {
@ -400,10 +370,7 @@ var (
dartSassBinaryNamesV2 = []string{"dart-sass", "sass"} dartSassBinaryNamesV2 = []string{"dart-sass", "sass"}
) )
// TODO(bep) we eventually want to remove this, but keep it for a while to throw an informative error. func IsDartSassV2() bool {
// We stopped supporting the old binary in Hugo 0.139.0.
func IsDartSassGeV2() bool {
// dart-sass-embedded was the first version of the embedded Dart Sass before it was moved into the main project.
return !strings.Contains(DartSassBinaryName, "embedded") return !strings.Contains(DartSassBinaryName, "embedded")
} }
@ -415,39 +382,22 @@ func IsDartSassGeV2() bool {
// 2. Their theme to work for at least the last few Hugo versions. // 2. Their theme to work for at least the last few Hugo versions.
func Deprecate(item, alternative string, version string) { func Deprecate(item, alternative string, version string) {
level := deprecationLogLevelFromVersion(version) level := deprecationLogLevelFromVersion(version)
deprecateLevel(item, alternative, version, level) DeprecateLevel(item, alternative, version, level)
}
// See Deprecate for details.
func DeprecateWithLogger(item, alternative string, version string, log logg.Logger) {
level := deprecationLogLevelFromVersion(version)
deprecateLevelWithLogger(item, alternative, version, level, log)
}
// DeprecateLevelMin informs about a deprecation starting at the given version, but with a minimum log level.
func DeprecateLevelMin(item, alternative string, version string, minLevel logg.Level) {
level := max(deprecationLogLevelFromVersion(version), minLevel)
deprecateLevel(item, alternative, version, level)
}
// deprecateLevel informs about a deprecation logging at the given level.
func deprecateLevel(item, alternative, version string, level logg.Level) {
deprecateLevelWithLogger(item, alternative, version, level, loggers.Log().Logger())
} }
// DeprecateLevel informs about a deprecation logging at the given level. // DeprecateLevel informs about a deprecation logging at the given level.
func deprecateLevelWithLogger(item, alternative, version string, level logg.Level, log logg.Logger) { func DeprecateLevel(item, alternative, version string, level logg.Level) {
var msg string var msg string
if level == logg.LevelError { if level == logg.LevelError {
msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative) msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in Hugo %s. %s", item, version, CurrentVersion.Next().ReleaseVersion(), alternative)
} else { } else {
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative) msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
} }
log.WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf("%s", msg) loggers.Log().Logger().WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf(msg)
} }
// We usually do about one minor version a month. // We ususally do about one minor version a month.
// We want people to run at least the current and previous version without any warnings. // We want people to run at least the current and previous version without any warnings.
// We want people who don't update Hugo that often to see the warnings and errors before we remove the feature. // We want people who don't update Hugo that often to see the warnings and errors before we remove the feature.
func deprecationLogLevelFromVersion(ver string) logg.Level { func deprecationLogLevelFromVersion(ver string) logg.Level {
@ -455,11 +405,11 @@ func deprecationLogLevelFromVersion(ver string) logg.Level {
to := CurrentVersion to := CurrentVersion
minorDiff := to.Minor - from.Minor minorDiff := to.Minor - from.Minor
switch { switch {
case minorDiff >= 15: case minorDiff >= 12:
// Start failing the build after about 15 months. // Start failing the build after about a year.
return logg.LevelError return logg.LevelError
case minorDiff >= 3: case minorDiff >= 6:
// Start printing warnings after about 3 months. // Start printing warnings after about six months.
return logg.LevelWarn return logg.LevelWarn
default: default:
return logg.LevelInfo return logg.LevelInfo

View file

@ -14,7 +14,6 @@
package hugo package hugo
import ( import (
"context"
"fmt" "fmt"
"testing" "testing"
@ -57,29 +56,12 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError) c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError)
ver := CurrentVersion ver := CurrentVersion
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo) c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
ver.Minor -= 3 ver.Minor -= 1
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
ver.Minor -= 6
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn) c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
ver.Minor -= 4 ver.Minor -= 6
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
ver.Minor -= 13
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError) c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
// Added just to find the threshold for where we can remove deprecated items.
// Subtract 5 from the minor version of the first ERRORed version => 0.122.0.
c.Assert(deprecationLogLevelFromVersion("0.127.0"), qt.Equals, logg.LevelError)
}
func TestMarkupScope(t *testing.T) {
c := qt.New(t)
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
info := NewInfo(conf, nil)
ctx := context.Background()
ctx = SetMarkupScope(ctx, "foo")
c.Assert(info.Context.MarkupScope(ctx), qt.Equals, "foo")
} }
type testConfig struct { type testConfig struct {

View file

@ -12,6 +12,7 @@
// limitations under the License. // limitations under the License.
//go:build extended //go:build extended
// +build extended
package hugo package hugo

View file

@ -12,6 +12,7 @@
// limitations under the License. // limitations under the License.
//go:build !extended //go:build !extended
// +build !extended
package hugo package hugo

View file

@ -152,9 +152,6 @@ func BuildVersionString() string {
if IsExtended { if IsExtended {
version += "+extended" version += "+extended"
} }
if IsWithdeploy {
version += "+withdeploy"
}
osArch := bi.GoOS + "/" + bi.GoArch osArch := bi.GoOS + "/" + bi.GoArch

View file

@ -17,7 +17,7 @@ package hugo
// This should be the only one. // This should be the only one.
var CurrentVersion = Version{ var CurrentVersion = Version{
Major: 0, Major: 0,
Minor: 148, Minor: 126,
PatchLevel: 0, PatchLevel: 2,
Suffix: "-DEV", Suffix: "",
} }

View file

@ -21,7 +21,7 @@ import (
"sync" "sync"
"github.com/bep/logg" "github.com/bep/logg"
"github.com/gohugoio/hugo/common/hashing" "github.com/gohugoio/hugo/identity"
) )
// PanicOnWarningHook panics on warnings. // PanicOnWarningHook panics on warnings.
@ -85,7 +85,7 @@ func (h *logOnceHandler) HandleLog(e *logg.Entry) error {
} }
h.mu.Lock() h.mu.Lock()
defer h.mu.Unlock() defer h.mu.Unlock()
hash := hashing.HashUint64(e.Level, e.Message, e.Fields) hash := identity.HashUint64(e.Level, e.Message, e.Fields)
if h.seen[hash] { if h.seen[hash] {
return errStop return errStop
} }

View file

@ -18,19 +18,18 @@ package loggers
import ( import (
"fmt" "fmt"
"io" "io"
"regexp"
"strings" "strings"
"sync" "sync"
"github.com/bep/logg" "github.com/bep/logg"
) )
// newNoAnsiEscapeHandler creates a new noAnsiEscapeHandler // newNoColoursHandler creates a new NoColoursHandler
func newNoAnsiEscapeHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noAnsiEscapeHandler { func newNoColoursHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noColoursHandler {
if predicate == nil { if predicate == nil {
predicate = func(e *logg.Entry) bool { return true } predicate = func(e *logg.Entry) bool { return true }
} }
return &noAnsiEscapeHandler{ return &noColoursHandler{
noLevelPrefix: noLevelPrefix, noLevelPrefix: noLevelPrefix,
outWriter: outWriter, outWriter: outWriter,
errWriter: errWriter, errWriter: errWriter,
@ -38,15 +37,15 @@ func newNoAnsiEscapeHandler(outWriter, errWriter io.Writer, noLevelPrefix bool,
} }
} }
type noAnsiEscapeHandler struct { type noColoursHandler struct {
mu sync.Mutex mu sync.Mutex
outWriter io.Writer outWriter io.Writer // Defaults to os.Stdout.
errWriter io.Writer errWriter io.Writer // Defaults to os.Stderr.
predicate func(*logg.Entry) bool predicate func(*logg.Entry) bool
noLevelPrefix bool noLevelPrefix bool
} }
func (h *noAnsiEscapeHandler) HandleLog(e *logg.Entry) error { func (h *noColoursHandler) HandleLog(e *logg.Entry) error {
if !h.predicate(e) { if !h.predicate(e) {
return nil return nil
} }
@ -72,12 +71,10 @@ func (h *noAnsiEscapeHandler) HandleLog(e *logg.Entry) error {
prefix = prefix + ": " prefix = prefix + ": "
} }
msg := stripANSI(e.Message)
if h.noLevelPrefix { if h.noLevelPrefix {
fmt.Fprintf(w, "%s%s", prefix, msg) fmt.Fprintf(w, "%s%s", prefix, e.Message)
} else { } else {
fmt.Fprintf(w, "%s %s%s", levelString[e.Level], prefix, msg) fmt.Fprintf(w, "%s %s%s", levelString[e.Level], prefix, e.Message)
} }
for _, field := range e.Fields { for _, field := range e.Fields {
@ -91,10 +88,3 @@ func (h *noAnsiEscapeHandler) HandleLog(e *logg.Entry) error {
return nil return nil
} }
var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*m`)
// stripANSI removes ANSI escape codes from s.
func stripANSI(s string) string {
return ansiRe.ReplaceAllString(s, "")
}

View file

@ -1,40 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Some functions in this file (see comments) is based on the Go source code,
// copyright The Go Authors and governed by a BSD-style license.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package loggers
import (
"bytes"
"testing"
"github.com/bep/logg"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/terminal"
)
func TestNoAnsiEscapeHandler(t *testing.T) {
c := qt.New(t)
test := func(s string) {
c.Assert(stripANSI(terminal.Notice(s)), qt.Equals, s)
}
test(`error in "file.md:1:2"`)
var buf bytes.Buffer
h := newNoAnsiEscapeHandler(&buf, &buf, false, nil)
h.HandleLog(&logg.Entry{Message: terminal.Notice(`error in "file.md:1:2"`), Level: logg.LevelInfo})
c.Assert(buf.String(), qt.Equals, "INFO error in \"file.md:1:2\"\n")
}

View file

@ -38,8 +38,8 @@ var (
// Options defines options for the logger. // Options defines options for the logger.
type Options struct { type Options struct {
Level logg.Level Level logg.Level
StdOut io.Writer Stdout io.Writer
StdErr io.Writer Stderr io.Writer
DistinctLevel logg.Level DistinctLevel logg.Level
StoreErrors bool StoreErrors bool
HandlerPost func(e *logg.Entry) error HandlerPost func(e *logg.Entry) error
@ -48,22 +48,21 @@ type Options struct {
// New creates a new logger with the given options. // New creates a new logger with the given options.
func New(opts Options) Logger { func New(opts Options) Logger {
if opts.StdOut == nil { if opts.Stdout == nil {
opts.StdOut = os.Stdout opts.Stdout = os.Stdout
} }
if opts.StdErr == nil { if opts.Stderr == nil {
opts.StdErr = os.Stderr opts.Stderr = os.Stdout
} }
if opts.Level == 0 { if opts.Level == 0 {
opts.Level = logg.LevelWarn opts.Level = logg.LevelWarn
} }
var logHandler logg.Handler var logHandler logg.Handler
if terminal.PrintANSIColors(os.Stderr) { if terminal.PrintANSIColors(os.Stdout) {
logHandler = newDefaultHandler(opts.StdErr, opts.StdErr) logHandler = newDefaultHandler(opts.Stdout, opts.Stderr)
} else { } else {
logHandler = newNoAnsiEscapeHandler(opts.StdErr, opts.StdErr, false, nil) logHandler = newNoColoursHandler(opts.Stdout, opts.Stderr, false, nil)
} }
errorsw := &strings.Builder{} errorsw := &strings.Builder{}
@ -96,7 +95,7 @@ func New(opts Options) Logger {
} }
if opts.StoreErrors { if opts.StoreErrors {
h := newNoAnsiEscapeHandler(io.Discard, errorsw, true, func(e *logg.Entry) bool { h := newNoColoursHandler(io.Discard, errorsw, true, func(e *logg.Entry) bool {
return e.Level >= logg.LevelError return e.Level >= logg.LevelError
}) })
@ -111,7 +110,7 @@ func New(opts Options) Logger {
logHandler = newStopHandler(logOnce, logHandler) logHandler = newStopHandler(logOnce, logHandler)
} }
if len(opts.SuppressStatements) > 0 { if opts.SuppressStatements != nil && len(opts.SuppressStatements) > 0 {
logHandler = newStopHandler(newSuppressStatementsHandler(opts.SuppressStatements), logHandler) logHandler = newStopHandler(newSuppressStatementsHandler(opts.SuppressStatements), logHandler)
} }
@ -138,8 +137,7 @@ func New(opts Options) Logger {
logCounters: logCounters, logCounters: logCounters,
errors: errorsw, errors: errorsw,
reset: reset, reset: reset,
stdOut: opts.StdOut, out: opts.Stdout,
stdErr: opts.StdErr,
level: opts.Level, level: opts.Level,
logger: logger, logger: logger,
tracel: l.WithLevel(logg.LevelTrace), tracel: l.WithLevel(logg.LevelTrace),
@ -155,6 +153,8 @@ func NewDefault() Logger {
opts := Options{ opts := Options{
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
Level: logg.LevelWarn, Level: logg.LevelWarn,
Stdout: os.Stdout,
Stderr: os.Stdout,
} }
return New(opts) return New(opts)
} }
@ -163,6 +163,8 @@ func NewTrace() Logger {
opts := Options{ opts := Options{
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
Level: logg.LevelTrace, Level: logg.LevelTrace,
Stdout: os.Stdout,
Stderr: os.Stdout,
} }
return New(opts) return New(opts)
} }
@ -187,8 +189,7 @@ type Logger interface {
Level() logg.Level Level() logg.Level
LoggCount(logg.Level) int LoggCount(logg.Level) int
Logger() logg.Logger Logger() logg.Logger
StdOut() io.Writer Out() io.Writer
StdErr() io.Writer
Printf(format string, v ...any) Printf(format string, v ...any)
Println(v ...any) Println(v ...any)
PrintTimerIfDelayed(start time.Time, name string) PrintTimerIfDelayed(start time.Time, name string)
@ -206,8 +207,7 @@ type logAdapter struct {
logCounters *logLevelCounter logCounters *logLevelCounter
errors *strings.Builder errors *strings.Builder
reset func() reset func()
stdOut io.Writer out io.Writer
stdErr io.Writer
level logg.Level level logg.Level
logger logg.Logger logger logg.Logger
tracel logg.LevelLogger tracel logg.LevelLogger
@ -259,12 +259,8 @@ func (l *logAdapter) Logger() logg.Logger {
return l.logger return l.logger
} }
func (l *logAdapter) StdOut() io.Writer { func (l *logAdapter) Out() io.Writer {
return l.stdOut return l.out
}
func (l *logAdapter) StdErr() io.Writer {
return l.stdErr
} }
// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger // PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
@ -275,7 +271,7 @@ func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
if milli < 500 { if milli < 500 {
return return
} }
fmt.Fprintf(l.stdErr, "%s in %v ms", name, milli) l.Printf("%s in %v ms", name, milli)
} }
func (l *logAdapter) Printf(format string, v ...any) { func (l *logAdapter) Printf(format string, v ...any) {
@ -283,11 +279,11 @@ func (l *logAdapter) Printf(format string, v ...any) {
if !strings.HasSuffix(format, "\n") { if !strings.HasSuffix(format, "\n") {
format += "\n" format += "\n"
} }
fmt.Fprintf(l.stdOut, format, v...) fmt.Fprintf(l.out, format, v...)
} }
func (l *logAdapter) Println(v ...any) { func (l *logAdapter) Println(v ...any) {
fmt.Fprintln(l.stdOut, v...) fmt.Fprintln(l.out, v...)
} }
func (l *logAdapter) Reset() { func (l *logAdapter) Reset() {
@ -327,13 +323,11 @@ func (l *logAdapter) Errors() string {
} }
func (l *logAdapter) Erroridf(id, format string, v ...any) { func (l *logAdapter) Erroridf(id, format string, v ...any) {
id = strings.ToLower(id)
format += l.idfInfoStatement("error", id, format) format += l.idfInfoStatement("error", id, format)
l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...) l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...)
} }
func (l *logAdapter) Warnidf(id, format string, v ...any) { func (l *logAdapter) Warnidf(id, format string, v ...any) {
id = strings.ToLower(id)
format += l.idfInfoStatement("warning", id, format) format += l.idfInfoStatement("warning", id, format)
l.warnl.WithField(FieldNameStatementID, id).Logf(format, v...) l.warnl.WithField(FieldNameStatementID, id).Logf(format, v...)
} }

View file

@ -31,13 +31,13 @@ func TestLogDistinct(t *testing.T) {
opts := loggers.Options{ opts := loggers.Options{
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
StoreErrors: true, StoreErrors: true,
StdOut: io.Discard, Stdout: io.Discard,
StdErr: io.Discard, Stderr: io.Discard,
} }
l := loggers.New(opts) l := loggers.New(opts)
for range 10 { for i := 0; i < 10; i++ {
l.Errorln("error 1") l.Errorln("error 1")
l.Errorln("error 2") l.Errorln("error 2")
l.Warnln("warn 1") l.Warnln("warn 1")
@ -54,8 +54,8 @@ func TestHookLast(t *testing.T) {
HandlerPost: func(e *logg.Entry) error { HandlerPost: func(e *logg.Entry) error {
panic(e.Message) panic(e.Message)
}, },
StdOut: io.Discard, Stdout: io.Discard,
StdErr: io.Discard, Stderr: io.Discard,
} }
l := loggers.New(opts) l := loggers.New(opts)
@ -70,8 +70,8 @@ func TestOptionStoreErrors(t *testing.T) {
opts := loggers.Options{ opts := loggers.Options{
StoreErrors: true, StoreErrors: true,
StdErr: &sb, Stderr: &sb,
StdOut: &sb, Stdout: &sb,
} }
l := loggers.New(opts) l := loggers.New(opts)
@ -131,13 +131,13 @@ func TestReset(t *testing.T) {
opts := loggers.Options{ opts := loggers.Options{
StoreErrors: true, StoreErrors: true,
DistinctLevel: logg.LevelWarn, DistinctLevel: logg.LevelWarn,
StdOut: io.Discard, Stdout: io.Discard,
StdErr: io.Discard, Stderr: io.Discard,
} }
l := loggers.New(opts) l := loggers.New(opts)
for range 3 { for i := 0; i < 3; i++ {
l.Errorln("error 1") l.Errorln("error 1")
l.Errorln("error 2") l.Errorln("error 2")
l.Errorln("error 1") l.Errorln("error 1")

View file

@ -21,15 +21,7 @@ import (
"github.com/bep/logg" "github.com/bep/logg"
) )
// SetGlobalLogger sets the global logger. func InitGlobalLogger(level logg.Level, panicOnWarnings bool) {
// This is used in a few places in Hugo, e.g. deprecated functions.
func SetGlobalLogger(logger Logger) {
logMu.Lock()
defer logMu.Unlock()
log = logger
}
func initGlobalLogger(level logg.Level, panicOnWarnings bool) {
logMu.Lock() logMu.Lock()
defer logMu.Unlock() defer logMu.Unlock()
var logHookLast func(e *logg.Entry) error var logHookLast func(e *logg.Entry) error
@ -58,5 +50,5 @@ func Log() Logger {
var log Logger var log Logger
func init() { func init() {
initGlobalLogger(logg.LevelWarn, false) InitGlobalLogger(logg.LevelWarn, false)
} }

View file

@ -13,14 +13,11 @@
package maps package maps
import ( import "sync"
"sync"
)
// Cache is a simple thread safe cache backed by a map. // Cache is a simple thread safe cache backed by a map.
type Cache[K comparable, T any] struct { type Cache[K comparable, T any] struct {
m map[K]T m map[K]T
hasBeenInitialized bool
sync.RWMutex sync.RWMutex
} }
@ -37,133 +34,45 @@ func (c *Cache[K, T]) Get(key K) (T, bool) {
return zero, false return zero, false
} }
c.RLock() c.RLock()
v, found := c.get(key)
c.RUnlock()
return v, found
}
func (c *Cache[K, T]) get(key K) (T, bool) {
v, found := c.m[key] v, found := c.m[key]
c.RUnlock()
return v, found return v, found
} }
// GetOrCreate gets the value for the given key if it exists, or creates it if not. // GetOrCreate gets the value for the given key if it exists, or creates it if not.
func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) { func (c *Cache[K, T]) GetOrCreate(key K, create func() T) T {
c.RLock() c.RLock()
v, found := c.m[key] v, found := c.m[key]
c.RUnlock() c.RUnlock()
if found { if found {
return v, nil return v
} }
c.Lock() c.Lock()
defer c.Unlock() defer c.Unlock()
v, found = c.m[key] v, found = c.m[key]
if found { if found {
return v, nil return v
}
v, err := create()
if err != nil {
return v, err
} }
v = create()
c.m[key] = v c.m[key] = v
return v, nil return v
}
// Contains returns whether the given key exists in the cache.
func (c *Cache[K, T]) Contains(key K) bool {
c.RLock()
_, found := c.m[key]
c.RUnlock()
return found
}
// InitAndGet initializes the cache if not already done and returns the value for the given key.
// The init state will be reset on Reset or Drain.
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
var v T
c.RLock()
if !c.hasBeenInitialized {
c.RUnlock()
if err := func() error {
c.Lock()
defer c.Unlock()
// Double check in case another goroutine has initialized it in the meantime.
if !c.hasBeenInitialized {
err := init(c.get, c.set)
if err != nil {
return err
}
c.hasBeenInitialized = true
}
return nil
}(); err != nil {
return v, err
}
// Reacquire the read lock.
c.RLock()
}
v = c.m[key]
c.RUnlock()
return v, nil
} }
// Set sets the given key to the given value. // Set sets the given key to the given value.
func (c *Cache[K, T]) Set(key K, value T) { func (c *Cache[K, T]) Set(key K, value T) {
c.Lock() c.Lock()
c.set(key, value)
c.Unlock()
}
// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
c.RLock()
if _, found := c.get(key); !found {
c.RUnlock()
c.Set(key, value)
} else {
c.RUnlock()
}
}
func (c *Cache[K, T]) set(key K, value T) {
c.m[key] = value c.m[key] = value
c.Unlock()
} }
// ForEeach calls the given function for each key/value pair in the cache. // ForEeach calls the given function for each key/value pair in the cache.
// If the function returns false, the iteration stops. func (c *Cache[K, T]) ForEeach(f func(K, T)) {
func (c *Cache[K, T]) ForEeach(f func(K, T) bool) {
c.RLock() c.RLock()
defer c.RUnlock() defer c.RUnlock()
for k, v := range c.m { for k, v := range c.m {
if !f(k, v) { f(k, v)
return
} }
} }
}
func (c *Cache[K, T]) Drain() map[K]T {
c.Lock()
m := c.m
c.m = make(map[K]T)
c.hasBeenInitialized = false
c.Unlock()
return m
}
func (c *Cache[K, T]) Len() int {
c.RLock()
defer c.RUnlock()
return len(c.m)
}
func (c *Cache[K, T]) Reset() {
c.Lock()
clear(c.m)
c.hasBeenInitialized = false
c.Unlock()
}
// SliceCache is a simple thread safe cache backed by a map. // SliceCache is a simple thread safe cache backed by a map.
type SliceCache[T any] struct { type SliceCache[T any] struct {

View file

@ -112,17 +112,17 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
} }
// LookupEqualFold finds key in m with case insensitive equality checks. // LookupEqualFold finds key in m with case insensitive equality checks.
func LookupEqualFold[T any | string](m map[string]T, key string) (T, string, bool) { func LookupEqualFold[T any | string](m map[string]T, key string) (T, bool) {
if v, found := m[key]; found { if v, found := m[key]; found {
return v, key, true return v, true
} }
for k, v := range m { for k, v := range m {
if strings.EqualFold(k, key) { if strings.EqualFold(k, key) {
return v, k, true return v, true
} }
} }
var s T var s T
return s, "", false return s, false
} }
// MergeShallow merges src into dst, but only if the key does not already exist in dst. // MergeShallow merges src into dst, but only if the key does not already exist in dst.

View file

@ -73,14 +73,10 @@ func TestPrepareParams(t *testing.T) {
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) { t.Run(fmt.Sprint(i), func(t *testing.T) {
// PrepareParams modifies input. // PrepareParams modifies input.
prepareClone := PrepareParamsClone(test.input)
PrepareParams(test.input) PrepareParams(test.input)
if !reflect.DeepEqual(test.expected, test.input) { if !reflect.DeepEqual(test.expected, test.input) {
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input) t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
} }
if !reflect.DeepEqual(test.expected, prepareClone) {
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, prepareClone)
}
}) })
} }
} }
@ -184,18 +180,16 @@ func TestLookupEqualFold(t *testing.T) {
"B": "bv", "B": "bv",
} }
v, k, found := LookupEqualFold(m1, "b") v, found := LookupEqualFold(m1, "b")
c.Assert(found, qt.IsTrue) c.Assert(found, qt.IsTrue)
c.Assert(v, qt.Equals, "bv") c.Assert(v, qt.Equals, "bv")
c.Assert(k, qt.Equals, "B")
m2 := map[string]string{ m2 := map[string]string{
"a": "av", "a": "av",
"B": "bv", "B": "bv",
} }
v, k, found = LookupEqualFold(m2, "b") v, found = LookupEqualFold(m2, "b")
c.Assert(found, qt.IsTrue) c.Assert(found, qt.IsTrue)
c.Assert(k, qt.Equals, "B")
c.Assert(v, qt.Equals, "bv") c.Assert(v, qt.Equals, "bv")
} }

View file

@ -1,144 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"slices"
"github.com/gohugoio/hugo/common/hashing"
)
// Ordered is a map that can be iterated in the order of insertion.
// Note that insertion order is not affected if a key is re-inserted into the map.
// In a nil map, all operations are no-ops.
// This is not thread safe.
type Ordered[K comparable, T any] struct {
// The keys in the order they were added.
keys []K
// The values.
values map[K]T
}
// NewOrdered creates a new Ordered map.
func NewOrdered[K comparable, T any]() *Ordered[K, T] {
return &Ordered[K, T]{values: make(map[K]T)}
}
// Set sets the value for the given key.
// Note that insertion order is not affected if a key is re-inserted into the map.
func (m *Ordered[K, T]) Set(key K, value T) {
if m == nil {
return
}
// Check if key already exists.
if _, found := m.values[key]; !found {
m.keys = append(m.keys, key)
}
m.values[key] = value
}
// Get gets the value for the given key.
func (m *Ordered[K, T]) Get(key K) (T, bool) {
if m == nil {
var v T
return v, false
}
value, found := m.values[key]
return value, found
}
// Has returns whether the given key exists in the map.
func (m *Ordered[K, T]) Has(key K) bool {
if m == nil {
return false
}
_, found := m.values[key]
return found
}
// Delete deletes the value for the given key.
func (m *Ordered[K, T]) Delete(key K) {
if m == nil {
return
}
delete(m.values, key)
for i, k := range m.keys {
if k == key {
m.keys = slices.Delete(m.keys, i, i+1)
break
}
}
}
// Clone creates a shallow copy of the map.
func (m *Ordered[K, T]) Clone() *Ordered[K, T] {
if m == nil {
return nil
}
clone := NewOrdered[K, T]()
for _, k := range m.keys {
clone.Set(k, m.values[k])
}
return clone
}
// Keys returns the keys in the order they were added.
func (m *Ordered[K, T]) Keys() []K {
if m == nil {
return nil
}
return m.keys
}
// Values returns the values in the order they were added.
func (m *Ordered[K, T]) Values() []T {
if m == nil {
return nil
}
var values []T
for _, k := range m.keys {
values = append(values, m.values[k])
}
return values
}
// Len returns the number of items in the map.
func (m *Ordered[K, T]) Len() int {
if m == nil {
return 0
}
return len(m.keys)
}
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
// TODO(bep) replace with iter.Seq2 when we bump go Go 1.24.
func (m *Ordered[K, T]) Range(f func(key K, value T) bool) {
if m == nil {
return
}
for _, k := range m.keys {
if !f(k, m.values[k]) {
return
}
}
}
// Hash calculates a hash from the values.
func (m *Ordered[K, T]) Hash() (uint64, error) {
if m == nil {
return 0, nil
}
return hashing.Hash(m.values)
}

View file

@ -1,99 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestOrdered(t *testing.T) {
c := qt.New(t)
m := NewOrdered[string, int]()
m.Set("a", 1)
m.Set("b", 2)
m.Set("c", 3)
c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(m.Values(), qt.DeepEquals, []int{1, 2, 3})
v, found := m.Get("b")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 2)
m.Set("b", 22)
c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(m.Values(), qt.DeepEquals, []int{1, 22, 3})
m.Delete("b")
c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "c"})
c.Assert(m.Values(), qt.DeepEquals, []int{1, 3})
}
func TestOrderedHash(t *testing.T) {
c := qt.New(t)
m := NewOrdered[string, int]()
m.Set("a", 1)
m.Set("b", 2)
m.Set("c", 3)
h1, err := m.Hash()
c.Assert(err, qt.IsNil)
m.Set("d", 4)
h2, err := m.Hash()
c.Assert(err, qt.IsNil)
c.Assert(h1, qt.Not(qt.Equals), h2)
m = NewOrdered[string, int]()
m.Set("b", 2)
m.Set("a", 1)
m.Set("c", 3)
h3, err := m.Hash()
c.Assert(err, qt.IsNil)
// Order does not matter.
c.Assert(h1, qt.Equals, h3)
}
func TestOrderedNil(t *testing.T) {
c := qt.New(t)
var m *Ordered[string, int]
m.Set("a", 1)
c.Assert(m.Keys(), qt.IsNil)
c.Assert(m.Values(), qt.IsNil)
v, found := m.Get("a")
c.Assert(found, qt.Equals, false)
c.Assert(v, qt.Equals, 0)
m.Delete("a")
var b bool
m.Range(func(k string, v int) bool {
b = true
return true
})
c.Assert(b, qt.Equals, false)
c.Assert(m.Len(), qt.Equals, 0)
c.Assert(m.Clone(), qt.IsNil)
h, err := m.Hash()
c.Assert(err, qt.IsNil)
c.Assert(h, qt.Equals, uint64(0))
}

View file

@ -303,7 +303,7 @@ func toMergeStrategy(v any) ParamsMergeStrategy {
} }
// PrepareParams // PrepareParams
// * makes all the keys in the given map lower cased and will do so recursively. // * makes all the keys in the given map lower cased and will do so
// * This will modify the map given. // * This will modify the map given.
// * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params. // * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params.
// * Any _merge value will be converted to proper type and value. // * Any _merge value will be converted to proper type and value.
@ -343,42 +343,3 @@ func PrepareParams(m Params) {
} }
} }
} }
// PrepareParamsClone is like PrepareParams, but it does not modify the input.
func PrepareParamsClone(m Params) Params {
m2 := make(Params)
for k, v := range m {
var retyped bool
lKey := strings.ToLower(k)
if lKey == MergeStrategyKey {
v = toMergeStrategy(v)
retyped = true
} else {
switch vv := v.(type) {
case map[any]any:
var p Params = cast.ToStringMap(v)
v = PrepareParamsClone(p)
retyped = true
case map[string]any:
var p Params = v.(map[string]any)
v = PrepareParamsClone(p)
retyped = true
case map[string]string:
p := make(Params)
for k, v := range vv {
p[k] = v
}
v = p
PrepareParams(p)
retyped = true
}
}
if retyped || k != lKey {
m2[lKey] = v
} else {
m2[k] = v
}
}
return m2
}

View file

@ -22,18 +22,31 @@ import (
"github.com/gohugoio/hugo/common/math" "github.com/gohugoio/hugo/common/math"
) )
type StoreProvider interface { // Scratch is a writable context used for stateful operations in Page/Node rendering.
// Store returns a Scratch that can be used to store temporary state.
// Store is not reset on server rebuilds.
Store() *Scratch
}
// Scratch is a writable context used for stateful build operations
type Scratch struct { type Scratch struct {
values map[string]any values map[string]any
mu sync.RWMutex mu sync.RWMutex
} }
// Scratcher provides a scratching service.
type Scratcher interface {
// Scratch returns a "scratch pad" that can be used to store state.
Scratch() *Scratch
}
type scratcher struct {
s *Scratch
}
func (s scratcher) Scratch() *Scratch {
return s.s
}
// NewScratcher creates a new Scratcher.
func NewScratcher() Scratcher {
return scratcher{s: NewScratch()}
}
// Add will, for single values, add (using the + operator) the addend to the existing addend (if found). // Add will, for single values, add (using the + operator) the addend to the existing addend (if found).
// Supports numeric values and strings. // Supports numeric values and strings.
// //

View file

@ -140,7 +140,7 @@ func TestScratchInParallel(t *testing.T) {
for i := 1; i <= 10; i++ { for i := 1; i <= 10; i++ {
wg.Add(1) wg.Add(1)
go func(j int) { go func(j int) {
for k := range 10 { for k := 0; k < 10; k++ {
newVal := int64(k + j) newVal := int64(k + j)
_, err := scratch.Add(key, newVal) _, err := scratch.Add(key, newVal)
@ -185,7 +185,7 @@ func TestScratchSetInMap(t *testing.T) {
scratch.SetInMap("key", "zyx", "Zyx") scratch.SetInMap("key", "zyx", "Zyx")
scratch.SetInMap("key", "abc", "Abc (updated)") scratch.SetInMap("key", "abc", "Abc (updated)")
scratch.SetInMap("key", "def", "Def") scratch.SetInMap("key", "def", "Def")
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, any([]any{"Abc (updated)", "Def", "Lux", "Zyx"})) c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []any{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"})
} }
func TestScratchDeleteInMap(t *testing.T) { func TestScratchDeleteInMap(t *testing.T) {
@ -199,7 +199,7 @@ func TestScratchDeleteInMap(t *testing.T) {
scratch.DeleteInMap("key", "abc") scratch.DeleteInMap("key", "abc")
scratch.SetInMap("key", "def", "Def") scratch.SetInMap("key", "def", "Def")
scratch.DeleteInMap("key", "lmn") // Do nothing scratch.DeleteInMap("key", "lmn") // Do nothing
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, any([]any{"Def", "Lux", "Zyx"})) c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []any{0: "Def", 1: "Lux", 2: "Zyx"})
} }
func TestScratchGetSortedMapValues(t *testing.T) { func TestScratchGetSortedMapValues(t *testing.T) {

View file

@ -26,32 +26,29 @@ func DoArithmetic(a, b any, op rune) (any, error) {
var ai, bi int64 var ai, bi int64
var af, bf float64 var af, bf float64
var au, bu uint64 var au, bu uint64
var isInt, isFloat, isUint bool
switch av.Kind() { switch av.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
ai = av.Int() ai = av.Int()
switch bv.Kind() { switch bv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
isInt = true
bi = bv.Int() bi = bv.Int()
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
isFloat = true
af = float64(ai) // may overflow af = float64(ai) // may overflow
ai = 0
bf = bv.Float() bf = bv.Float()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
bu = bv.Uint() bu = bv.Uint()
if ai >= 0 { if ai >= 0 {
isUint = true
au = uint64(ai) au = uint64(ai)
ai = 0
} else { } else {
isInt = true
bi = int64(bu) // may overflow bi = int64(bu) // may overflow
bu = 0
} }
default: default:
return nil, errors.New("can't apply the operator to the values") return nil, errors.New("can't apply the operator to the values")
} }
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
isFloat = true
af = av.Float() af = av.Float()
switch bv.Kind() { switch bv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
@ -69,18 +66,17 @@ func DoArithmetic(a, b any, op rune) (any, error) {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
bi = bv.Int() bi = bv.Int()
if bi >= 0 { if bi >= 0 {
isUint = true
bu = uint64(bi) bu = uint64(bi)
bi = 0
} else { } else {
isInt = true
ai = int64(au) // may overflow ai = int64(au) // may overflow
au = 0
} }
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
isFloat = true
af = float64(au) // may overflow af = float64(au) // may overflow
au = 0
bf = bv.Float() bf = bv.Float()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
isUint = true
bu = bv.Uint() bu = bv.Uint()
default: default:
return nil, errors.New("can't apply the operator to the values") return nil, errors.New("can't apply the operator to the values")
@ -98,32 +94,38 @@ func DoArithmetic(a, b any, op rune) (any, error) {
switch op { switch op {
case '+': case '+':
if isInt { if ai != 0 || bi != 0 {
return ai + bi, nil return ai + bi, nil
} else if isFloat { } else if af != 0 || bf != 0 {
return af + bf, nil return af + bf, nil
} } else if au != 0 || bu != 0 {
return au + bu, nil return au + bu, nil
}
return 0, nil
case '-': case '-':
if isInt { if ai != 0 || bi != 0 {
return ai - bi, nil return ai - bi, nil
} else if isFloat { } else if af != 0 || bf != 0 {
return af - bf, nil return af - bf, nil
} } else if au != 0 || bu != 0 {
return au - bu, nil return au - bu, nil
case '*':
if isInt {
return ai * bi, nil
} else if isFloat {
return af * bf, nil
} }
return 0, nil
case '*':
if ai != 0 || bi != 0 {
return ai * bi, nil
} else if af != 0 || bf != 0 {
return af * bf, nil
} else if au != 0 || bu != 0 {
return au * bu, nil return au * bu, nil
}
return 0, nil
case '/': case '/':
if isInt && bi != 0 { if bi != 0 {
return ai / bi, nil return ai / bi, nil
} else if isFloat && bf != 0 { } else if bf != 0 {
return af / bf, nil return af / bf, nil
} else if isUint && bu != 0 { } else if bu != 0 {
return au / bu, nil return au / bu, nil
} }
return nil, errors.New("can't divide the value by 0") return nil, errors.New("can't divide the value by 0")

View file

@ -30,12 +30,10 @@ func TestDoArithmetic(t *testing.T) {
expect any expect any
}{ }{
{3, 2, '+', int64(5)}, {3, 2, '+', int64(5)},
{0, 0, '+', int64(0)},
{3, 2, '-', int64(1)}, {3, 2, '-', int64(1)},
{3, 2, '*', int64(6)}, {3, 2, '*', int64(6)},
{3, 2, '/', int64(1)}, {3, 2, '/', int64(1)},
{3.0, 2, '+', float64(5)}, {3.0, 2, '+', float64(5)},
{0.0, 0, '+', float64(0.0)},
{3.0, 2, '-', float64(1)}, {3.0, 2, '-', float64(1)},
{3.0, 2, '*', float64(6)}, {3.0, 2, '*', float64(6)},
{3.0, 2, '/', float64(1.5)}, {3.0, 2, '/', float64(1.5)},
@ -44,22 +42,18 @@ func TestDoArithmetic(t *testing.T) {
{3, 2.0, '*', float64(6)}, {3, 2.0, '*', float64(6)},
{3, 2.0, '/', float64(1.5)}, {3, 2.0, '/', float64(1.5)},
{3.0, 2.0, '+', float64(5)}, {3.0, 2.0, '+', float64(5)},
{0.0, 0.0, '+', float64(0.0)},
{3.0, 2.0, '-', float64(1)}, {3.0, 2.0, '-', float64(1)},
{3.0, 2.0, '*', float64(6)}, {3.0, 2.0, '*', float64(6)},
{3.0, 2.0, '/', float64(1.5)}, {3.0, 2.0, '/', float64(1.5)},
{uint(3), uint(2), '+', uint64(5)}, {uint(3), uint(2), '+', uint64(5)},
{uint(0), uint(0), '+', uint64(0)},
{uint(3), uint(2), '-', uint64(1)}, {uint(3), uint(2), '-', uint64(1)},
{uint(3), uint(2), '*', uint64(6)}, {uint(3), uint(2), '*', uint64(6)},
{uint(3), uint(2), '/', uint64(1)}, {uint(3), uint(2), '/', uint64(1)},
{uint(3), 2, '+', uint64(5)}, {uint(3), 2, '+', uint64(5)},
{uint(0), 0, '+', uint64(0)},
{uint(3), 2, '-', uint64(1)}, {uint(3), 2, '-', uint64(1)},
{uint(3), 2, '*', uint64(6)}, {uint(3), 2, '*', uint64(6)},
{uint(3), 2, '/', uint64(1)}, {uint(3), 2, '/', uint64(1)},
{3, uint(2), '+', uint64(5)}, {3, uint(2), '+', uint64(5)},
{0, uint(0), '+', uint64(0)},
{3, uint(2), '-', uint64(1)}, {3, uint(2), '-', uint64(1)},
{3, uint(2), '*', uint64(6)}, {3, uint(2), '*', uint64(6)},
{3, uint(2), '/', uint64(1)}, {3, uint(2), '/', uint64(1)},
@ -72,15 +66,16 @@ func TestDoArithmetic(t *testing.T) {
{-3, uint(2), '*', int64(-6)}, {-3, uint(2), '*', int64(-6)},
{-3, uint(2), '/', int64(-1)}, {-3, uint(2), '/', int64(-1)},
{uint(3), 2.0, '+', float64(5)}, {uint(3), 2.0, '+', float64(5)},
{uint(0), 0.0, '+', float64(0)},
{uint(3), 2.0, '-', float64(1)}, {uint(3), 2.0, '-', float64(1)},
{uint(3), 2.0, '*', float64(6)}, {uint(3), 2.0, '*', float64(6)},
{uint(3), 2.0, '/', float64(1.5)}, {uint(3), 2.0, '/', float64(1.5)},
{3.0, uint(2), '+', float64(5)}, {3.0, uint(2), '+', float64(5)},
{0.0, uint(0), '+', float64(0)},
{3.0, uint(2), '-', float64(1)}, {3.0, uint(2), '-', float64(1)},
{3.0, uint(2), '*', float64(6)}, {3.0, uint(2), '*', float64(6)},
{3.0, uint(2), '/', float64(1.5)}, {3.0, uint(2), '/', float64(1.5)},
{0, 0, '+', 0},
{0, 0, '-', 0},
{0, 0, '*', 0},
{"foo", "bar", '+', "foobar"}, {"foo", "bar", '+', "foobar"},
{3, 0, '/', false}, {3, 0, '/', false},
{3.0, 0, '/', false}, {3.0, 0, '/', false},

View file

@ -42,7 +42,7 @@ func TestPara(t *testing.T) {
c.Run("Order", func(c *qt.C) { c.Run("Order", func(c *qt.C) {
n := 500 n := 500
ints := make([]int, n) ints := make([]int, n)
for i := range n { for i := 0; i < n; i++ {
ints[i] = i ints[i] = i
} }
@ -51,7 +51,7 @@ func TestPara(t *testing.T) {
var result []int var result []int
var mu sync.Mutex var mu sync.Mutex
for i := range n { for i := 0; i < n; i++ {
i := i i := i
r.Run(func() error { r.Run(func() error {
mu.Lock() mu.Lock()
@ -78,7 +78,7 @@ func TestPara(t *testing.T) {
var counter int64 var counter int64
for range n { for i := 0; i < n; i++ {
r.Run(func() error { r.Run(func() error {
atomic.AddInt64(&counter, 1) atomic.AddInt64(&counter, 1)
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)

View file

@ -237,17 +237,12 @@ func prettifyPath(in string, b filepathPathBridge) string {
return b.Join(b.Dir(in), name, "index"+ext) return b.Join(b.Dir(in), name, "index"+ext)
} }
// CommonDirPath returns the common directory of the given paths. // CommonDir returns the common directory of the given paths.
func CommonDirPath(path1, path2 string) string { func CommonDir(path1, path2 string) string {
if path1 == "" || path2 == "" { if path1 == "" || path2 == "" {
return "" return ""
} }
hadLeadingSlash := strings.HasPrefix(path1, "/") || strings.HasPrefix(path2, "/")
path1 = TrimLeading(path1)
path2 = TrimLeading(path2)
p1 := strings.Split(path1, "/") p1 := strings.Split(path1, "/")
p2 := strings.Split(path2, "/") p2 := strings.Split(path2, "/")
@ -261,13 +256,7 @@ func CommonDirPath(path1, path2 string) string {
} }
} }
s := strings.Join(common, "/") return strings.Join(common, "/")
if hadLeadingSlash && s != "" {
s = "/" + s
}
return s
} }
// Sanitize sanitizes string to be used in Hugo's file paths and URLs, allowing only // Sanitize sanitizes string to be used in Hugo's file paths and URLs, allowing only
@ -395,27 +384,12 @@ func PathEscape(pth string) string {
// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer. // ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer.
func ToSlashTrimLeading(s string) string { func ToSlashTrimLeading(s string) string {
return TrimLeading(filepath.ToSlash(s)) return strings.TrimPrefix(filepath.ToSlash(s), "/")
}
// TrimLeading trims the leading slash from the given string.
func TrimLeading(s string) string {
return strings.TrimPrefix(s, "/")
} }
// ToSlashTrimTrailing is just a filepath.ToSlash with an added / suffix trimmer. // ToSlashTrimTrailing is just a filepath.ToSlash with an added / suffix trimmer.
func ToSlashTrimTrailing(s string) string { func ToSlashTrimTrailing(s string) string {
return TrimTrailing(filepath.ToSlash(s)) return strings.TrimSuffix(filepath.ToSlash(s), "/")
}
// TrimTrailing trims the trailing slash from the given string.
func TrimTrailing(s string) string {
return strings.TrimSuffix(s, "/")
}
// ToSlashTrim trims any leading and trailing slashes from the given string and converts it to a forward slash separated path.
func ToSlashTrim(s string) string {
return strings.Trim(filepath.ToSlash(s), "/")
} }
// ToSlashPreserveLeading converts the path given to a forward slash separated path // ToSlashPreserveLeading converts the path given to a forward slash separated path
@ -423,8 +397,3 @@ func ToSlashTrim(s string) string {
func ToSlashPreserveLeading(s string) string { func ToSlashPreserveLeading(s string) string {
return "/" + strings.Trim(filepath.ToSlash(s), "/") return "/" + strings.Trim(filepath.ToSlash(s), "/")
} }
// IsSameFilePath checks if s1 and s2 are the same file path.
func IsSameFilePath(s1, s2 string) bool {
return path.Clean(ToSlashTrim(s1)) == path.Clean(ToSlashTrim(s2))
}

View file

@ -262,52 +262,3 @@ func TestFieldsSlash(t *testing.T) {
c.Assert(FieldsSlash("/"), qt.DeepEquals, []string{}) c.Assert(FieldsSlash("/"), qt.DeepEquals, []string{})
c.Assert(FieldsSlash(""), qt.DeepEquals, []string{}) c.Assert(FieldsSlash(""), qt.DeepEquals, []string{})
} }
func TestCommonDirPath(t *testing.T) {
c := qt.New(t)
for _, this := range []struct {
a, b, expected string
}{
{"/a/b/c", "/a/b/d", "/a/b"},
{"/a/b/c", "a/b/d", "/a/b"},
{"a/b/c", "/a/b/d", "/a/b"},
{"a/b/c", "a/b/d", "a/b"},
{"/a/b/c", "/a/b/c", "/a/b/c"},
{"/a/b/c", "/a/b/c/d", "/a/b/c"},
{"/a/b/c", "/a/b", "/a/b"},
{"/a/b/c", "/a", "/a"},
{"/a/b/c", "/d/e/f", ""},
} {
c.Assert(CommonDirPath(this.a, this.b), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b))
}
}
func TestIsSameFilePath(t *testing.T) {
c := qt.New(t)
for _, this := range []struct {
a, b string
expected bool
}{
{"/a/b/c", "/a/b/c", true},
{"/a/b/c", "/a/b/c/", true},
{"/a/b/c", "/a/b/d", false},
{"/a/b/c", "/a/b", false},
{"/a/b/c", "/a/b/c/d", false},
{"/a/b/c", "/a/b/cd", false},
{"/a/b/c", "/a/b/cc", false},
{"/a/b/c", "/a/b/c/", true},
{"/a/b/c", "/a/b/c//", true},
{"/a/b/c", "/a/b/c/.", true},
{"/a/b/c", "/a/b/c/./", true},
{"/a/b/c", "/a/b/c/./.", true},
{"/a/b/c", "/a/b/c/././", true},
{"/a/b/c", "/a/b/c/././.", true},
{"/a/b/c", "/a/b/c/./././", true},
{"/a/b/c", "/a/b/c/./././.", true},
{"/a/b/c", "/a/b/c/././././", true},
} {
c.Assert(IsSameFilePath(filepath.FromSlash(this.a), filepath.FromSlash(this.b)), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b))
}
}

View file

@ -23,11 +23,6 @@ import (
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
)
const (
identifierBaseof = "baseof"
) )
// PathParser parses a path into a Path. // PathParser parses a path into a Path.
@ -38,10 +33,6 @@ type PathParser struct {
// Reports whether the given language is disabled. // Reports whether the given language is disabled.
IsLangDisabled func(string) bool IsLangDisabled func(string) bool
// IsOutputFormat reports whether the given name is a valid output format.
// The second argument is optional.
IsOutputFormat func(name, ext string) bool
// Reports whether the given ext is a content file. // Reports whether the given ext is a content file.
IsContentExt func(string) bool IsContentExt func(string) bool
} }
@ -92,10 +83,13 @@ func (pp *PathParser) Parse(c, s string) *Path {
} }
func (pp *PathParser) newPath(component string) *Path { func (pp *PathParser) newPath(component string) *Path {
p := &Path{} return &Path{
p.reset() component: component,
p.component = component posContainerLow: -1,
return p posContainerHigh: -1,
posSectionHigh: -1,
posIdentifierLanguage: -1,
}
} }
func (pp *PathParser) parse(component, s string) (*Path, error) { func (pp *PathParser) parse(component, s string) (*Path, error) {
@ -120,101 +114,10 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
return p, nil return p, nil
} }
func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot, numDots int, isLast bool) {
if p.posContainerHigh != -1 {
return
}
mayHaveLang := numDots > 1 && p.posIdentifierLanguage == -1 && pp.LanguageIndex != nil
mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
mayHaveOutputFormat := component == files.ComponentFolderLayouts
mayHaveKind := p.posIdentifierKind == -1 && mayHaveOutputFormat
var mayHaveLayout bool
if p.pathType == TypeShortcode {
mayHaveLayout = !isLast && component == files.ComponentFolderLayouts
} else {
mayHaveLayout = component == files.ComponentFolderLayouts
}
var found bool
var high int
if len(p.identifiersKnown) > 0 {
high = lastDot
} else {
high = len(p.s)
}
id := types.LowHigh[string]{Low: i + 1, High: high}
sid := p.s[id.Low:id.High]
if len(p.identifiersKnown) == 0 {
// The first is always the extension.
p.identifiersKnown = append(p.identifiersKnown, id)
found = true
// May also be the output format.
if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
p.posIdentifierOutputFormat = 0
}
} else {
var langFound bool
if mayHaveLang {
var disabled bool
_, langFound = pp.LanguageIndex[sid]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
if disabled {
p.disabled = true
langFound = true
}
}
found = langFound
if langFound {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLanguage = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveOutputFormat {
// At this point we may already have resolved an output format,
// but we need to keep looking for a more specific one, e.g. amp before html.
// Use both name and extension to prevent
// false positives on the form css.html.
if pp.IsOutputFormat(sid, p.Ext()) {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierOutputFormat = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveKind {
if kinds.GetKindMain(sid) != "" {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierKind = len(p.identifiersKnown) - 1
}
}
if !found && sid == identifierBaseof {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierBaseof = len(p.identifiersKnown) - 1
}
if !found && mayHaveLayout {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLayout = len(p.identifiersKnown) - 1
found = true
}
if !found {
p.identifiersUnknown = append(p.identifiersUnknown, id)
}
}
}
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) { func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
hasLang := pp.LanguageIndex != nil
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
s = path.Clean(filepath.ToSlash(s)) s = path.Clean(filepath.ToSlash(s))
if s == "." { if s == "." {
@ -237,26 +140,46 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
p.s = s p.s = s
slashCount := 0 slashCount := 0
lastDot := 0
lastSlashIdx := strings.LastIndex(s, "/")
numDots := strings.Count(s[lastSlashIdx+1:], ".")
if strings.Contains(s, "/_shortcodes/") {
p.pathType = TypeShortcode
}
for i := len(s) - 1; i >= 0; i-- { for i := len(s) - 1; i >= 0; i-- {
c := s[i] c := s[i]
switch c { switch c {
case '.': case '.':
pp.parseIdentifier(component, s, p, i, lastDot, numDots, false) if p.posContainerHigh == -1 {
lastDot = i var high int
if len(p.identifiers) > 0 {
high = p.identifiers[len(p.identifiers)-1].Low - 1
} else {
high = len(p.s)
}
id := types.LowHigh{Low: i + 1, High: high}
if len(p.identifiers) == 0 {
p.identifiers = append(p.identifiers, id)
} else if len(p.identifiers) == 1 {
// Check for a valid language.
s := p.s[id.Low:id.High]
if hasLang {
var disabled bool
_, langFound := pp.LanguageIndex[s]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
if disabled {
p.disabled = true
langFound = true
}
}
if langFound {
p.posIdentifierLanguage = 1
p.identifiers = append(p.identifiers, id)
}
}
}
}
case '/': case '/':
slashCount++ slashCount++
if p.posContainerHigh == -1 { if p.posContainerHigh == -1 {
if lastDot > 0 {
pp.parseIdentifier(component, s, p, i, lastDot, numDots, true)
}
p.posContainerHigh = i + 1 p.posContainerHigh = i + 1
} else if p.posContainerLow == -1 { } else if p.posContainerLow == -1 {
p.posContainerLow = i + 1 p.posContainerLow = i + 1
@ -267,52 +190,26 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
} }
} }
if len(p.identifiersKnown) > 0 { if len(p.identifiers) > 0 {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext()) isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiersKnown[len(p.identifiersKnown)-1] id := p.identifiers[len(p.identifiers)-1]
if id.Low > p.posContainerHigh {
b := p.s[p.posContainerHigh : id.Low-1] b := p.s[p.posContainerHigh : id.Low-1]
if isContent { if isContent {
switch b { switch b {
case "index": case "index":
p.pathType = TypeLeaf p.bundleType = PathTypeLeaf
case "_index": case "_index":
p.pathType = TypeBranch p.bundleType = PathTypeBranch
default: default:
p.pathType = TypeContentSingle p.bundleType = PathTypeContentSingle
} }
if slashCount == 2 && p.IsLeafBundle() { if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0 p.posSectionHigh = 0
} }
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) { } else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
p.pathType = TypeContentData p.bundleType = PathTypeContentData
}
}
}
if p.pathType < TypeMarkup && component == files.ComponentFolderLayouts {
if p.posIdentifierBaseof != -1 {
p.pathType = TypeBaseof
} else {
pth := p.Path()
if strings.Contains(pth, "/_shortcodes/") {
p.pathType = TypeShortcode
} else if strings.Contains(pth, "/_markup/") {
p.pathType = TypeMarkup
} else if strings.HasPrefix(pth, "/_partials/") {
p.pathType = TypePartial
}
}
}
if p.pathType == TypeShortcode && p.posIdentifierLayout != -1 {
id := p.identifiersKnown[p.posIdentifierLayout]
if id.Low == p.posContainerHigh {
// First identifier is shortcode name.
p.posIdentifierLayout = -1
} }
} }
@ -321,44 +218,35 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
func ModifyPathBundleTypeResource(p *Path) { func ModifyPathBundleTypeResource(p *Path) {
if p.IsContent() { if p.IsContent() {
p.pathType = TypeContentResource p.bundleType = PathTypeContentResource
} else { } else {
p.pathType = TypeFile p.bundleType = PathTypeFile
} }
} }
//go:generate stringer -type Type type PathType int
type Type int
const ( const (
// A generic resource, e.g. a JSON file. // A generic resource, e.g. a JSON file.
TypeFile Type = iota PathTypeFile PathType = iota
// All below are content files. // All below are content files.
// A resource of a content type with front matter. // A resource of a content type with front matter.
TypeContentResource PathTypeContentResource
// E.g. /blog/my-post.md // E.g. /blog/my-post.md
TypeContentSingle PathTypeContentSingle
// All below are bundled content files. // All below are bundled content files.
// Leaf bundles, e.g. /blog/my-post/index.md // Leaf bundles, e.g. /blog/my-post/index.md
TypeLeaf PathTypeLeaf
// Branch bundles, e.g. /blog/_index.md // Branch bundles, e.g. /blog/_index.md
TypeBranch PathTypeBranch
// Content data file, _content.gotmpl. // Content data file, _content.gotmpl.
TypeContentData PathTypeContentData
// Layout types.
TypeMarkup
TypeShortcode
TypePartial
TypeBaseof
) )
type Path struct { type Path struct {
@ -370,16 +258,11 @@ type Path struct {
posSectionHigh int posSectionHigh int
component string component string
pathType Type bundleType PathType
identifiersKnown []types.LowHigh[string] identifiers []types.LowHigh
identifiersUnknown []types.LowHigh[string]
posIdentifierLanguage int posIdentifierLanguage int
posIdentifierOutputFormat int
posIdentifierKind int
posIdentifierLayout int
posIdentifierBaseof int
disabled bool disabled bool
trimLeadingSlash bool trimLeadingSlash bool
@ -410,13 +293,9 @@ func (p *Path) reset() {
p.posContainerHigh = -1 p.posContainerHigh = -1
p.posSectionHigh = -1 p.posSectionHigh = -1
p.component = "" p.component = ""
p.pathType = 0 p.bundleType = 0
p.identifiersKnown = p.identifiersKnown[:0] p.identifiers = p.identifiers[:0]
p.posIdentifierLanguage = -1 p.posIdentifierLanguage = -1
p.posIdentifierOutputFormat = -1
p.posIdentifierKind = -1
p.posIdentifierLayout = -1
p.posIdentifierBaseof = -1
p.disabled = false p.disabled = false
p.trimLeadingSlash = false p.trimLeadingSlash = false
p.unnormalized = nil p.unnormalized = nil
@ -437,9 +316,6 @@ func (p *Path) norm(s string) string {
// IdentifierBase satisfies identity.Identity. // IdentifierBase satisfies identity.Identity.
func (p *Path) IdentifierBase() string { func (p *Path) IdentifierBase() string {
if p.Component() == files.ComponentFolderLayouts {
return p.Path()
}
return p.Base() return p.Base()
} }
@ -456,13 +332,6 @@ func (p *Path) Container() string {
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1]) return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
} }
func (p *Path) String() string {
if p == nil {
return "<nil>"
}
return p.Path()
}
// ContainerDir returns the container directory for this path. // ContainerDir returns the container directory for this path.
// For content bundles this will be the parent directory. // For content bundles this will be the parent directory.
func (p *Path) ContainerDir() string { func (p *Path) ContainerDir() string {
@ -483,13 +352,13 @@ func (p *Path) Section() string {
// IsContent returns true if the path is a content file (e.g. mypost.md). // IsContent returns true if the path is a content file (e.g. mypost.md).
// Note that this will also return true for content files in a bundle. // Note that this will also return true for content files in a bundle.
func (p *Path) IsContent() bool { func (p *Path) IsContent() bool {
return p.Type() >= TypeContentResource && p.Type() <= TypeContentData return p.BundleType() >= PathTypeContentResource
} }
// isContentPage returns true if the path is a content file (e.g. mypost.md), // isContentPage returns true if the path is a content file (e.g. mypost.md),
// but nof if inside a leaf bundle. // but nof if inside a leaf bundle.
func (p *Path) isContentPage() bool { func (p *Path) isContentPage() bool {
return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData return p.BundleType() >= PathTypeContentSingle
} }
// Name returns the last element of path. // Name returns the last element of path.
@ -503,7 +372,7 @@ func (p *Path) Name() string {
// Name returns the last element of path without any extension. // Name returns the last element of path without any extension.
func (p *Path) NameNoExt() string { func (p *Path) NameNoExt() string {
if i := p.identifierIndex(0); i != -1 { if i := p.identifierIndex(0); i != -1 {
return p.s[p.posContainerHigh : p.identifiersKnown[i].Low-1] return p.s[p.posContainerHigh : p.identifiers[i].Low-1]
} }
return p.s[p.posContainerHigh:] return p.s[p.posContainerHigh:]
} }
@ -515,7 +384,7 @@ func (p *Path) NameNoLang() string {
return p.Name() return p.Name()
} }
return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:] return p.s[p.posContainerHigh:p.identifiers[i].Low-1] + p.s[p.identifiers[i].High:]
} }
// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension). // BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension).
@ -529,26 +398,10 @@ func (p *Path) BaseNameNoIdentifier() string {
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension). // NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
func (p *Path) NameNoIdentifier() string { func (p *Path) NameNoIdentifier() string {
lowHigh := p.nameLowHigh() if len(p.identifiers) > 0 {
return p.s[lowHigh.Low:lowHigh.High] return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
}
func (p *Path) nameLowHigh() types.LowHigh[string] {
if len(p.identifiersKnown) > 0 {
lastID := p.identifiersKnown[len(p.identifiersKnown)-1]
if p.posContainerHigh == lastID.Low {
// The last identifier is the name.
return lastID
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: p.identifiersKnown[len(p.identifiersKnown)-1].Low - 1,
}
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: len(p.s),
} }
return p.s[p.posContainerHigh:]
} }
// Dir returns all but the last element of path, typically the path's directory. // Dir returns all but the last element of path, typically the path's directory.
@ -568,11 +421,6 @@ func (p *Path) Path() (d string) {
return p.norm(p.s) return p.norm(p.s)
} }
// PathNoLeadingSlash returns the full path without the leading slash.
func (p *Path) PathNoLeadingSlash() string {
return p.Path()[1:]
}
// Unnormalized returns the Path with the original case preserved. // Unnormalized returns the Path with the original case preserved.
func (p *Path) Unnormalized() *Path { func (p *Path) Unnormalized() *Path {
return p.unnormalized return p.unnormalized
@ -588,28 +436,6 @@ func (p *Path) PathNoIdentifier() string {
return p.base(false, false) return p.base(false, false)
} }
// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
if len(p.identifiersKnown) == 0 {
return p.norm(p.s)
}
i := p.identifierIndex(0)
if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
i = j
}
if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
i = j
}
if i == -1 {
return p.norm(p.s)
}
id := p.identifiersKnown[i]
return p.norm(p.s[:id.Low-1])
}
// PathRel returns the path relative to the given owner. // PathRel returns the path relative to the given owner.
func (p *Path) PathRel(owner *Path) string { func (p *Path) PathRel(owner *Path) string {
ob := owner.Base() ob := owner.Base()
@ -636,42 +462,26 @@ func (p *Path) Base() string {
return p.base(!p.isContentPage(), p.IsBundle()) return p.base(!p.isContentPage(), p.IsBundle())
} }
// Used in template lookups.
// For pages with Type set, we treat that as the section.
func (p *Path) BaseReTyped(typ string) (d string) {
base := p.Base()
if p.Section() == typ {
return base
}
d = "/" + typ
if p.posSectionHigh != -1 {
d += base[p.posSectionHigh:]
}
d = p.norm(d)
return
}
// BaseNoLeadingSlash returns the base path without the leading slash. // BaseNoLeadingSlash returns the base path without the leading slash.
func (p *Path) BaseNoLeadingSlash() string { func (p *Path) BaseNoLeadingSlash() string {
return p.Base()[1:] return p.Base()[1:]
} }
func (p *Path) base(preserveExt, isBundle bool) string { func (p *Path) base(preserveExt, isBundle bool) string {
if len(p.identifiersKnown) == 0 { if len(p.identifiers) == 0 {
return p.norm(p.s) return p.norm(p.s)
} }
if preserveExt && len(p.identifiersKnown) == 1 { if preserveExt && len(p.identifiers) == 1 {
// Preserve extension. // Preserve extension.
return p.norm(p.s) return p.norm(p.s)
} }
var high int id := p.identifiers[len(p.identifiers)-1]
high := id.Low - 1
if isBundle { if isBundle {
high = p.posContainerHigh - 1 high = p.posContainerHigh - 1
} else {
high = p.nameLowHigh().High
} }
if high == 0 { if high == 0 {
@ -683,7 +493,7 @@ func (p *Path) base(preserveExt, isBundle bool) string {
} }
// For txt files etc. we want to preserve the extension. // For txt files etc. we want to preserve the extension.
id := p.identifiersKnown[0] id = p.identifiers[0]
return p.norm(p.s[:high] + p.s[id.Low-1:id.High]) return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
} }
@ -692,20 +502,8 @@ func (p *Path) Ext() string {
return p.identifierAsString(0) return p.identifierAsString(0)
} }
func (p *Path) OutputFormat() string {
return p.identifierAsString(p.posIdentifierOutputFormat)
}
func (p *Path) Kind() string {
return p.identifierAsString(p.posIdentifierKind)
}
func (p *Path) Layout() string {
return p.identifierAsString(p.posIdentifierLayout)
}
func (p *Path) Lang() string { func (p *Path) Lang() string {
return p.identifierAsString(p.posIdentifierLanguage) return p.identifierAsString(1)
} }
func (p *Path) Identifier(i int) string { func (p *Path) Identifier(i int) string {
@ -717,43 +515,35 @@ func (p *Path) Disabled() bool {
} }
func (p *Path) Identifiers() []string { func (p *Path) Identifiers() []string {
ids := make([]string, len(p.identifiersKnown)) ids := make([]string, len(p.identifiers))
for i, id := range p.identifiersKnown { for i, id := range p.identifiers {
ids[i] = p.s[id.Low:id.High] ids[i] = p.s[id.Low:id.High]
} }
return ids return ids
} }
func (p *Path) IdentifiersUnknown() []string { func (p *Path) BundleType() PathType {
ids := make([]string, len(p.identifiersUnknown)) return p.bundleType
for i, id := range p.identifiersUnknown {
ids[i] = p.s[id.Low:id.High]
}
return ids
}
func (p *Path) Type() Type {
return p.pathType
} }
func (p *Path) IsBundle() bool { func (p *Path) IsBundle() bool {
return p.pathType >= TypeLeaf && p.pathType <= TypeContentData return p.bundleType >= PathTypeLeaf
} }
func (p *Path) IsBranchBundle() bool { func (p *Path) IsBranchBundle() bool {
return p.pathType == TypeBranch return p.bundleType == PathTypeBranch
} }
func (p *Path) IsLeafBundle() bool { func (p *Path) IsLeafBundle() bool {
return p.pathType == TypeLeaf return p.bundleType == PathTypeLeaf
} }
func (p *Path) IsContentData() bool { func (p *Path) IsContentData() bool {
return p.pathType == TypeContentData return p.bundleType == PathTypeContentData
} }
func (p Path) ForType(t Type) *Path { func (p Path) ForBundleType(t PathType) *Path {
p.pathType = t p.bundleType = t
return &p return &p
} }
@ -763,12 +553,12 @@ func (p *Path) identifierAsString(i int) string {
return "" return ""
} }
id := p.identifiersKnown[i] id := p.identifiers[i]
return p.s[id.Low:id.High] return p.s[id.Low:id.High]
} }
func (p *Path) identifierIndex(i int) int { func (p *Path) identifierIndex(i int) int {
if i < 0 || i >= len(p.identifiersKnown) { if i < 0 || i >= len(p.identifiers) {
return -1 return -1
} }
return i return i

View file

@ -18,7 +18,6 @@ import (
"testing" "testing"
"github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
) )
@ -27,18 +26,10 @@ var testParser = &PathParser{
LanguageIndex: map[string]int{ LanguageIndex: map[string]int{
"no": 0, "no": 0,
"en": 1, "en": 1,
"fr": 2,
}, },
IsContentExt: func(ext string) bool { IsContentExt: func(ext string) bool {
return ext == "md" return ext == "md"
}, },
IsOutputFormat: func(name, ext string) bool {
switch name {
case "html", "amp", "csv", "rss":
return true
}
return false
},
} }
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
@ -114,19 +105,17 @@ func TestParse(t *testing.T) {
"Basic Markdown file", "Basic Markdown file",
"/a/b/c.md", "/a/b/c.md",
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Type(), qt.Equals, TypeContentSingle)
c.Assert(p.IsContent(), qt.IsTrue) c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse) c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.Name(), qt.Equals, "c.md") c.Assert(p.Name(), qt.Equals, "c.md")
c.Assert(p.Base(), qt.Equals, "/a/b/c") c.Assert(p.Base(), qt.Equals, "/a/b/c")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c")
c.Assert(p.Section(), qt.Equals, "a") c.Assert(p.Section(), qt.Equals, "a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c") c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
c.Assert(p.Path(), qt.Equals, "/a/b/c.md") c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
c.Assert(p.Dir(), qt.Equals, "/a/b") c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Container(), qt.Equals, "b") c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a/b") c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
c.Assert(p.Ext(), qt.Equals, "md")
}, },
}, },
{ {
@ -141,7 +130,7 @@ func TestParse(t *testing.T) {
// Reclassify it as a content resource. // Reclassify it as a content resource.
ModifyPathBundleTypeResource(p) ModifyPathBundleTypeResource(p)
c.Assert(p.Type(), qt.Equals, TypeContentResource) c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
c.Assert(p.IsContent(), qt.IsTrue) c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.Name(), qt.Equals, "b.md") c.Assert(p.Name(), qt.Equals, "b.md")
c.Assert(p.Base(), qt.Equals, "/a/b.md") c.Assert(p.Base(), qt.Equals, "/a/b.md")
@ -174,10 +163,8 @@ func TestParse(t *testing.T) {
c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b") c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt") c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"}) c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt") c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt") c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt") c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.Ext(), qt.Equals, "txt") c.Assert(p.Ext(), qt.Equals, "txt")
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b") c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
@ -187,11 +174,7 @@ func TestParse(t *testing.T) {
"Home branch cundle", "Home branch cundle",
"/_index.md", "/_index.md",
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
c.Assert(p.IsBranchBundle(), qt.IsTrue)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.Base(), qt.Equals, "/") c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
c.Assert(p.Path(), qt.Equals, "/_index.md") c.Assert(p.Path(), qt.Equals, "/_index.md")
c.Assert(p.Container(), qt.Equals, "") c.Assert(p.Container(), qt.Equals, "")
c.Assert(p.ContainerDir(), qt.Equals, "/") c.Assert(p.ContainerDir(), qt.Equals, "/")
@ -202,14 +185,12 @@ func TestParse(t *testing.T) {
"/a/index.md", "/a/index.md",
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a") c.Assert(p.Base(), qt.Equals, "/a")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a") c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a") c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a") c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.ContainerDir(), qt.Equals, "") c.Assert(p.ContainerDir(), qt.Equals, "")
c.Assert(p.Dir(), qt.Equals, "/a") c.Assert(p.Dir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md") c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"index"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"}) c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
c.Assert(p.IsBranchBundle(), qt.IsFalse) c.Assert(p.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue) c.Assert(p.IsBundle(), qt.IsTrue)
@ -227,7 +208,6 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b") c.Assert(p.Base(), qt.Equals, "/a/b")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b") c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b")
c.Assert(p.Container(), qt.Equals, "b") c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a") c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Dir(), qt.Equals, "/a/b") c.Assert(p.Dir(), qt.Equals, "/a/b")
@ -240,7 +220,6 @@ func TestParse(t *testing.T) {
c.Assert(p.NameNoExt(), qt.Equals, "index.no") c.Assert(p.NameNoExt(), qt.Equals, "index.no")
c.Assert(p.NameNoIdentifier(), qt.Equals, "index") c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
c.Assert(p.NameNoLang(), qt.Equals, "index.md") c.Assert(p.NameNoLang(), qt.Equals, "index.md")
c.Assert(p.Path(), qt.Equals, "/a/b/index.no.md")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md") c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md")
c.Assert(p.Section(), qt.Equals, "a") c.Assert(p.Section(), qt.Equals, "a")
}, },
@ -376,225 +355,11 @@ func TestParse(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
c.Run(test.name, func(c *qt.C) { c.Run(test.name, func(c *qt.C) {
if test.name != "Home branch cundle" {
// return
}
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path)) test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
}) })
} }
} }
func TestParseLayouts(t *testing.T) {
c := qt.New(t)
tests := []struct {
name string
path string
assert func(c *qt.C, p *Path)
}{
{
"Basic",
"/list.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Lang",
"/list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Kind",
"/section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/section.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Layout",
"/list.section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Layout multiple",
"/mylayout.list.section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "mylayout")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list", "mylayout"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/mylayout.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Layout shortcode",
"/_shortcodes/myshort.list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
},
},
{
"Layout baseof",
"/baseof.list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
},
},
{
"Lang and output format",
"/list.no.amp.not.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Base(), qt.Equals, "/list.html")
},
},
{
"Term",
"/term.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/term.html")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"})
c.Assert(p.PathNoIdentifier(), qt.Equals, "/term")
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term")
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "term")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Shortcode with layout",
"/_shortcodes/myshortcode.list.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/_shortcodes/myshortcode.html")
c.Assert(p.Type(), qt.Equals, TypeShortcode)
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list"})
c.Assert(p.Layout(), qt.Equals, "list")
c.Assert(p.PathNoIdentifier(), qt.Equals, "/_shortcodes/myshortcode")
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/_shortcodes/myshortcode.list")
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Sub dir",
"/pages/home.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"})
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "home")
c.Assert(p.OutputFormat(), qt.Equals, "html")
c.Assert(p.Dir(), qt.Equals, "/pages")
},
},
{
"Baseof",
"/pages/baseof.list.section.fr.amp.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
c.Assert(p.Lang(), qt.Equals, "fr")
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Dir(), qt.Equals, "/pages")
c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof")
c.Assert(p.Type(), qt.Equals, TypeBaseof)
c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html")
},
},
{
"Markup",
"/_markup/render-link.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeMarkup)
},
},
{
"Markup nested",
"/foo/_markup/render-link.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeMarkup)
},
},
{
"Shortcode",
"/_shortcodes/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Shortcode nested",
"/foo/_shortcodes/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Shortcode nested sub",
"/foo/_shortcodes/foo/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Partials",
"/_partials/foo.bar",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypePartial)
},
},
{
"Shortcode lang in root",
"/_shortcodes/no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.NameNoIdentifier(), qt.Equals, "no")
},
},
{
"Shortcode lang layout",
"/_shortcodes/myshortcode.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Layout(), qt.Equals, "")
c.Assert(p.NameNoIdentifier(), qt.Equals, "myshortcode")
},
},
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Shortcode lang layout" {
// return
}
test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
})
}
}
func TestHasExt(t *testing.T) { func TestHasExt(t *testing.T) {
c := qt.New(t) c := qt.New(t)

View file

@ -78,26 +78,3 @@ disablePathToLower = true
b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single") b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single")
b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single") b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single")
} }
func TestIssue13596(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/p1/index.md --
---
title: p1
---
-- content/p1/a.1.txt --
-- content/p1/a.2.txt --
-- layouts/all.html --
{{ range .Resources.Match "*" }}{{ .Name }}|{{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "a.1.txt|a.2.txt|")
b.AssertFileExists("public/p1/a.1.txt", true)
b.AssertFileExists("public/p1/a.2.txt", true) // fails
}

View file

@ -0,0 +1,27 @@
// Code generated by "stringer -type=PathType"; DO NOT EDIT.
package paths
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[PathTypeFile-0]
_ = x[PathTypeContentResource-1]
_ = x[PathTypeContentSingle-2]
_ = x[PathTypeLeaf-3]
_ = x[PathTypeBranch-4]
}
const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
func (i PathType) String() string {
if i < 0 || i >= PathType(len(_PathType_index)-1) {
return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
}

View file

@ -1,32 +0,0 @@
// Code generated by "stringer -type Type"; DO NOT EDIT.
package paths
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TypeFile-0]
_ = x[TypeContentResource-1]
_ = x[TypeContentSingle-2]
_ = x[TypeLeaf-3]
_ = x[TypeBranch-4]
_ = x[TypeContentData-5]
_ = x[TypeMarkup-6]
_ = x[TypeShortcode-7]
_ = x[TypePartial-8]
_ = x[TypeBaseof-9]
}
const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof"
var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121}
func (i Type) String() string {
if i < 0 || i >= Type(len(_Type_index)-1) {
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Type_name[_Type_index[i]:_Type_index[i+1]]
}

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved. // Copyright 2021 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -18,7 +18,6 @@ import (
"net/url" "net/url"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
) )
@ -160,77 +159,9 @@ func Uglify(in string) string {
return path.Clean(in) return path.Clean(in)
} }
// URLEscape escapes unicode letters. // UrlToFilename converts the URL s to a filename.
func URLEscape(uri string) string {
// escape unicode letters
u, err := url.Parse(uri)
if err != nil {
panic(err)
}
return u.String()
}
// TrimExt trims the extension from a path..
func TrimExt(in string) string {
return strings.TrimSuffix(in, path.Ext(in))
}
// From https://github.com/golang/go/blob/e0c76d95abfc1621259864adb3d101cf6f1f90fc/src/cmd/go/internal/web/url.go#L45
func UrlFromFilename(filename string) (*url.URL, error) {
if !filepath.IsAbs(filename) {
return nil, fmt.Errorf("filepath must be absolute")
}
// If filename has a Windows volume name, convert the volume to a host and prefix
// per https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/.
if vol := filepath.VolumeName(filename); vol != "" {
if strings.HasPrefix(vol, `\\`) {
filename = filepath.ToSlash(filename[2:])
i := strings.IndexByte(filename, '/')
if i < 0 {
// A degenerate case.
// \\host.example.com (without a share name)
// becomes
// file://host.example.com/
return &url.URL{
Scheme: "file",
Host: filename,
Path: "/",
}, nil
}
// \\host.example.com\Share\path\to\file
// becomes
// file://host.example.com/Share/path/to/file
return &url.URL{
Scheme: "file",
Host: filename[:i],
Path: filepath.ToSlash(filename[i:]),
}, nil
}
// C:\path\to\file
// becomes
// file:///C:/path/to/file
return &url.URL{
Scheme: "file",
Path: "/" + filepath.ToSlash(filename),
}, nil
}
// /path/to/file
// becomes
// file:///path/to/file
return &url.URL{
Scheme: "file",
Path: filepath.ToSlash(filename),
}, nil
}
// UrlStringToFilename converts the URL s to a filename.
// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned. // If ParseRequestURI fails, the input is just converted to OS specific slashes and returned.
func UrlStringToFilename(s string) (string, bool) { func UrlToFilename(s string) (string, bool) {
u, err := url.ParseRequestURI(s) u, err := url.ParseRequestURI(s)
if err != nil { if err != nil {
return filepath.FromSlash(s), false return filepath.FromSlash(s), false
@ -240,34 +171,25 @@ func UrlStringToFilename(s string) (string, bool) {
if p == "" { if p == "" {
p, _ = url.QueryUnescape(u.Opaque) p, _ = url.QueryUnescape(u.Opaque)
return filepath.FromSlash(p), false return filepath.FromSlash(p), true
}
if runtime.GOOS != "windows" {
return p, true
}
if len(p) == 0 || p[0] != '/' {
return filepath.FromSlash(p), false
} }
p = filepath.FromSlash(p) p = filepath.FromSlash(p)
if len(u.Host) == 1 { if u.Host != "" {
// file://c/Users/... // C:\data\file.txt
return strings.ToUpper(u.Host) + ":" + p, true p = strings.ToUpper(u.Host) + ":" + p
} }
if u.Host != "" && u.Host != "localhost" { return p, true
if filepath.VolumeName(u.Host) != "" {
return "", false
}
return `\\` + u.Host + p, true
} }
if vol := filepath.VolumeName(p[1:]); vol == "" || strings.HasPrefix(vol, `\\`) { // URLEscape escapes unicode letters.
return "", false func URLEscape(uri string) string {
// escape unicode letters
u, err := url.Parse(uri)
if err != nil {
panic(err)
} }
return u.String()
return p[1:], true
} }

View file

@ -24,9 +24,6 @@ func (p P[T]) And(ps ...P[T]) P[T] {
return false return false
} }
} }
if p == nil {
return true
}
return p(v) return p(v)
} }
} }
@ -39,9 +36,6 @@ func (p P[T]) Or(ps ...P[T]) P[T] {
return true return true
} }
} }
if p == nil {
return false
}
return p(v) return p(v)
} }
} }

View file

@ -51,7 +51,7 @@ func Run[T any](ctx context.Context, cfg Config[T]) Group[T] {
// Buffered for performance. // Buffered for performance.
ch := make(chan T, cfg.NumWorkers) ch := make(chan T, cfg.NumWorkers)
for range cfg.NumWorkers { for i := 0; i < cfg.NumWorkers; i++ {
g.Go(func() error { g.Go(func() error {
for { for {
select { select {

View file

@ -1,150 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tasks
import (
"sync"
"time"
)
// RunEvery runs a function at intervals defined by the function itself.
// Functions can be added and removed while running.
type RunEvery struct {
// Any error returned from the function will be passed to this function.
HandleError func(string, error)
// If set, the function will be run immediately.
RunImmediately bool
// The named functions to run.
funcs map[string]*Func
mu sync.Mutex
started bool
closed bool
quit chan struct{}
}
type Func struct {
// The shortest interval between each run.
IntervalLow time.Duration
// The longest interval between each run.
IntervalHigh time.Duration
// The function to run.
F func(interval time.Duration) (time.Duration, error)
interval time.Duration
last time.Time
}
func (r *RunEvery) Start() error {
if r.started {
return nil
}
r.started = true
r.quit = make(chan struct{})
go func() {
if r.RunImmediately {
r.run()
}
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-r.quit:
return
case <-ticker.C:
r.run()
}
}
}()
return nil
}
// Close stops the RunEvery from running.
func (r *RunEvery) Close() error {
if r.closed {
return nil
}
r.closed = true
if r.quit != nil {
close(r.quit)
}
return nil
}
// Add adds a function to the RunEvery.
func (r *RunEvery) Add(name string, f Func) {
r.mu.Lock()
defer r.mu.Unlock()
if r.funcs == nil {
r.funcs = make(map[string]*Func)
}
if f.IntervalLow == 0 {
f.IntervalLow = 500 * time.Millisecond
}
if f.IntervalHigh <= f.IntervalLow {
f.IntervalHigh = 20 * time.Second
}
start := max(f.IntervalHigh/3, f.IntervalLow)
f.interval = start
f.last = time.Now()
r.funcs[name] = &f
}
// Remove removes a function from the RunEvery.
func (r *RunEvery) Remove(name string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.funcs, name)
}
// Has returns whether the RunEvery has a function with the given name.
func (r *RunEvery) Has(name string) bool {
r.mu.Lock()
defer r.mu.Unlock()
_, found := r.funcs[name]
return found
}
func (r *RunEvery) run() {
r.mu.Lock()
defer r.mu.Unlock()
for name, f := range r.funcs {
if time.Now().Before(f.last.Add(f.interval)) {
continue
}
f.last = time.Now()
interval, err := f.F(f.interval)
if err != nil && r.HandleError != nil {
r.HandleError(name, err)
}
if interval < f.IntervalLow {
interval = f.IntervalLow
}
if interval > f.IntervalHigh {
interval = f.IntervalHigh
}
f.interval = interval
}
}

View file

@ -17,6 +17,7 @@ package terminal
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
"strings" "strings"
isatty "github.com/mattn/go-isatty" isatty "github.com/mattn/go-isatty"
@ -40,6 +41,10 @@ func PrintANSIColors(f *os.File) bool {
// IsTerminal return true if the file descriptor is terminal and the TERM // IsTerminal return true if the file descriptor is terminal and the TERM
// environment variable isn't a dumb one. // environment variable isn't a dumb one.
func IsTerminal(f *os.File) bool { func IsTerminal(f *os.File) bool {
if runtime.GOOS == "windows" {
return false
}
fd := f.Fd() fd := f.Fd()
return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)) return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd))
} }

View file

@ -1,54 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import "sync"
type Closer interface {
Close() error
}
// CloserFunc is a convenience type to create a Closer from a function.
type CloserFunc func() error
func (f CloserFunc) Close() error {
return f()
}
type CloseAdder interface {
Add(Closer)
}
type Closers struct {
mu sync.Mutex
cs []Closer
}
func (cs *Closers) Add(c Closer) {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.cs = append(cs.cs, c)
}
func (cs *Closers) Close() error {
cs.mu.Lock()
defer cs.mu.Unlock()
for _, c := range cs.cs {
c.Close()
}
cs.cs = cs.cs[:0]
return nil
}

View file

@ -69,7 +69,7 @@ func ToStringSlicePreserveStringE(v any) ([]string, error) {
switch vv.Kind() { switch vv.Kind() {
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:
result = make([]string, vv.Len()) result = make([]string, vv.Len())
for i := range vv.Len() { for i := 0; i < vv.Len(); i++ {
s, err := cast.ToStringE(vv.Index(i).Interface()) s, err := cast.ToStringE(vv.Index(i).Interface())
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -15,28 +15,27 @@
package types package types
import ( import (
"slices"
"sync" "sync"
) )
// EvictingQueue is a queue which automatically evicts elements from the head of // EvictingStringQueue is a queue which automatically evicts elements from the head of
// the queue when attempting to add new elements onto the queue and it is full. // the queue when attempting to add new elements onto the queue and it is full.
// This queue orders elements LIFO (last-in-first-out). It throws away duplicates. // This queue orders elements LIFO (last-in-first-out). It throws away duplicates.
type EvictingQueue[T comparable] struct { // Note: This queue currently does not contain any remove (poll etc.) methods.
type EvictingStringQueue struct {
size int size int
vals []T vals []string
set map[T]bool set map[string]bool
mu sync.Mutex mu sync.Mutex
zero T
} }
// NewEvictingQueue creates a new queue with the given size. // NewEvictingStringQueue creates a new queue with the given size.
func NewEvictingQueue[T comparable](size int) *EvictingQueue[T] { func NewEvictingStringQueue(size int) *EvictingStringQueue {
return &EvictingQueue[T]{size: size, set: make(map[T]bool)} return &EvictingStringQueue{size: size, set: make(map[string]bool)}
} }
// Add adds a new string to the tail of the queue if it's not already there. // Add adds a new string to the tail of the queue if it's not already there.
func (q *EvictingQueue[T]) Add(v T) *EvictingQueue[T] { func (q *EvictingStringQueue) Add(v string) *EvictingStringQueue {
q.mu.Lock() q.mu.Lock()
if q.set[v] { if q.set[v] {
q.mu.Unlock() q.mu.Unlock()
@ -46,7 +45,7 @@ func (q *EvictingQueue[T]) Add(v T) *EvictingQueue[T] {
if len(q.set) == q.size { if len(q.set) == q.size {
// Full // Full
delete(q.set, q.vals[0]) delete(q.set, q.vals[0])
q.vals = slices.Delete(q.vals, 0, 1) q.vals = append(q.vals[:0], q.vals[1:]...)
} }
q.set[v] = true q.set[v] = true
q.vals = append(q.vals, v) q.vals = append(q.vals, v)
@ -55,7 +54,7 @@ func (q *EvictingQueue[T]) Add(v T) *EvictingQueue[T] {
return q return q
} }
func (q *EvictingQueue[T]) Len() int { func (q *EvictingStringQueue) Len() int {
if q == nil { if q == nil {
return 0 return 0
} }
@ -65,22 +64,19 @@ func (q *EvictingQueue[T]) Len() int {
} }
// Contains returns whether the queue contains v. // Contains returns whether the queue contains v.
func (q *EvictingQueue[T]) Contains(v T) bool { func (q *EvictingStringQueue) Contains(v string) bool {
if q == nil {
return false
}
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock() defer q.mu.Unlock()
return q.set[v] return q.set[v]
} }
// Peek looks at the last element added to the queue. // Peek looks at the last element added to the queue.
func (q *EvictingQueue[T]) Peek() T { func (q *EvictingStringQueue) Peek() string {
q.mu.Lock() q.mu.Lock()
l := len(q.vals) l := len(q.vals)
if l == 0 { if l == 0 {
q.mu.Unlock() q.mu.Unlock()
return q.zero return ""
} }
elem := q.vals[l-1] elem := q.vals[l-1]
q.mu.Unlock() q.mu.Unlock()
@ -88,12 +84,9 @@ func (q *EvictingQueue[T]) Peek() T {
} }
// PeekAll looks at all the elements in the queue, with the newest first. // PeekAll looks at all the elements in the queue, with the newest first.
func (q *EvictingQueue[T]) PeekAll() []T { func (q *EvictingStringQueue) PeekAll() []string {
if q == nil {
return nil
}
q.mu.Lock() q.mu.Lock()
vals := make([]T, len(q.vals)) vals := make([]string, len(q.vals))
copy(vals, q.vals) copy(vals, q.vals)
q.mu.Unlock() q.mu.Unlock()
for i, j := 0, len(vals)-1; i < j; i, j = i+1, j-1 { for i, j := 0, len(vals)-1; i < j; i, j = i+1, j-1 {
@ -103,9 +96,9 @@ func (q *EvictingQueue[T]) PeekAll() []T {
} }
// PeekAllSet returns PeekAll as a set. // PeekAllSet returns PeekAll as a set.
func (q *EvictingQueue[T]) PeekAllSet() map[T]bool { func (q *EvictingStringQueue) PeekAllSet() map[string]bool {
all := q.PeekAll() all := q.PeekAll()
set := make(map[T]bool) set := make(map[string]bool)
for _, v := range all { for _, v := range all {
set[v] = true set[v] = true
} }

View file

@ -23,7 +23,7 @@ import (
func TestEvictingStringQueue(t *testing.T) { func TestEvictingStringQueue(t *testing.T) {
c := qt.New(t) c := qt.New(t)
queue := NewEvictingQueue[string](3) queue := NewEvictingStringQueue(3)
c.Assert(queue.Peek(), qt.Equals, "") c.Assert(queue.Peek(), qt.Equals, "")
queue.Add("a") queue.Add("a")
@ -53,9 +53,9 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
var wg sync.WaitGroup var wg sync.WaitGroup
val := "someval" val := "someval"
queue := NewEvictingQueue[string](3) queue := NewEvictingStringQueue(3)
for range 100 { for j := 0; j < 100; j++ {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()

View file

@ -13,24 +13,8 @@
package hstring package hstring
import ( type RenderedString string
"html/template"
"github.com/gohugoio/hugo/common/types" func (s RenderedString) String() string {
)
var _ types.PrintableValueProvider = HTML("")
// HTML is a string that represents rendered HTML.
// When printed in templates it will be rendered as template.HTML and considered safe so no need to pipe it into `safeHTML`.
// This type was introduced as a wasy to prevent a common case of inifinite recursion in the template rendering
// when the `linkify` option is enabled with a common (wrong) construct like `{{ .Text | .Page.RenderString }}` in a hook template.
type HTML string
func (s HTML) String() string {
return string(s) return string(s)
} }
func (s HTML) PrintableValue() any {
return template.HTML(s)
}

View file

@ -25,6 +25,6 @@ func TestRenderedString(t *testing.T) {
c := qt.New(t) c := qt.New(t)
// Validate that it will behave like a string in Hugo settings. // Validate that it will behave like a string in Hugo settings.
c.Assert(cast.ToString(HTML("Hugo")), qt.Equals, "Hugo") c.Assert(cast.ToString(RenderedString("Hugo")), qt.Equals, "Hugo")
c.Assert(template.HTML(HTML("Hugo")), qt.Equals, template.HTML("Hugo")) c.Assert(template.HTML(RenderedString("Hugo")), qt.Equals, template.HTML("Hugo"))
} }

View file

@ -28,16 +28,6 @@ type RLocker interface {
RUnlock() RUnlock()
} }
type Locker interface {
Lock()
Unlock()
}
type RWLocker interface {
RLocker
Locker
}
// KeyValue is a interface{} tuple. // KeyValue is a interface{} tuple.
type KeyValue struct { type KeyValue struct {
Key any Key any
@ -69,7 +59,7 @@ func (k KeyValues) String() string {
// KeyValues struct. // KeyValues struct.
func NewKeyValuesStrings(key string, values ...string) KeyValues { func NewKeyValuesStrings(key string, values ...string) KeyValues {
iv := make([]any, len(values)) iv := make([]any, len(values))
for i := range values { for i := 0; i < len(values); i++ {
iv[i] = values[i] iv[i] = values[i]
} }
return KeyValues{Key: key, Values: iv} return KeyValues{Key: key, Values: iv}
@ -117,20 +107,12 @@ func Unwrapv(v any) any {
return v return v
} }
// LowHigh represents a byte or slice boundary. // LowHigh is typically used to represent a slice boundary.
type LowHigh[S ~[]byte | string] struct { type LowHigh struct {
Low int Low int
High int High int
} }
func (l LowHigh[S]) IsZero() bool {
return l.Low < 0 || (l.Low == 0 && l.High == 0)
}
func (l LowHigh[S]) Value(source S) S {
return source[l.Low:l.High]
}
// This is only used for debugging purposes. // This is only used for debugging purposes.
var InvocationCounter atomic.Int64 var InvocationCounter atomic.Int64
@ -138,8 +120,3 @@ var InvocationCounter atomic.Int64
func NewBool(b bool) *bool { func NewBool(b bool) *bool {
return &b return &b
} }
// PrintableValueProvider is implemented by types that can provide a printable value.
type PrintableValueProvider interface {
PrintableValue() any
}

Some files were not shown because too many files have changed in this diff Show more