mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 15:58:53 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd1657c360 |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22600.20100
|
||||
- image: bepsays/ci-hugoreleaser:1.22300.20200
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20100
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22300.20200
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
have_fun: false
|
||||
memory_config:
|
||||
disabled: false
|
||||
code_review:
|
||||
disable: false
|
||||
comment_severity_threshold: HIGH
|
||||
max_review_comments: -1
|
||||
pull_request_opened:
|
||||
help: true
|
||||
summary: false
|
||||
code_review: false
|
||||
include_drafts: false
|
||||
ignore_patterns: []
|
||||
@@ -16,20 +16,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
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@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
|
||||
with:
|
||||
context: .
|
||||
provenance: mode=max
|
||||
@@ -46,4 +46,3 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: HUGO_BUILD_TAGS=extended,withdeploy
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
with:
|
||||
operations-per-run: 999
|
||||
days-before-issue-stale: 365
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
stale-pr-message: This PR has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
|
||||
|
||||
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell us in a comment.
|
||||
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell is in a comment.
|
||||
|
||||
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
stale-issue-label: 'Stale'
|
||||
|
||||
+21
-23
@@ -6,17 +6,17 @@ name: Test
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
SASS_VERSION: 1.80.3
|
||||
DART_SASS_SHA_LINUX: 7c933edbad0a7d389192c5b79393485c088bd2c4398e32f5754c32af006a9ffd
|
||||
DART_SASS_SHA_MACOS: 79e060b0e131c3bb3c16926bafc371dc33feab122bfa8c01aa337a072097967b
|
||||
DART_SASS_SHA_WINDOWS: 0bc4708b37cd1bac4740e83ac5e3176e66b774f77fd5dd364da5b5cfc9bfb469
|
||||
SASS_VERSION: 1.63.2
|
||||
DART_SASS_SHA_LINUX: 3ea33c95ad5c35fda6e9a0956199eef38a398f496cfb8750e02479d7d1dd42af
|
||||
DART_SASS_SHA_MACOS: 11c70f259836b250b44a9cb57fed70e030f21f45069b467d371685855f1eb4f0
|
||||
DART_SASS_SHA_WINDOWS: cd8cd36a619dd8e27f93d3186c52d70eb7d69472aa6c85f5094b29693e773f64
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.25.x, 1.26.x]
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -24,7 +24,9 @@ jobs:
|
||||
name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||
with:
|
||||
tool-cache: true
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
@@ -32,9 +34,9 @@ jobs:
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
@@ -43,21 +45,18 @@ jobs:
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
with:
|
||||
ruby-version: "3.4.5"
|
||||
- name: Install Ruby gems
|
||||
run: |
|
||||
gem install asciidoctor -v "2.0.26"
|
||||
gem install asciidoctor-diagram -v "3.1.0"
|
||||
- name: Install GoAT
|
||||
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
|
||||
ruby-version: "2.7"
|
||||
bundler-cache: true #
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install Mage
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@c642db5eedd1d729bb8c92034770d0b2f769eda6 # v2.0.2
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
@@ -106,10 +105,7 @@ jobs:
|
||||
run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Run staticcheck
|
||||
run: |
|
||||
export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck"
|
||||
staticcheck ./...
|
||||
rm -rf ${{ runner.temp }}/staticcheck
|
||||
run: staticcheck ./...
|
||||
- if: matrix.os != 'windows-latest'
|
||||
name: Check
|
||||
run: |
|
||||
@@ -121,14 +117,16 @@ jobs:
|
||||
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
|
||||
name: Test
|
||||
run: |
|
||||
mage -v test
|
||||
mage -v test;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended,withdeploy
|
||||
- name: Build tags
|
||||
run: |
|
||||
go install -tags extended
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Build for dragonfly
|
||||
run: |
|
||||
go install
|
||||
go clean -i -cache
|
||||
env:
|
||||
GOARCH: amd64
|
||||
GOOS: dragonfly
|
||||
|
||||
@@ -3,5 +3,3 @@
|
||||
imports.*
|
||||
dist/
|
||||
public/
|
||||
.DS_Store
|
||||
cache/filecache/_gen/
|
||||
@@ -1 +0,0 @@
|
||||
tpl/tplimpl/embedded/templates/**
|
||||
@@ -1,17 +0,0 @@
|
||||
* Brevity is good.
|
||||
* Assume that the maintainers and readers of the code you write are Go experts:
|
||||
* Don't use comments to explain the obvious.
|
||||
* Use self-explanatory variable and function names.
|
||||
* Use short variable names when the context is clear.
|
||||
* If you need to add temporary debug printing, use `hdebug.Printf`.[^1]
|
||||
* Never export symbols that's not needed outside of the package.
|
||||
* Avoid global state at (almost) all cost.
|
||||
* This is a project with a long history; assume that a similiar problem has been solved before, look hard for helper functions before creating new ones.
|
||||
* In tests, use `qt` matchers (e.g. `b.Assert(err, qt.ErrorMatches, ...)`) instead of raw `if`/`t.Fatal` checks.
|
||||
* In tests, always use the latest Hugo specification, e.g. for layouts, it's `layouts/page.html` and not `layouts/_default/single.html`, `layouts/list.html` and not `layouts/_default/list.html`
|
||||
* Brevity is good. This applies to code, comments and commit messages. Don't write a novel.
|
||||
* Use `./check.sh ./somepackage/...` when iterating.
|
||||
* Use `./check.sh` when you're done.
|
||||
|
||||
|
||||
[^1]: CI build fail if you forget to remove the debug printing.
|
||||
+3
-15
@@ -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
|
||||
|
||||
@@ -19,7 +19,6 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
|
||||
* [Reporting Issues](#reporting-issues)
|
||||
* [Submitting Patches](#submitting-patches)
|
||||
* [Code Contribution Guidelines](#code-contribution-guidelines)
|
||||
* [AI Assistance Notice](#ai-assistance-notice)
|
||||
* [Git Commit Message Guidelines](#git-commit-message-guidelines)
|
||||
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
|
||||
* [Building Hugo with Your Changes](#building-hugo-with-your-changes)
|
||||
@@ -55,7 +54,7 @@ If it is of some complexity, the contributor is expected to maintain and support
|
||||
|
||||
Any non-trivial code change needs to update an open [issue](https://github.com/gohugoio/hugo/issues). A non-trivial code change without an issue reference with one of the labels `bug` or `enhancement` will not be merged.
|
||||
|
||||
Note that we do not accept new features that require [CGO](https://go.dev/wiki/cgo).
|
||||
Note that we do not accept new features that require [CGO](https://github.com/golang/go/wiki/cgo).
|
||||
We have one exception to this rule which is LibSASS.
|
||||
|
||||
**Bug fixes are, of course, always welcome.**
|
||||
@@ -77,19 +76,9 @@ To make the contribution process as seamless as possible, we ask for the followi
|
||||
* Run `go fmt`.
|
||||
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
|
||||
* Squash your commits into a single commit. `git rebase -i`. It’s okay to force update your pull request with `git push -f`.
|
||||
* Ensure that `./check.sh` succeeds. Note that some tests are skipped when running locally, some because they are slow. To run these locally, do `CI_LOCAL=true ./check.sh ./somepackage/...`.
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
|
||||
* Follow the **Git Commit Message Guidelines** below.
|
||||
|
||||
## AI Assistance Notice
|
||||
|
||||
If a substantial part of your contribution is autogenerated with AI, **this must be disclosed in the pull request**, along with the extent to which AI assistance was used.
|
||||
|
||||
An example disclosure:
|
||||
|
||||
> This PR was written primarily by Claude Code.
|
||||
|
||||
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it. Also, AI contributions from non-maintainers needs to have a fairly narrow scope (e.g. a bug fix), as we have limited review capacity.
|
||||
|
||||
### Git Commit Message Guidelines
|
||||
|
||||
This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages,
|
||||
@@ -104,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 is a documentation update, prefix with `docs:`.
|
||||
* 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*.
|
||||
Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*.
|
||||
|
||||
+5
-7
@@ -2,8 +2,8 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
ARG GO_VERSION="1.26"
|
||||
ARG ALPINE_VERSION="3.22"
|
||||
ARG GO_VERSION="1.23.2"
|
||||
ARG ALPINE_VERSION="3.20"
|
||||
ARG DART_SASS_VERSION="1.79.3"
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx
|
||||
@@ -19,7 +19,7 @@ RUN apk add clang lld
|
||||
COPY --from=xx / /
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
RUN xx-apk add musl-dev gcc g++
|
||||
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 .
|
||||
@@ -65,16 +65,14 @@ RUN apk add --no-cache \
|
||||
git \
|
||||
runuser \
|
||||
nodejs \
|
||||
npm \
|
||||
openssh-client \
|
||||
tar
|
||||
npm
|
||||
|
||||
RUN mkdir -p /var/hugo/bin /cache && \
|
||||
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 && \
|
||||
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
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ A fast and flexible static site generator built with love by [bep], [spf13], and
|
||||
[](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
|
||||
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a> | <a rel="me" href="https://x.com/gohugoiov2">X</a>
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -52,7 +52,7 @@ Use Hugo's embedded web server during development to instantly see changes to co
|
||||
|
||||
Hugo's fast asset pipelines include:
|
||||
|
||||
- Image processing – Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract metadata
|
||||
- Image processing – Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
|
||||
- JavaScript bundling – Transpile TypeScript and JSX to JavaScript, bundle, tree shake, minify, create source maps, and perform SRI hashing.
|
||||
- Sass processing – Transpile Sass to CSS, bundle, tree shake, minify, create source maps, perform SRI hashing, and integrate with PostCSS
|
||||
- Tailwind CSS processing – Compile Tailwind CSS utility classes into standard CSS, bundle, tree shake, optimize, minify, perform SRI hashing, and integrate with PostCSS
|
||||
@@ -65,24 +65,11 @@ See the [features] section of the documentation for a comprehensive summary of H
|
||||
|
||||
<p> </p>
|
||||
<p float="left">
|
||||
<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>
|
||||
|
||||
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/cloudcannon-cms-logo.svg" width="200" alt="CloudCannon"></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>
|
||||
|
||||
<a href="https://route4me.com/" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/Route4MeLogoBlueOnWhite.svg" width="200" alt="Route Planning & Route Optimization Software"></a>
|
||||
</p>
|
||||
|
||||
## Editions
|
||||
|
||||
Hugo offers a standard edition with core features, plus extended and extended/deploy editions with more. Use the standard edition unless you need the features below.
|
||||
|
||||
Feature|extended edition|extended/deploy edition
|
||||
:--|:-:|:-:
|
||||
[Transpile Sass to CSS] via embedded LibSass. Note that embedded LibSass was deprecated in v0.153.0 and will be removed in a future release. Use the [Dart Sass] transpiler instead, which is compatible 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 [details].|:x:|:heavy_check_mark:
|
||||
|
||||
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
|
||||
[transpile sass to css]: https://gohugo.io/functions/css/sass/
|
||||
[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/
|
||||
|
||||
## Installation
|
||||
|
||||
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
|
||||
@@ -94,11 +81,15 @@ Install Hugo from a [prebuilt binary], package manager, or package repository. P
|
||||
|
||||
## 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:
|
||||
|
||||
- Standard edition: Go 1.25.0 or later
|
||||
- Extended edition: Go 1.25.0 or later, and GCC
|
||||
- Extended/deploy edition: Go 1.25.0 or later, and GCC
|
||||
- Standard edition: Go 1.20 or later
|
||||
- Extended edition: Go 1.20 or later, and GCC
|
||||
|
||||
Build the standard edition:
|
||||
|
||||
@@ -111,13 +102,6 @@ Build the extended edition:
|
||||
```text
|
||||
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
|
||||
|
||||
[](https://star-history.com/#gohugoio/hugo&Timeline)
|
||||
@@ -156,16 +140,6 @@ If there is sufficient interest, [create a proposal]. Do not submit a pull reque
|
||||
|
||||
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
For the Hugo source code, see [LICENSE](/LICENSE).
|
||||
|
||||
We also bundle some libraries in binary/WASM form:
|
||||
|
||||
* [libwebp](https://github.com/webmproject/libwebp), [BSD-3-Clause license](https://github.com/webmproject/libwebp?tab=BSD-3-Clause-1-ov-file#readme)
|
||||
* [Katex](https://github.com/KaTeX/KaTeX), [MIT license](https://github.com/KaTeX/KaTeX?tab=MIT-1-ov-file#readme)
|
||||
* [QuickJS](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme), [License](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme)
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
|
||||
@@ -174,116 +148,153 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
|
||||
<summary>See current dependencies</summary>
|
||||
|
||||
```text
|
||||
cloud.google.com/go/compute/metadata="v0.2.3"
|
||||
cloud.google.com/go/iam="v1.1.5"
|
||||
cloud.google.com/go/storage="v1.35.1"
|
||||
cloud.google.com/go="v0.110.10"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore="v1.9.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity="v1.4.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal="v1.5.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob="v1.2.0"
|
||||
github.com/Azure/go-autorest/autorest/to="v0.4.0"
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go="v1.2.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/JohannesKaufmann/dom="v0.2.0"
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2="v2.5.0"
|
||||
github.com/alecthomas/chroma/v2="v2.21.1"
|
||||
github.com/aymerick/douceur="v0.2.0"
|
||||
github.com/alecthomas/chroma/v2="v2.14.0"
|
||||
github.com/armon/go-radix="v1.0.1-0.20221118154546-54df44f2176c"
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream="v1.5.4"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.26.1"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.16.12"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.14.10"
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager="v1.15.7"
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources="v1.3.5"
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2="v2.6.5"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.7.2"
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a="v1.2.9"
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront="v1.35.4"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding="v1.10.4"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum="v1.2.9"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.10.9"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared="v1.16.9"
|
||||
github.com/aws/aws-sdk-go-v2/service/s3="v1.47.5"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.18.5"
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc="v1.21.5"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.26.5"
|
||||
github.com/aws/aws-sdk-go-v2="v1.26.1"
|
||||
github.com/aws/aws-sdk-go="v1.50.7"
|
||||
github.com/aws/smithy-go="v1.20.2"
|
||||
github.com/bep/clocks="v0.5.0"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.9.0"
|
||||
github.com/bep/gitmap="v1.1.2"
|
||||
github.com/bep/goat="v0.5.0"
|
||||
github.com/bep/godartsass/v2="v2.5.0"
|
||||
github.com/bep/golibsass="v1.2.0"
|
||||
github.com/bep/goportabletext="v0.1.0"
|
||||
github.com/bep/helpers="v0.6.0"
|
||||
github.com/bep/imagemeta="v0.12.0"
|
||||
github.com/bep/lazycache="v0.8.0"
|
||||
github.com/bep/godartsass/v2="v2.0.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/lazycache="v0.4.0"
|
||||
github.com/bep/logg="v0.4.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/overlayfs="v0.10.0"
|
||||
github.com/bep/simplecobra="v0.6.1"
|
||||
github.com/bep/textandbinarywriter="v0.0.0-20251212174530-cd9f0732f60f"
|
||||
github.com/bep/overlayfs="v0.9.2"
|
||||
github.com/bep/simplecobra="v0.4.0"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/bits-and-blooms/bitset="v1.24.4"
|
||||
github.com/cespare/xxhash/v2="v2.3.0"
|
||||
github.com/clbanning/mxj/v2="v2.7.0"
|
||||
github.com/clipperhouse/displaywidth="v0.6.0"
|
||||
github.com/clipperhouse/stringish="v0.1.1"
|
||||
github.com/clipperhouse/uax29/v2="v2.3.0"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.6"
|
||||
github.com/cli/safeexec="v1.0.1"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.3"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.11.5"
|
||||
github.com/evanw/esbuild="v0.27.2"
|
||||
github.com/fatih/color="v1.18.0"
|
||||
github.com/dlclark/regexp2="v1.11.0"
|
||||
github.com/dustin/go-humanize="v1.0.1"
|
||||
github.com/evanw/esbuild="v0.21.4"
|
||||
github.com/fatih/color="v1.16.0"
|
||||
github.com/frankban/quicktest="v1.14.6"
|
||||
github.com/fsnotify/fsnotify="v1.9.0"
|
||||
github.com/getkin/kin-openapi="v0.133.0"
|
||||
github.com/go-openapi/jsonpointer="v0.21.0"
|
||||
github.com/go-openapi/swag="v0.23.0"
|
||||
github.com/gobuffalo/flect="v1.0.3"
|
||||
github.com/fsnotify/fsnotify="v1.7.0"
|
||||
github.com/getkin/kin-openapi="v0.123.0"
|
||||
github.com/ghodss/yaml="v1.0.0"
|
||||
github.com/go-openapi/jsonpointer="v0.20.2"
|
||||
github.com/go-openapi/swag="v0.22.8"
|
||||
github.com/gobuffalo/flect="v1.0.2"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/goccy/go-yaml="v1.19.1"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20251018145728-cfcc22d823c6"
|
||||
github.com/gohugoio/go-radix="v1.2.0"
|
||||
github.com/gohugoio/hashstructure="v0.6.0"
|
||||
github.com/gohugoio/httpcache="v0.8.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.5.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.1"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
|
||||
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.2.0"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v1.0.1"
|
||||
github.com/google/go-cmp="v0.7.0"
|
||||
github.com/gorilla/css="v1.0.1"
|
||||
github.com/gorilla/websocket="v1.5.3"
|
||||
github.com/hairyhenderson/go-codeowners="v0.7.0"
|
||||
github.com/golang-jwt/jwt/v5="v5.1.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/s2a-go="v0.1.7"
|
||||
github.com/google/uuid="v1.4.0"
|
||||
github.com/google/wire="v0.5.0"
|
||||
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.7"
|
||||
github.com/invopop/yaml="v0.2.0"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/jmespath/go-jmespath="v0.4.0"
|
||||
github.com/josharian/intern="v1.0.0"
|
||||
github.com/kr/pretty="v0.3.1"
|
||||
github.com/kr/text="v0.2.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.13"
|
||||
github.com/kylelemons/godebug="v1.1.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.12"
|
||||
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/mattn/go-colorable="v0.1.13"
|
||||
github.com/mattn/go-isatty="v0.0.20"
|
||||
github.com/mattn/go-runewidth="v0.0.19"
|
||||
github.com/microcosm-cc/bluemonday="v1.0.27"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/mitchellh/hashstructure="v1.1.0"
|
||||
github.com/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c"
|
||||
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.9.1"
|
||||
github.com/oasdiff/yaml3="v0.0.0-20250309153720-d2182401db90"
|
||||
github.com/oasdiff/yaml="v0.0.0-20250309154309-f31be36b4037"
|
||||
github.com/olekukonko/cat="v0.0.0-20250911104152-50322a0618f6"
|
||||
github.com/olekukonko/errors="v1.1.0"
|
||||
github.com/olekukonko/ll="v0.1.3"
|
||||
github.com/olekukonko/tablewriter="v1.1.2"
|
||||
github.com/niklasfasching/go-org="v1.7.0"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
|
||||
github.com/pelletier/go-toml/v2="v2.2.4"
|
||||
github.com/pelletier/go-toml/v2="v2.2.2"
|
||||
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/rogpeppe/go-internal="v1.14.1"
|
||||
github.com/rogpeppe/go-internal="v1.12.0"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
github.com/sass/dart-sass/compiler="1.97.1"
|
||||
github.com/sass/dart-sass/implementation="1.97.1"
|
||||
github.com/sass/dart-sass/protocol="3.2.0"
|
||||
github.com/spf13/afero="v1.15.0"
|
||||
github.com/spf13/cast="v1.10.0"
|
||||
github.com/spf13/cobra="v1.10.2"
|
||||
github.com/rwcarlsen/goexif="v0.0.0-20190401172101-9e8deecbddbd"
|
||||
github.com/sass/dart-sass/compiler="1.77.5"
|
||||
github.com/sass/dart-sass/implementation="1.77.5"
|
||||
github.com/sass/dart-sass/protocol="2.7.1"
|
||||
github.com/sass/libsass="3.6.5"
|
||||
github.com/spf13/afero="v1.11.0"
|
||||
github.com/spf13/cast="v1.6.0"
|
||||
github.com/spf13/cobra="v1.8.0"
|
||||
github.com/spf13/fsync="v0.10.1"
|
||||
github.com/spf13/pflag="v1.0.9"
|
||||
github.com/tdewolff/minify/v2="v2.24.8"
|
||||
github.com/tdewolff/parse/v2="v2.8.5"
|
||||
github.com/tetratelabs/wazero="v1.10.1"
|
||||
github.com/webmproject/libwebp="v1.6.0"
|
||||
github.com/woodsbury/decimal128="v1.3.0"
|
||||
github.com/yuin/goldmark-emoji="v1.0.6"
|
||||
github.com/yuin/goldmark="v1.7.13"
|
||||
github.com/spf13/pflag="v1.0.5"
|
||||
github.com/tdewolff/minify/v2="v2.20.20"
|
||||
github.com/tdewolff/parse/v2="v2.7.13"
|
||||
github.com/webmproject/libwebp="v1.3.2"
|
||||
github.com/yuin/goldmark-emoji="v1.0.3"
|
||||
github.com/yuin/goldmark="v1.7.4"
|
||||
go.opencensus.io="v0.24.0"
|
||||
go.uber.org/automaxprocs="v1.5.3"
|
||||
go.yaml.in/yaml/v3="v3.0.4"
|
||||
golang.org/x/crypto="v0.46.0"
|
||||
golang.org/x/image="v0.34.0"
|
||||
golang.org/x/mod="v0.31.0"
|
||||
golang.org/x/net="v0.48.0"
|
||||
golang.org/x/sync="v0.19.0"
|
||||
golang.org/x/sys="v0.39.0"
|
||||
golang.org/x/text="v0.32.0"
|
||||
golang.org/x/tools="v0.40.0"
|
||||
google.golang.org/protobuf="v1.36.10"
|
||||
gocloud.dev="v0.36.0"
|
||||
golang.org/x/crypto="v0.23.0"
|
||||
golang.org/x/exp="v0.0.0-20221031165847-c99f073a8326"
|
||||
golang.org/x/image="v0.16.0"
|
||||
golang.org/x/mod="v0.17.0"
|
||||
golang.org/x/net="v0.25.0"
|
||||
golang.org/x/oauth2="v0.15.0"
|
||||
golang.org/x/sync="v0.7.0"
|
||||
golang.org/x/sys="v0.20.0"
|
||||
golang.org/x/text="v0.15.0"
|
||||
golang.org/x/time="v0.5.0"
|
||||
golang.org/x/tools="v0.20.0"
|
||||
golang.org/x/xerrors="v0.0.0-20231012003039-104605ab7028"
|
||||
google.golang.org/api="v0.152.0"
|
||||
google.golang.org/genproto/googleapis/api="v0.0.0-20231120223509-83a465c0220f"
|
||||
google.golang.org/genproto/googleapis/rpc="v0.0.0-20231120223509-83a465c0220f"
|
||||
google.golang.org/genproto="v0.0.0-20231120223509-83a465c0220f"
|
||||
google.golang.org/grpc="v1.59.0"
|
||||
google.golang.org/protobuf="v1.33.0"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
rsc.io/qr="v0.2.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -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
|
||||
Executable
+12
@@ -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
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
gobench -package=./hugolib -bench="BenchmarkSiteNew/Deep_content_tree"
|
||||
Executable
+1
@@ -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
|
||||
Vendored
+8
-14
@@ -69,7 +69,7 @@ func New(opts Options) *Cache {
|
||||
|
||||
infol := opts.Log.InfoCommand("dynacache")
|
||||
|
||||
evictedIdentities := collections.NewStackThreadSafe[KeyIdentity]()
|
||||
evictedIdentities := collections.NewStack[KeyIdentity]()
|
||||
|
||||
onEvict := func(k, v any) {
|
||||
if !opts.Watching {
|
||||
@@ -129,7 +129,7 @@ type Cache struct {
|
||||
partitions map[string]PartitionManager
|
||||
|
||||
onEvict func(k, v any)
|
||||
evictedIdentities *collections.StackThreadSafe[KeyIdentity]
|
||||
evictedIdentities *collections.Stack[KeyIdentity]
|
||||
|
||||
opts Options
|
||||
infol logg.LevelLogger
|
||||
@@ -176,12 +176,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.
|
||||
// predicate is optional and will clear any entry for which it returns true.
|
||||
func (c *Cache) ClearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) {
|
||||
func (c *Cache) ClearOnRebuild(changeset ...identity.Identity) {
|
||||
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
|
||||
NumWorkers: len(c.partitions),
|
||||
Handle: func(ctx context.Context, partition PartitionManager) error {
|
||||
partition.clearOnRebuild(predicate, changeset...)
|
||||
partition.clearOnRebuild(changeset...)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
@@ -340,7 +339,7 @@ func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts Optio
|
||||
panic("invalid Weight, must be between 1 and 100")
|
||||
}
|
||||
|
||||
if !partitionNameRe.MatchString(name) {
|
||||
if partitionNameRe.FindString(name) != name {
|
||||
panic(fmt.Sprintf("invalid partition name %q", name))
|
||||
}
|
||||
|
||||
@@ -480,12 +479,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) {
|
||||
if predicate == nil {
|
||||
predicate = func(k, v any) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
|
||||
opts := p.getOptions()
|
||||
if opts.ClearWhen == ClearNever {
|
||||
return
|
||||
@@ -531,7 +525,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
|
||||
// elements marked as stale in the other partitions.
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
if predicate(key, v) || shouldDelete(key, v) {
|
||||
if shouldDelete(key, v) {
|
||||
p.trace.Log(
|
||||
logg.StringFunc(
|
||||
func() string {
|
||||
@@ -607,7 +601,7 @@ type PartitionManager interface {
|
||||
adjustMaxSize(addend int) int
|
||||
getMaxSize() int
|
||||
getOptions() OptionsPartition
|
||||
clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity)
|
||||
clearOnRebuild(changeset ...identity.Identity)
|
||||
clearMatching(predicate func(k, v any) bool)
|
||||
clearStale()
|
||||
}
|
||||
|
||||
Vendored
+6
-6
@@ -147,13 +147,13 @@ func TestClear(t *testing.T) {
|
||||
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 4)
|
||||
|
||||
cache.ClearOnRebuild(nil)
|
||||
cache.ClearOnRebuild()
|
||||
|
||||
// Stale items are always cleared.
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 2)
|
||||
|
||||
cache = newTestCache(t)
|
||||
cache.ClearOnRebuild(nil, identity.StringIdentity("changed"))
|
||||
cache.ClearOnRebuild(identity.StringIdentity("changed"))
|
||||
|
||||
c.Assert(cache.Keys(nil), qt.HasLen, 1)
|
||||
|
||||
@@ -191,16 +191,16 @@ func TestPanicInCreate(t *testing.T) {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range 3 {
|
||||
for range 3 {
|
||||
for i := 0; i < 3; i++ {
|
||||
for j := 0; j < 3; j++ {
|
||||
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 {
|
||||
for i := 0; i < 3; i++ {
|
||||
for j := 0; j < 3; j++ {
|
||||
v, err := p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) {
|
||||
return testItem{
|
||||
name: key,
|
||||
|
||||
Vendored
+59
-142
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -16,7 +16,6 @@ package filecache
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -46,12 +45,16 @@ const (
|
||||
type Cache struct {
|
||||
Fs afero.Fs
|
||||
|
||||
cfg FileCacheConfig
|
||||
// Max age for items in this cache. Negative duration means forever,
|
||||
// 0 is effectively turning this cache off.
|
||||
maxAge time.Duration
|
||||
|
||||
entryLocker *lockTracker
|
||||
// When set, we just remove this entire root directory on expiration.
|
||||
pruneAllRootDir string
|
||||
|
||||
nlocker *lockTracker
|
||||
|
||||
initOnce sync.Once
|
||||
isInited bool
|
||||
initErr error
|
||||
}
|
||||
|
||||
@@ -85,15 +88,12 @@ type ItemInfo struct {
|
||||
}
|
||||
|
||||
// NewCache creates a new file cache with the given filesystem and max age.
|
||||
func NewCache(fs afero.Fs, cfg FileCacheConfig) *Cache {
|
||||
if err := cfg.init(); err != nil {
|
||||
panic(fmt.Sprintf("invalid cache config: %s", err))
|
||||
}
|
||||
|
||||
func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
|
||||
return &Cache{
|
||||
Fs: fs,
|
||||
entryLocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
|
||||
cfg: cfg,
|
||||
Fs: fs,
|
||||
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
|
||||
maxAge: maxAge,
|
||||
pruneAllRootDir: pruneAllRootDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,15 +109,9 @@ func (l *lockedFile) Close() error {
|
||||
}
|
||||
|
||||
func (c *Cache) init() error {
|
||||
if c == nil {
|
||||
panic("cache is nil")
|
||||
}
|
||||
|
||||
c.initOnce.Do(func() {
|
||||
c.isInited = true
|
||||
// Create the base dir if it does not exist.
|
||||
if err := c.Fs.MkdirAll("", 0o777); err != nil && !os.IsExist(err) {
|
||||
err = fmt.Errorf("failled to create base cache directory: %s", err)
|
||||
c.initErr = err
|
||||
}
|
||||
})
|
||||
@@ -132,19 +126,19 @@ func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
|
||||
}
|
||||
|
||||
id = cleanID(id)
|
||||
c.entryLocker.Lock(id)
|
||||
c.nlocker.Lock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
f, err := helpers.OpenFileForWriting(c.Fs, id)
|
||||
if err != nil {
|
||||
c.entryLocker.Unlock(id)
|
||||
c.nlocker.Unlock(id)
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
return info, &lockedFile{
|
||||
File: f,
|
||||
unlock: func() { c.entryLocker.Unlock(id) },
|
||||
unlock: func() { c.nlocker.Unlock(id) },
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -162,8 +156,8 @@ func (c *Cache) ReadOrCreate(id string,
|
||||
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info = ItemInfo{Name: id}
|
||||
|
||||
@@ -192,9 +186,9 @@ func (c *Cache) ReadOrCreate(id string,
|
||||
// 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.entryLocker.Lock(id)
|
||||
c.nlocker.Lock(id)
|
||||
return func() {
|
||||
c.entryLocker.Unlock(id)
|
||||
c.nlocker.Unlock(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,8 +201,8 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
@@ -226,7 +220,7 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.cfg.MaxAge == 0 {
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return info, hugio.ToReadCloser(r), nil
|
||||
}
|
||||
@@ -237,40 +231,6 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
c.writeReader(id, io.TeeReader(r, &buff))
|
||||
}
|
||||
|
||||
// AbsFilenameFromID returns the filename for the given id in the cache.
|
||||
// This will be an absolute path.
|
||||
func (c *Cache) AbsFilenameFromID(id string) string {
|
||||
return filepath.Join(c.cfg.DirCompiled, cleanID(id))
|
||||
}
|
||||
|
||||
// GetOrCreateInfo tries to get the item info with the given id from cache. If not found or expired, create will
|
||||
// be invoked with the id. The create function is expected to create the cache item with the given id. The returned ItemInfo will have the id as Name.
|
||||
// This method is protected by a named lock using the given id as identifier.
|
||||
func (c *Cache) GetOrCreateInfo(id string, create func(id string) error) (ItemInfo, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, err
|
||||
}
|
||||
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if !c.removeIfNeeded(id) {
|
||||
// The file exists and is not expired, so we consider it a cache hit.
|
||||
return info, nil
|
||||
}
|
||||
|
||||
if err := create(id); err != nil {
|
||||
c.remove(id)
|
||||
return info, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (c *Cache) writeReader(id string, r io.Reader) error {
|
||||
dir := filepath.Dir(id)
|
||||
if dir != "" {
|
||||
@@ -294,8 +254,8 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
@@ -315,7 +275,7 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.cfg.MaxAge == 0 {
|
||||
if c.maxAge == 0 {
|
||||
return info, b, nil
|
||||
}
|
||||
|
||||
@@ -326,39 +286,15 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
return info, b, nil
|
||||
}
|
||||
|
||||
// SetBytes sets the file content with the given id in the cache.
|
||||
func (c *Cache) SetBytes(id string, data []byte) error {
|
||||
if err := c.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
if c.cfg.MaxAge == 0 {
|
||||
// No caching.
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.writeReader(id, bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// GetBytes gets the file content with the given id from the cache, nil if none found.
|
||||
func (c *Cache) GetBytes(id string) ([]byte, error) {
|
||||
_, b, err := c.GetItemBytes(id)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// GetItemBytes gets the ItemInfo and file content with the given id from the cache, nil if none found.
|
||||
func (c *Cache) GetItemBytes(id string) (ItemInfo, []byte, error) {
|
||||
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
@@ -378,8 +314,8 @@ func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
@@ -388,23 +324,10 @@ func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
|
||||
return info, r, nil
|
||||
}
|
||||
|
||||
// removeIfNeeded checks if the file with the given id should be re-created.
|
||||
func (c *Cache) removeIfNeeded(id string) bool {
|
||||
if c.cfg.MaxAge == 0 {
|
||||
// No caching, remove.
|
||||
c.remove(id)
|
||||
return true
|
||||
}
|
||||
if removed, err := c.removeIfExpired(id); err != nil || removed {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getOrRemove gets the file with the given id. If it's expired, it will
|
||||
// be removed.
|
||||
func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
|
||||
if c.cfg.MaxAge == 0 {
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return nil
|
||||
}
|
||||
@@ -422,7 +345,7 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
|
||||
}
|
||||
|
||||
func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
|
||||
if c.cfg.MaxAge == 0 {
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return nil, false
|
||||
}
|
||||
@@ -447,7 +370,7 @@ func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
|
||||
}
|
||||
|
||||
func (c *Cache) removeIfExpired(id string) (bool, error) {
|
||||
if c.cfg.MaxAge <= 0 {
|
||||
if c.maxAge <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -457,37 +380,29 @@ func (c *Cache) removeIfExpired(id string) (bool, error) {
|
||||
}
|
||||
|
||||
if c.isExpired(fi.ModTime()) {
|
||||
c.remove(id)
|
||||
c.Fs.Remove(id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *Cache) remove(id string) {
|
||||
if c.cfg.entryIsDir {
|
||||
c.Fs.RemoveAll(id)
|
||||
} else {
|
||||
c.Fs.Remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) isExpired(modTime time.Time) bool {
|
||||
if c.cfg.MaxAge < 0 {
|
||||
if c.maxAge < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Note the use of time.Since here.
|
||||
// We cannot use Hugo's global Clock for this.
|
||||
return c.cfg.MaxAge == 0 || time.Since(modTime) > c.cfg.MaxAge
|
||||
return c.maxAge == 0 || time.Since(modTime) > c.maxAge
|
||||
}
|
||||
|
||||
// For testing
|
||||
func (c *Cache) GetString(id string) string {
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
if err != nil {
|
||||
@@ -502,17 +417,6 @@ func (c *Cache) GetString(id string) string {
|
||||
// Caches is a named set of caches.
|
||||
type Caches map[string]*Cache
|
||||
|
||||
func (f Caches) SetResourceFs(fs afero.Fs) {
|
||||
for _, c := range f {
|
||||
if c.cfg.IsResourceDir {
|
||||
if c.isInited {
|
||||
panic("cannot set resource fs after init")
|
||||
}
|
||||
c.Fs = hugofs.NewBasePathFs(fs, c.cfg.DirCompiled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets a named cache, nil if none found.
|
||||
func (f Caches) Get(name string) *Cache {
|
||||
return f[strings.ToLower(name)]
|
||||
@@ -520,21 +424,34 @@ func (f Caches) Get(name string) *Cache {
|
||||
|
||||
// NewCaches creates a new set of file caches from the given
|
||||
// configuration.
|
||||
func NewCaches(dcfg Configs, sourceFs afero.Fs) (Caches, error) {
|
||||
fs := sourceFs
|
||||
func NewCaches(p *helpers.PathSpec) (Caches, error) {
|
||||
dcfg := p.Cfg.GetConfigSection("caches").(Configs)
|
||||
fs := p.Fs.Source
|
||||
|
||||
m := make(Caches)
|
||||
for k, v := range dcfg {
|
||||
var cfs afero.Fs
|
||||
|
||||
if v.IsResourceDir {
|
||||
cfs = nil // Set later. TODO(bep) this needs to be cleanded up.
|
||||
cfs = p.BaseFs.ResourcesCache
|
||||
} else {
|
||||
cfs = hugofs.NewBasePathFs(fs, v.DirCompiled)
|
||||
cfs = fs
|
||||
}
|
||||
|
||||
c := NewCache(cfs, v)
|
||||
if cfs == nil {
|
||||
panic("nil fs")
|
||||
}
|
||||
|
||||
m[k] = c
|
||||
baseDir := v.DirCompiled
|
||||
|
||||
bfs := hugofs.NewBasePathFs(cfs, baseDir)
|
||||
|
||||
var pruneAllRootDir string
|
||||
if k == CacheKeyModules {
|
||||
pruneAllRootDir = "pkg"
|
||||
}
|
||||
|
||||
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -563,7 +480,7 @@ func (h *httpCache) Get(id string) (resp []byte, ok bool) {
|
||||
}
|
||||
|
||||
func (h *httpCache) Set(id string, resp []byte) {
|
||||
if h.c.cfg.MaxAge == 0 {
|
||||
if h.c.maxAge == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Vendored
+33
-103
@@ -15,7 +15,6 @@
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
@@ -23,7 +22,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@@ -35,50 +34,35 @@ const (
|
||||
cacheDirProject = ":cacheDir/:project"
|
||||
)
|
||||
|
||||
var defaultCacheConfig = FileCacheConfig{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: cacheDirProject,
|
||||
}
|
||||
|
||||
const (
|
||||
CacheKeyImages = "images"
|
||||
CacheKeyAssets = "assets"
|
||||
CacheKeyModules = "modules"
|
||||
CacheKeyModuleQueries = "modulequeries"
|
||||
CacheKeyModuleGitInfo = "modulegitinfo"
|
||||
CacheKeyGetResource = "getresource"
|
||||
CacheKeyMisc = "misc"
|
||||
CacheKeyGetJSON = "getjson"
|
||||
CacheKeyGetCSV = "getcsv"
|
||||
CacheKeyImages = "images"
|
||||
CacheKeyAssets = "assets"
|
||||
CacheKeyModules = "modules"
|
||||
CacheKeyGetResource = "getresource"
|
||||
CacheKeyMisc = "misc"
|
||||
)
|
||||
|
||||
type Configs map[string]FileCacheConfig
|
||||
|
||||
// CacheDirModules returns the compiled path to the modules cache.
|
||||
// For internal use.
|
||||
func (c Configs) CacheDirModules() string {
|
||||
return c[CacheKeyModules].DirCompiled
|
||||
}
|
||||
|
||||
// CacheDirMisc returns the compiled path to the misc cache.
|
||||
// For internal use.
|
||||
func (c Configs) CacheDirMisc() string {
|
||||
return c[CacheKeyMisc].DirCompiled
|
||||
}
|
||||
|
||||
var defaultCacheConfigs = Configs{
|
||||
CacheKeyModules: {
|
||||
MaxAge: -1,
|
||||
Dir: ":cacheDir/modules",
|
||||
fileCacheConfigInternal: fileCacheConfigInternal{
|
||||
entryIsDir: true,
|
||||
isReadOnly: true, // we need to make it writable when pruning.
|
||||
},
|
||||
},
|
||||
CacheKeyModuleQueries: {
|
||||
MaxAge: 24 * time.Hour,
|
||||
Dir: ":cacheDir/modules",
|
||||
},
|
||||
CacheKeyModuleGitInfo: {
|
||||
MaxAge: 24 * time.Hour,
|
||||
Dir: ":cacheDir/modules",
|
||||
fileCacheConfigInternal: fileCacheConfigInternal{
|
||||
entryIsDir: true,
|
||||
},
|
||||
},
|
||||
CacheKeyGetJSON: defaultCacheConfig,
|
||||
CacheKeyGetCSV: defaultCacheConfig,
|
||||
CacheKeyImages: {
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
@@ -97,13 +81,6 @@ var defaultCacheConfigs = Configs{
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for k, v := range defaultCacheConfigs {
|
||||
v.name = k
|
||||
defaultCacheConfigs[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
type FileCacheConfig struct {
|
||||
// Max age of cache entries in this cache. Any items older than this will
|
||||
// be removed and not returned from the cache.
|
||||
@@ -115,50 +92,22 @@ type FileCacheConfig struct {
|
||||
MaxAge time.Duration
|
||||
|
||||
// The directory where files are stored.
|
||||
Dir string
|
||||
Dir string
|
||||
DirCompiled string `json:"-"`
|
||||
|
||||
fileCacheConfigInternal `json:"-"`
|
||||
// Will resources/_gen will get its own composite filesystem that
|
||||
// also checks any theme.
|
||||
IsResourceDir bool `json:"-"`
|
||||
}
|
||||
|
||||
func (cfg *FileCacheConfig) init() error {
|
||||
if cfg.DirCompiled == "" {
|
||||
// From unit tests. Just check that it does not contain any placeholders.
|
||||
if strings.Contains(cfg.Dir, ":") {
|
||||
return fmt.Errorf("cache dir %q contains unresolved placeholders", cfg.Dir)
|
||||
}
|
||||
cfg.DirCompiled = cfg.Dir
|
||||
}
|
||||
// Sanity check the config.
|
||||
if len(cfg.DirCompiled) < 5 {
|
||||
panic(fmt.Sprintf("invalid cache dir: %q", cfg.DirCompiled))
|
||||
}
|
||||
return nil
|
||||
// GetJSONCache gets the file cache for getJSON.
|
||||
func (f Caches) GetJSONCache() *Cache {
|
||||
return f[CacheKeyGetJSON]
|
||||
}
|
||||
|
||||
type fileCacheConfigInternal struct {
|
||||
DirCompiled string
|
||||
|
||||
name string // The name of this cache, e.g. "images", "modules" etc.
|
||||
entryIsDir bool // when set, the cache entries represents directories directly below the base dir.
|
||||
isReadOnly bool // when set, the cache is read only and needs to be pruned differently. This is used for the Go modules cache.
|
||||
IsResourceDir bool // resources/_gen will get its own composite filesystem that also checks any theme. TODO(bep) unexport this.
|
||||
}
|
||||
|
||||
// MarshalJSON marshals FileCacheConfig to JSON with MaxAge as a human-readable string.
|
||||
func (c FileCacheConfig) MarshalJSON() ([]byte, error) {
|
||||
var maxAge any
|
||||
if c.MaxAge == -1 {
|
||||
maxAge = -1
|
||||
} else {
|
||||
maxAge = strings.TrimSuffix(c.MaxAge.String(), "0m0s")
|
||||
}
|
||||
return json.Marshal(&struct {
|
||||
MaxAge any `json:"maxAge"`
|
||||
Dir string `json:"dir"`
|
||||
}{
|
||||
MaxAge: maxAge,
|
||||
Dir: c.Dir,
|
||||
})
|
||||
// GetCSVCache gets the file cache for getCSV.
|
||||
func (f Caches) GetCSVCache() *Cache {
|
||||
return f[CacheKeyGetCSV]
|
||||
}
|
||||
|
||||
// ImageCache gets the file cache for processed images.
|
||||
@@ -171,25 +120,6 @@ func (f Caches) ModulesCache() *Cache {
|
||||
return f[CacheKeyModules]
|
||||
}
|
||||
|
||||
// ModuleQueriesCache gets the file cache for Hugo Module version queries.
|
||||
// Returns nil if not found.
|
||||
func (f Caches) ModuleQueriesCache() *Cache {
|
||||
c, ok := f[CacheKeyModuleQueries]
|
||||
if !ok {
|
||||
panic("module queries cache not set")
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// ModuleGitInfoCache gets the file cache for Hugo Module git info.
|
||||
func (f Caches) ModuleGitInfoCache() *Cache {
|
||||
c, ok := f[CacheKeyModuleGitInfo]
|
||||
if !ok {
|
||||
panic("module git info cache not set")
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
|
||||
func (f Caches) AssetsCache() *Cache {
|
||||
return f[CacheKeyAssets]
|
||||
@@ -217,14 +147,10 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
_, isOsFs := fs.(*afero.OsFs)
|
||||
|
||||
for k, v := range m {
|
||||
if _, ok := v.(hmaps.Params); !ok {
|
||||
if _, ok := v.(maps.Params); !ok {
|
||||
continue
|
||||
}
|
||||
var ok bool
|
||||
cc, ok := c[k]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q is not a valid cache name", k)
|
||||
}
|
||||
cc := defaultCacheConfig
|
||||
|
||||
dc := &mapstructure.DecoderConfig{
|
||||
Result: &cc,
|
||||
@@ -245,8 +171,12 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
return c, errors.New("must provide cache Dir")
|
||||
}
|
||||
|
||||
c[k] = cc
|
||||
name := strings.ToLower(k)
|
||||
if !valid[name] {
|
||||
return nil, fmt.Errorf("%q is not a valid cache name", name)
|
||||
}
|
||||
|
||||
c[name] = cc
|
||||
}
|
||||
|
||||
for k, v := range c {
|
||||
|
||||
+14
-39
@@ -14,7 +14,6 @@
|
||||
package filecache_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
@@ -44,9 +43,12 @@ assetDir = "assets"
|
||||
archetypeDir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.misc]
|
||||
[caches.getJSON]
|
||||
maxAge = "10m"
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = "11h"
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
[caches.getResource]
|
||||
@@ -59,9 +61,9 @@ dir = "/path/to/c4"
|
||||
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
|
||||
c.Assert(len(decoded), qt.Equals, 7)
|
||||
|
||||
c2 := decoded["misc"]
|
||||
c.Assert(c2.MaxAge.String(), qt.Equals, "10m0s")
|
||||
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c1/filecache/misc"))
|
||||
c2 := decoded["getcsv"]
|
||||
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
|
||||
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv"))
|
||||
|
||||
c3 := decoded["images"]
|
||||
c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1))
|
||||
@@ -88,9 +90,12 @@ archeTypedir = "archetypes"
|
||||
|
||||
ignoreCache = true
|
||||
[caches]
|
||||
[caches.misc]
|
||||
[caches.getJSON]
|
||||
maxAge = 1234
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = 3456
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
[caches.getResource]
|
||||
@@ -127,45 +132,15 @@ func TestDecodeConfigDefault(t *testing.T) {
|
||||
c.Assert(len(decoded), qt.Equals, 7)
|
||||
|
||||
imgConfig := decoded[filecache.CacheKeyImages]
|
||||
miscConfig := decoded[filecache.CacheKeyMisc]
|
||||
jsonConfig := decoded[filecache.CacheKeyGetJSON]
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
c.Assert(imgConfig.DirCompiled, qt.Equals, filepath.FromSlash("_gen/images"))
|
||||
} else {
|
||||
c.Assert(imgConfig.DirCompiled, qt.Equals, "_gen/images")
|
||||
c.Assert(miscConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/misc")
|
||||
c.Assert(jsonConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson")
|
||||
}
|
||||
|
||||
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
|
||||
c.Assert(miscConfig.IsResourceDir, qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestFileCacheConfigMarshalJSON(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cfg := config.New()
|
||||
cfg.Set("cacheDir", "/cache")
|
||||
cfg.Set("workingDir", "/my/project")
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
|
||||
|
||||
moduleQueriesConfig := decoded[filecache.CacheKeyModuleQueries]
|
||||
c.Assert(moduleQueriesConfig.MaxAge, qt.Equals, 24*time.Hour)
|
||||
|
||||
// Also verify the new moduleGitInfo cache.
|
||||
moduleGitInfoConfig := decoded[filecache.CacheKeyModuleGitInfo]
|
||||
c.Assert(moduleGitInfoConfig.MaxAge, qt.Equals, 24*time.Hour)
|
||||
|
||||
b, err := json.Marshal(moduleQueriesConfig)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(string(b), qt.Contains, `"maxAge":"24h"`)
|
||||
c.Assert(string(b), qt.Not(qt.Contains), "86400000000000")
|
||||
c.Assert(string(b), qt.Not(qt.Contains), "8.64e")
|
||||
|
||||
moduleQueriesConfig.MaxAge = -1
|
||||
b, err = json.Marshal(moduleQueriesConfig)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(b), qt.Contains, `"maxAge":-1`)
|
||||
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
|
||||
}
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ title: "Home"
|
||||
---
|
||||
-- assets/a/pixel.png --
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
-- layouts/home.html --
|
||||
-- layouts/index.html --
|
||||
{{ warnf "HOME!" }}
|
||||
{{ $img := resources.GetMatch "**.png" }}
|
||||
{{ $img = $img.Resize "3x3" }}
|
||||
|
||||
Vendored
+7
-43
@@ -50,8 +50,8 @@ func (c Caches) Prune() (int, error) {
|
||||
// Prune removes expired and unused items from this cache.
|
||||
// If force is set, everything will be removed not considering expiry time.
|
||||
func (c *Cache) Prune(force bool) (int, error) {
|
||||
if c.cfg.entryIsDir {
|
||||
return c.pruneRootDirs(force)
|
||||
if c.pruneAllRootDir != "" {
|
||||
return c.pruneRootDir(force)
|
||||
}
|
||||
if err := c.init(); err != nil {
|
||||
return 0, err
|
||||
@@ -93,9 +93,9 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
|
||||
shouldRemove := force || c.isExpired(info.ModTime())
|
||||
|
||||
if !shouldRemove && len(c.entryLocker.seen) > 0 {
|
||||
if !shouldRemove && len(c.nlocker.seen) > 0 {
|
||||
// Remove it if it's not been touched/used in the last build.
|
||||
_, seen := c.entryLocker.seen[name]
|
||||
_, seen := c.nlocker.seen[name]
|
||||
shouldRemove = !seen
|
||||
}
|
||||
|
||||
@@ -117,43 +117,11 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
return counter, err
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDirs(force bool) (int, error) {
|
||||
dirs, err := afero.ReadDir(c.Fs, "")
|
||||
if err != nil {
|
||||
if herrors.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
counter := 0
|
||||
|
||||
for _, dir := range dirs {
|
||||
if !dir.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
count, err := c.pruneRootDir(dir.Name(), force)
|
||||
if err != nil {
|
||||
return counter, err
|
||||
}
|
||||
counter += count
|
||||
}
|
||||
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDir(dirname string, force bool) (int, error) {
|
||||
func (c *Cache) pruneRootDir(force bool) (int, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Sanity check.
|
||||
if dirname != "pkg" && len(dirname) < 5 {
|
||||
panic(fmt.Sprintf("invalid cache dir name: %q", dirname))
|
||||
}
|
||||
|
||||
info, err := c.Fs.Stat(dirname)
|
||||
info, err := c.Fs.Stat(c.pruneAllRootDir)
|
||||
if err != nil {
|
||||
if herrors.IsNotExist(err) {
|
||||
return 0, nil
|
||||
@@ -165,9 +133,5 @@ func (c *Cache) pruneRootDir(dirname string, force bool) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if c.cfg.isReadOnly {
|
||||
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, dirname)
|
||||
}
|
||||
|
||||
return 1, c.Fs.RemoveAll(dirname)
|
||||
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, c.pruneAllRootDir)
|
||||
}
|
||||
|
||||
+11
-12
@@ -39,9 +39,12 @@ assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.misc]
|
||||
[caches.getjson]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/c"
|
||||
[caches.getcsv]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/d"
|
||||
[caches.assets]
|
||||
maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
@@ -50,16 +53,13 @@ maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
`
|
||||
|
||||
for _, name := range []string{filecache.CacheKeyAssets, filecache.CacheKeyImages} {
|
||||
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
|
||||
msg := qt.Commentf("cache: %s", name)
|
||||
fs := afero.NewMemMapFs()
|
||||
p := newPathsSpec(t, fs, configStr)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
caches, err := filecache.NewCaches(fileCachConfig, fs)
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
caches, err := filecache.NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(fs)
|
||||
cache := caches[name]
|
||||
for i := range 10 {
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
cache.GetOrCreateBytes(id, func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
@@ -74,7 +74,7 @@ dir = ":resourceDir/_gen"
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 5, msg)
|
||||
|
||||
for i := range 10 {
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
if i < 5 {
|
||||
@@ -84,9 +84,8 @@ dir = ":resourceDir/_gen"
|
||||
}
|
||||
}
|
||||
|
||||
caches, err = filecache.NewCaches(fileCachConfig, fs)
|
||||
caches, err = filecache.NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(fs)
|
||||
cache = caches[name]
|
||||
// Touch one and then prune.
|
||||
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
|
||||
@@ -98,7 +97,7 @@ dir = ":resourceDir/_gen"
|
||||
c.Assert(count, qt.Equals, 4)
|
||||
|
||||
// Now only the i5 should be left.
|
||||
for i := range 10 {
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
if i != 5 {
|
||||
|
||||
Vendored
+17
-26
@@ -22,8 +22,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -66,7 +64,7 @@ assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.misc]
|
||||
[caches.getJSON]
|
||||
maxAge = "10h"
|
||||
dir = ":cacheDir/c"
|
||||
|
||||
@@ -80,13 +78,11 @@ dir = ":cacheDir/c"
|
||||
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
|
||||
|
||||
p := newPathsSpec(t, osfs, configStr)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
|
||||
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
|
||||
caches, err := filecache.NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(p.SourceFs)
|
||||
|
||||
cache := caches.Get("Misc")
|
||||
cache := caches.Get("GetJSON")
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
|
||||
cache = caches.Get("Images")
|
||||
@@ -108,8 +104,8 @@ dir = ":cacheDir/c"
|
||||
return []byte("bcd"), nil
|
||||
}
|
||||
|
||||
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache()} {
|
||||
for range 2 {
|
||||
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
|
||||
for i := 0; i < 2; i++ {
|
||||
info, r, err := ca.GetOrCreate("a", rf("abc"))
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(r, qt.Not(qt.IsNil))
|
||||
@@ -136,6 +132,8 @@ dir = ":cacheDir/c"
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(caches.Get("getJSON"), qt.Not(qt.IsNil))
|
||||
|
||||
info, w, err := caches.ImageCache().WriteCloser("mykey")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(info.Name, qt.Equals, "mykey")
|
||||
@@ -151,7 +149,7 @@ dir = ":cacheDir/c"
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
|
||||
info, b, err = caches.ImageCache().GetItemBytes("mykey")
|
||||
info, b, err = caches.ImageCache().GetBytes("mykey")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(info.Name, qt.Equals, "mykey")
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
@@ -160,7 +158,6 @@ dir = ":cacheDir/c"
|
||||
}
|
||||
|
||||
func TestFileCacheConcurrent(t *testing.T) {
|
||||
htesting.SkipSlowTestUnlessCI(t)
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
@@ -175,19 +172,18 @@ assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.misc]
|
||||
[caches.getjson]
|
||||
maxAge = "1s"
|
||||
dir = "/cache/c"
|
||||
|
||||
`
|
||||
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(p.Fs.Source)
|
||||
|
||||
const cacheName = "misc"
|
||||
caches, err := filecache.NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
const cacheName = "getjson"
|
||||
|
||||
filenameData := func(i int) (string, string) {
|
||||
data := fmt.Sprintf("data: %d", i)
|
||||
@@ -197,11 +193,11 @@ dir = "/cache/c"
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range 50 {
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for range 20 {
|
||||
for j := 0; j < 20; j++ {
|
||||
ca := caches.Get(cacheName)
|
||||
c.Assert(ca, qt.Not(qt.IsNil))
|
||||
filename, data := filenameData(i)
|
||||
@@ -252,12 +248,7 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
cfg := filecache.FileCacheConfig{
|
||||
MaxAge: 100 * time.Hour,
|
||||
Dir: "cache/c",
|
||||
}
|
||||
|
||||
cache := filecache.NewCache(afero.NewMemMapFs(), cfg)
|
||||
cache := filecache.NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
|
||||
|
||||
const id = "a32"
|
||||
|
||||
@@ -279,7 +270,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
c.Assert(err, qt.IsNil)
|
||||
acfg := testconfig.GetTestConfig(fs, cfg)
|
||||
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil, nil)
|
||||
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
}
|
||||
|
||||
Vendored
+12
-45
@@ -25,8 +25,6 @@ import (
|
||||
|
||||
// DefaultConfig holds the default configuration for the HTTP cache.
|
||||
var DefaultConfig = Config{
|
||||
RespectCacheControlNoStoreInRequest: true,
|
||||
RespectCacheControlNoStoreInResponse: false,
|
||||
Cache: Cache{
|
||||
For: GlobMatcher{
|
||||
Excludes: []string{"**"},
|
||||
@@ -44,13 +42,7 @@ var DefaultConfig = Config{
|
||||
|
||||
// Config holds the configuration for the HTTP cache.
|
||||
type Config struct {
|
||||
// When enabled and there's a Cache-Control: no-store directive in the request, response will never be stored in disk cache.
|
||||
RespectCacheControlNoStoreInRequest bool
|
||||
|
||||
// When enabled and there's a Cache-Control: no-store directive in the response, response will never be stored in disk cache.
|
||||
RespectCacheControlNoStoreInResponse bool
|
||||
|
||||
// Enables HTTP cache behavior (RFC 9111) for these resources.
|
||||
// Configures the HTTP cache behaviour (RFC 9111).
|
||||
// When this is not enabled for a resource, Hugo will go straight to the file cache.
|
||||
Cache Cache
|
||||
|
||||
@@ -60,14 +52,12 @@ type Config struct {
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
// Enable HTTP cache behavior (RFC 9111) for these resources.
|
||||
// Enable HTTP cache behaviour (RFC 9111) for these rsources.
|
||||
For GlobMatcher
|
||||
}
|
||||
|
||||
func (c *Config) Compile() (ConfigCompiled, error) {
|
||||
cc := ConfigCompiled{
|
||||
Base: *c,
|
||||
}
|
||||
var cc ConfigCompiled
|
||||
|
||||
p, err := c.Cache.For.CompilePredicate()
|
||||
if err != nil {
|
||||
@@ -132,12 +122,7 @@ type GlobMatcher struct {
|
||||
Includes []string
|
||||
}
|
||||
|
||||
func (gm GlobMatcher) IsZero() bool {
|
||||
return len(gm.Includes) == 0 && len(gm.Excludes) == 0
|
||||
}
|
||||
|
||||
type ConfigCompiled struct {
|
||||
Base Config
|
||||
For predicate.P[string]
|
||||
PollConfigs []PollConfigCompiled
|
||||
}
|
||||
@@ -170,19 +155,16 @@ func (p PollConfigCompiled) IsZero() bool {
|
||||
}
|
||||
|
||||
func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
|
||||
if gm.IsZero() {
|
||||
panic("no includes or excludes")
|
||||
}
|
||||
var b predicate.PR[string]
|
||||
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) predicate.Match {
|
||||
return predicate.BoolMatch(g.Match(s))
|
||||
fn := func(s string) bool {
|
||||
return g.Match(s)
|
||||
}
|
||||
b = b.Or(fn)
|
||||
p = p.Or(fn)
|
||||
}
|
||||
|
||||
for _, exclude := range gm.Excludes {
|
||||
@@ -190,16 +172,16 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fn := func(s string) predicate.Match {
|
||||
return predicate.BoolMatch(!g.Match(s))
|
||||
fn := func(s string) bool {
|
||||
return !g.Match(s)
|
||||
}
|
||||
b = b.And(fn)
|
||||
p = p.And(fn)
|
||||
}
|
||||
|
||||
return b.BoolFunc(), nil
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
|
||||
func DecodeConfig(bcfg config.BaseConfig, m map[string]any) (Config, error) {
|
||||
if len(m) == 0 {
|
||||
return DefaultConfig, nil
|
||||
}
|
||||
@@ -221,20 +203,5 @@ func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
|
||||
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
|
||||
}
|
||||
|
||||
-31
@@ -22,8 +22,6 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigCustom(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[httpcache]
|
||||
@@ -53,8 +51,6 @@ includes = ["**gohugo.io**"]
|
||||
}
|
||||
|
||||
func TestConfigDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
`
|
||||
@@ -66,30 +62,3 @@ func TestConfigDefault(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
Vendored
-31
@@ -17,7 +17,6 @@ import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
)
|
||||
|
||||
func TestGlobMatcher(t *testing.T) {
|
||||
@@ -41,33 +40,3 @@ func TestGlobMatcher(t *testing.T) {
|
||||
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]any{})
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Default to all packages if none specified
|
||||
PACKAGES="${1:-./...}"
|
||||
|
||||
echo "==> Checking packages: $PACKAGES"
|
||||
|
||||
# Timing arrays
|
||||
declare -a STEP_NAMES
|
||||
declare -a STEP_TIMES
|
||||
|
||||
time_step() {
|
||||
local name="$1"
|
||||
shift
|
||||
local start=$(date +%s.%N)
|
||||
"$@"
|
||||
local end=$(date +%s.%N)
|
||||
local elapsed=$(echo "$end - $start" | bc)
|
||||
STEP_NAMES+=("$name")
|
||||
STEP_TIMES+=("$elapsed")
|
||||
}
|
||||
|
||||
# Check gofmt
|
||||
run_gofmt() {
|
||||
echo "==> Running gofmt..."
|
||||
# Convert package pattern to path (e.g., ./hugolib/... -> ./hugolib)
|
||||
local path="${PACKAGES%/...}"
|
||||
GOFMT_OUTPUT=$(gofmt -l "$path" 2>&1) || true
|
||||
if [ -n "$GOFMT_OUTPUT" ]; then
|
||||
echo "gofmt found issues in:"
|
||||
echo "$GOFMT_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
# Run staticcheck
|
||||
run_staticcheck() {
|
||||
# Check if staticcheck is installed, install if not
|
||||
if ! command -v staticcheck &> /dev/null; then
|
||||
echo "==> Installing staticcheck..."
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
fi
|
||||
echo "==> Running staticcheck..."
|
||||
staticcheck $PACKAGES
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
# Run tests
|
||||
run_tests() {
|
||||
echo "==> Running tests..."
|
||||
local output
|
||||
if ! output=$(go test -failfast $PACKAGES 2>&1); then
|
||||
echo "$output"
|
||||
exit 1
|
||||
fi
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
# Run all steps with timing
|
||||
TOTAL_START=$(date +%s.%N)
|
||||
|
||||
time_step "gofmt" run_gofmt
|
||||
time_step "staticcheck" run_staticcheck
|
||||
time_step "tests" run_tests
|
||||
|
||||
TOTAL_END=$(date +%s.%N)
|
||||
TOTAL_ELAPSED=$(echo "$TOTAL_END - $TOTAL_START" | bc)
|
||||
|
||||
# Print timing summary
|
||||
echo ""
|
||||
echo "==> All checks passed!"
|
||||
echo ""
|
||||
echo "Timing summary:"
|
||||
echo "---------------"
|
||||
for i in "${!STEP_NAMES[@]}"; do
|
||||
printf " %-15s %6.2fs\n" "${STEP_NAMES[$i]}" "${STEP_TIMES[$i]}"
|
||||
done
|
||||
echo "---------------"
|
||||
printf " %-15s %6.2fs\n" "Total" "$TOTAL_ELAPSED"
|
||||
+8
-9
@@ -26,7 +26,6 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -73,7 +72,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
nameAndPackage := func(t reflect.Type) (string, string) {
|
||||
var name, pkg string
|
||||
|
||||
isPointer := t.Kind() == reflect.Pointer
|
||||
isPointer := t.Kind() == reflect.Ptr
|
||||
|
||||
if isPointer {
|
||||
t = t.Elem()
|
||||
@@ -103,7 +102,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
}
|
||||
|
||||
for _, t := range include {
|
||||
for i := range t.NumMethod() {
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
|
||||
m := t.Method(i)
|
||||
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}
|
||||
|
||||
for i := range numIn {
|
||||
for i := 0; i < numIn; i++ {
|
||||
in := m.Type.In(i)
|
||||
|
||||
name, pkg := nameAndPackage(in)
|
||||
@@ -138,7 +137,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
numOut := m.Type.NumOut()
|
||||
|
||||
if numOut > 0 {
|
||||
for i := range numOut {
|
||||
for i := 0; i < numOut; i++ {
|
||||
out := m.Type.Out(i)
|
||||
name, pkg := nameAndPackage(out)
|
||||
|
||||
@@ -305,7 +304,7 @@ func (m Method) inOutStr() string {
|
||||
}
|
||||
|
||||
args := make([]string, len(m.In))
|
||||
for i := range args {
|
||||
for i := 0; i < len(args); i++ {
|
||||
args[i] = fmt.Sprintf("arg%d", i)
|
||||
}
|
||||
return "(" + strings.Join(args, ", ") + ")"
|
||||
@@ -317,7 +316,7 @@ func (m Method) inStr() string {
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
return "(" + strings.Join(args, ", ") + ")"
|
||||
@@ -340,7 +339,7 @@ func (m Method) outStrNamed() string {
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
@@ -436,7 +435,7 @@ func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (st
|
||||
// Exclude self
|
||||
for i, pkgImp := range pkgImports {
|
||||
if pkgImp == pkgPath {
|
||||
pkgImports = slices.Delete(pkgImports, i, i+1)
|
||||
pkgImports = append(pkgImports[:i], pkgImports[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ import (
|
||||
|
||||
func TestMethods(t *testing.T) {
|
||||
var (
|
||||
zeroIE = reflect.TypeFor[IEmbed]()
|
||||
zeroIEOnly = reflect.TypeFor[IEOnly]()
|
||||
zeroI = reflect.TypeFor[I]()
|
||||
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
|
||||
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
|
||||
zeroI = reflect.TypeOf((*I)(nil)).Elem()
|
||||
)
|
||||
|
||||
dir, _ := os.Getwd()
|
||||
|
||||
+44
-46
@@ -39,6 +39,7 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
@@ -97,15 +98,13 @@ type commonConfig struct {
|
||||
type configKey struct {
|
||||
counter int32
|
||||
ignoreModulesDoesNotExists bool
|
||||
skipNpmCheck bool
|
||||
}
|
||||
|
||||
// This is the root command.
|
||||
type rootCommand struct {
|
||||
Printf func(format string, v ...any)
|
||||
Println func(a ...any)
|
||||
StdOut io.Writer
|
||||
StdErr io.Writer
|
||||
Printf func(format string, v ...interface{})
|
||||
Println func(a ...interface{})
|
||||
Out io.Writer
|
||||
|
||||
logger loggers.Logger
|
||||
|
||||
@@ -132,7 +131,6 @@ type rootCommand struct {
|
||||
gc bool
|
||||
poll string
|
||||
forceSyncStatic bool
|
||||
panicOnWarning bool
|
||||
|
||||
// Profile flags (for debugging of performance problems)
|
||||
cpuprofile string
|
||||
@@ -143,6 +141,8 @@ type rootCommand struct {
|
||||
|
||||
logLevel string
|
||||
|
||||
verbose bool
|
||||
debug bool
|
||||
quiet bool
|
||||
devMode bool // Hidden flag.
|
||||
|
||||
@@ -196,7 +196,6 @@ func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*c
|
||||
Logger: r.logger,
|
||||
Environment: r.environment,
|
||||
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
|
||||
SkipNpmCheck: key.skipNpmCheck,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -253,7 +252,6 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
|
||||
Environment: r.environment,
|
||||
Logger: r.logger,
|
||||
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
|
||||
SkipNpmCheck: key.skipNpmCheck,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -335,9 +333,6 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
|
||||
}
|
||||
|
||||
func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
|
||||
if conf == nil {
|
||||
return nil, fmt.Errorf("conf must be set")
|
||||
}
|
||||
k := configKey{counter: r.configVersionID.Load()}
|
||||
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
|
||||
depsCfg := r.newDepsConfig(conf)
|
||||
@@ -364,7 +359,7 @@ func (r *rootCommand) getOrCreateHugo(cfg config.Provider, ignoreModuleDoesNotEx
|
||||
}
|
||||
|
||||
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}
|
||||
return deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, LogOut: r.logger.Out(), LogLevel: r.logger.Level(), ChangesFromBuild: r.changesFromBuild}
|
||||
}
|
||||
|
||||
func (r *rootCommand) Name() string {
|
||||
@@ -408,7 +403,9 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
|
||||
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
|
||||
|
||||
r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
|
||||
for _, group := range watchGroups {
|
||||
r.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
watcher, err := b.newWatcher(r.poll, watchDirs...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -427,23 +424,21 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
}
|
||||
|
||||
func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
r.StdOut = os.Stdout
|
||||
r.StdErr = os.Stderr
|
||||
r.Out = os.Stdout
|
||||
if r.quiet {
|
||||
r.StdOut = io.Discard
|
||||
r.StdErr = io.Discard
|
||||
r.Out = io.Discard
|
||||
}
|
||||
// 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 {
|
||||
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 {
|
||||
fmt.Fprintln(r.StdOut, a...)
|
||||
fmt.Fprintln(r.Out, a...)
|
||||
}
|
||||
}
|
||||
_, running := runner.Command.(*serverCommand)
|
||||
@@ -452,8 +447,6 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
@@ -489,21 +482,26 @@ func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
|
||||
default:
|
||||
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
|
||||
}
|
||||
|
||||
var logHookLast func(e *logg.Entry) error
|
||||
if r.panicOnWarning {
|
||||
logHookLast = loggers.PanicOnWarningHook
|
||||
if r.debug {
|
||||
hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
|
||||
level = logg.LevelDebug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
optsLogger := loggers.Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: level,
|
||||
StdOut: r.StdOut,
|
||||
StdErr: r.StdErr,
|
||||
Stdout: r.Out,
|
||||
Stderr: r.Out,
|
||||
StoreErrors: running,
|
||||
HandlerPost: logHookLast,
|
||||
}
|
||||
|
||||
return loggers.New(optsLogger), nil
|
||||
@@ -530,8 +528,8 @@ func (r *rootCommand) initRootCommand(subCommandName string, cd *simplecobra.Com
|
||||
commandName = subCommandName
|
||||
}
|
||||
cmd.Use = fmt.Sprintf("%s [flags]", commandName)
|
||||
cmd.Short = "Build your project"
|
||||
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo project.
|
||||
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
|
||||
built with love by spf13 and friends in Go.
|
||||
@@ -551,7 +549,6 @@ Complete documentation is available at https://gohugo.io/.`
|
||||
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
|
||||
_ = cmd.MarkFlagDirname("themesDir")
|
||||
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.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)
|
||||
@@ -563,6 +560,8 @@ Complete documentation is available at https://gohugo.io/.`
|
||||
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.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().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("logLevel", cobra.FixedCompletions([]string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
@@ -601,12 +600,13 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolVar(&r.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
|
||||
cmd.Flags().StringVar(&r.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
|
||||
_ = cmd.RegisterFlagCompletionFunc("poll", cobra.NoFileCompletions)
|
||||
cmd.Flags().BoolVar(&r.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
|
||||
cmd.Flags().Bool("panicOnWarning", false, "panic on first WARNING log")
|
||||
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
|
||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||
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("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("printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
|
||||
cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.")
|
||||
@@ -632,14 +632,13 @@ func (r *rootCommand) timeTrack(start time.Time, name string) {
|
||||
}
|
||||
|
||||
type simpleCommand struct {
|
||||
use string
|
||||
name string
|
||||
short string
|
||||
long string
|
||||
aliases []string
|
||||
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
|
||||
withc func(cmd *cobra.Command, r *rootCommand)
|
||||
initc func(cd *simplecobra.Commandeer) error
|
||||
use string
|
||||
name string
|
||||
short string
|
||||
long string
|
||||
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
|
||||
withc func(cmd *cobra.Command, r *rootCommand)
|
||||
initc func(cd *simplecobra.Commandeer) error
|
||||
|
||||
commands []simplecobra.Commander
|
||||
|
||||
@@ -666,7 +665,6 @@ func (c *simpleCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = c.short
|
||||
cmd.Long = c.long
|
||||
cmd.Aliases = c.aliases
|
||||
if c.use != "" {
|
||||
cmd.Use = c.use
|
||||
}
|
||||
@@ -684,7 +682,7 @@ func (c *simpleCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
}
|
||||
|
||||
func mapLegacyArgs(args []string) []string {
|
||||
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "project", "site", "theme", "content") {
|
||||
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "site", "theme", "content") {
|
||||
// Insert "content" as the second argument
|
||||
args = append(args[:1], append([]string{"content"}, args[1:]...)...)
|
||||
}
|
||||
|
||||
+13
-16
@@ -23,9 +23,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config/allconfig"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
@@ -44,9 +43,8 @@ func newConfigCommand() *configCommand {
|
||||
type configCommand struct {
|
||||
r *rootCommand
|
||||
|
||||
format string
|
||||
lang string
|
||||
printZero bool
|
||||
format string
|
||||
lang string
|
||||
|
||||
commands []simplecobra.Commander
|
||||
}
|
||||
@@ -72,7 +70,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
return fmt.Errorf("language %q not found", c.lang)
|
||||
}
|
||||
} else {
|
||||
config = conf.configs.LanguageConfigMap[conf.configs.Base.DefaultContentLanguage]
|
||||
config = conf.configs.LanguageConfigSlice[0]
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -80,7 +78,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
dec.SetIndent("", " ")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -91,11 +89,11 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
default:
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
hmaps.ConvertFloat64WithNoDecimalsToInt(m)
|
||||
maps.ConvertFloat64WithNoDecimalsToInt(m)
|
||||
switch format {
|
||||
case "yaml":
|
||||
return parser.InterfaceToConfig(m, metadecoders.YAML, os.Stdout)
|
||||
@@ -112,12 +110,11 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
c.r = cd.Root.Command.(*rootCommand)
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Display project configuration"
|
||||
cmd.Long = `Display project configuration, both default and custom settings.`
|
||||
cmd.Short = "Display site configuration"
|
||||
cmd.Long = `Display site configuration, both default and custom settings.`
|
||||
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.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)
|
||||
applyLocalFlagsBuildConfig(cmd, c.r)
|
||||
|
||||
@@ -129,9 +126,9 @@ func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
}
|
||||
|
||||
type configModMount struct {
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
Sites sitesmatrix.Sites `json:"sites,omitzero"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
Lang string `json:"lang,omitempty"`
|
||||
}
|
||||
|
||||
type configModMounts struct {
|
||||
@@ -147,7 +144,7 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
|
||||
mounts = append(mounts, configModMount{
|
||||
Source: mount.Source,
|
||||
Target: mount.Target,
|
||||
Sites: mount.Sites,
|
||||
Lang: mount.Lang,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -202,56 +200,6 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *convertCommand) copyContentDirsForOutput(pagesBackedByFile page.Pages) error {
|
||||
contentDirs := make(map[string]bool)
|
||||
for _, p := range pagesBackedByFile {
|
||||
filename := p.File().Filename()
|
||||
contentDir := strings.TrimSuffix(filename, p.File().Path())
|
||||
if contentDir == filename {
|
||||
continue
|
||||
}
|
||||
contentDirs[filepath.Clean(contentDir)] = true
|
||||
}
|
||||
|
||||
var contentDirList []string
|
||||
for contentDir := range contentDirs {
|
||||
contentDirList = append(contentDirList, contentDir)
|
||||
}
|
||||
slices.Sort(contentDirList)
|
||||
|
||||
outputDirAbs, err := filepath.Abs(c.outputDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve output path %q: %w", c.outputDir, err)
|
||||
}
|
||||
|
||||
for _, contentDir := range contentDirList {
|
||||
outputContentDirAbs := filepath.Join(outputDirAbs, filepath.Base(contentDir))
|
||||
|
||||
skipDirs := make(map[string]bool)
|
||||
relToOutputDir, err := filepath.Rel(contentDir, outputDirAbs)
|
||||
if err == nil && relToOutputDir != ".." && !strings.HasPrefix(relToOutputDir, ".."+string(filepath.Separator)) {
|
||||
skipDirs[filepath.Clean(outputDirAbs)] = true
|
||||
}
|
||||
relToOutputContentDir, err := filepath.Rel(contentDir, outputContentDirAbs)
|
||||
if err == nil && relToOutputContentDir != ".." && !strings.HasPrefix(relToOutputContentDir, ".."+string(filepath.Separator)) {
|
||||
skipDirs[filepath.Clean(outputContentDirAbs)] = true
|
||||
}
|
||||
|
||||
var shouldCopy func(filename string) bool
|
||||
if len(skipDirs) > 0 {
|
||||
shouldCopy = func(filename string) bool {
|
||||
return !skipDirs[filepath.Clean(filename)]
|
||||
}
|
||||
}
|
||||
|
||||
if err := hugio.CopyDir(hugofs.Os, contentDir, outputContentDirAbs, shouldCopy); err != nil {
|
||||
return fmt.Errorf("failed to copy %q to %q: %w", contentDir, outputContentDirAbs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *convertCommand) convertContents(format metadecoders.Format) error {
|
||||
if c.outputDir == "" && !c.unsafe {
|
||||
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
|
||||
@@ -271,12 +219,6 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error {
|
||||
pagesBackedByFile = append(pagesBackedByFile, p)
|
||||
}
|
||||
|
||||
if c.outputDir != "" {
|
||||
if err := c.copyContentDirsForOutput(pagesBackedByFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
site.Log.Println("processing", len(pagesBackedByFile), "content files")
|
||||
for _, p := range site.AllPages() {
|
||||
if err := c.convertAndSavePage(p, site, format); err != nil {
|
||||
|
||||
+15
-3
@@ -12,6 +12,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build withdeploy
|
||||
// +build withdeploy
|
||||
|
||||
package commands
|
||||
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/gohugoio/hugo/deploy"
|
||||
"github.com/gohugoio/hugo/deploy/deployconfig"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -27,8 +29,8 @@ import (
|
||||
func newDeployCommand() simplecobra.Commander {
|
||||
return &simpleCommand{
|
||||
name: "deploy",
|
||||
short: "Deploy your project to a cloud provider",
|
||||
long: `Deploy your project to a cloud provider
|
||||
short: "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
|
||||
documentation.
|
||||
@@ -45,7 +47,17 @@ documentation.
|
||||
return deployer.Deploy(ctx)
|
||||
},
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !withdeploy
|
||||
// +build !withdeploy
|
||||
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
@@ -43,7 +44,6 @@ func newDeployCommand() simplecobra.Commander {
|
||||
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")
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
applyDeployFlags(cmd, r)
|
||||
cmd.Hidden = true
|
||||
},
|
||||
}
|
||||
|
||||
+11
-33
@@ -21,14 +21,12 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/docshelper"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
@@ -37,6 +35,7 @@ import (
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func newGenCommand() *genCommand {
|
||||
@@ -50,8 +49,6 @@ func newGenCommand() *genCommand {
|
||||
highlightStyle string
|
||||
lineNumbersInlineStyle string
|
||||
lineNumbersTableStyle string
|
||||
omitEmpty bool
|
||||
omitClassComments bool
|
||||
)
|
||||
|
||||
newChromaStyles := func() simplecobra.Commander {
|
||||
@@ -60,13 +57,9 @@ func newGenCommand() *genCommand {
|
||||
short: "Generate CSS stylesheet for the Chroma code highlighter",
|
||||
long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
|
||||
|
||||
See https://gohugo.io/quick-reference/syntax-highlighting-styles/ 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 {
|
||||
style = strings.ToLower(style)
|
||||
if !slices.Contains(styles.Names(), style) {
|
||||
return fmt.Errorf("invalid style: %s", style)
|
||||
}
|
||||
builder := styles.Get(style).Builder()
|
||||
if highlightStyle != "" {
|
||||
builder.Add(chroma.LineHighlight, highlightStyle)
|
||||
@@ -81,24 +74,13 @@ See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if omitEmpty {
|
||||
// See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
|
||||
hugo.Deprecate("--omitEmpty", "Flag is no longer needed, empty classes are now always omitted.", "v0.149.0")
|
||||
}
|
||||
options := []html.Option{
|
||||
html.WithCSSComments(!omitClassComments),
|
||||
}
|
||||
formatter := html.New(options...)
|
||||
|
||||
w := os.Stdout
|
||||
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
|
||||
formatter.WriteCSS(w, style)
|
||||
formatter := html.New(html.WithAllClasses(true))
|
||||
formatter.WriteCSS(os.Stdout, style)
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style")
|
||||
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
|
||||
@@ -106,10 +88,6 @@ See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview
|
||||
_ = 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.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules (deprecated, no longer needed)`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().BoolVar(&omitClassComments, "omitClassComments", false, `omit CSS class comment prefixes in the generated CSS`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("omitClassComments", cobra.NoFileCompletions)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -164,7 +142,7 @@ url: %s
|
||||
|
||||
return &simpleCommand{
|
||||
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.
|
||||
This command is, mostly, used to create up-to-date documentation
|
||||
of Hugo's command-line interface for https://gohugo.io/.
|
||||
@@ -189,13 +167,13 @@ url: %s
|
||||
prepender := func(filename string) string {
|
||||
name := filepath.Base(filename)
|
||||
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)
|
||||
}
|
||||
|
||||
linkHandler := func(name string) string {
|
||||
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, "...")
|
||||
doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler)
|
||||
@@ -216,7 +194,7 @@ url: %s
|
||||
newDocsHelper := func() simplecobra.Commander {
|
||||
return &simpleCommand{
|
||||
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 {
|
||||
r.Println("Generate docs data to", docsHelperTarget)
|
||||
@@ -237,7 +215,7 @@ url: %s
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
@@ -249,7 +227,7 @@ url: %s
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
yamlEnc := yaml.NewEncoder(f, yaml.UseSingleQuote(true), yaml.AutoInt())
|
||||
yamlEnc := yaml.NewEncoder(f)
|
||||
if err := yamlEnc.Encode(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+2
-2
@@ -76,12 +76,12 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
|
||||
|
||||
// Flags with a different name in the config.
|
||||
keyMap := map[string]string{
|
||||
"minify": "minify.minifyOutput",
|
||||
"minify": "minifyOutput",
|
||||
"destination": "publishDir",
|
||||
"editor": "newContentEditor",
|
||||
}
|
||||
|
||||
// Flags that we for some reason don't want to expose in the project config.
|
||||
// Flags that we for some reason don't want to expose in the site config.
|
||||
internalKeySet := map[string]bool{
|
||||
"quiet": true,
|
||||
"verbose": true,
|
||||
|
||||
+31
-90
@@ -27,15 +27,13 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
@@ -56,16 +54,15 @@ import (
|
||||
type hugoBuilder struct {
|
||||
r *rootCommand
|
||||
|
||||
confmu sync.Mutex
|
||||
confOld *commonConfig
|
||||
conf *commonConfig
|
||||
confmu sync.Mutex
|
||||
conf *commonConfig
|
||||
|
||||
// May be nil.
|
||||
s *serverCommand
|
||||
|
||||
// Currently only set when in "fast render mode".
|
||||
changeDetector *fileChangeDetector
|
||||
visitedURLs *types.EvictingQueue[string]
|
||||
visitedURLs *types.EvictingStringQueue
|
||||
|
||||
fullRebuildSem *semaphore.Weighted
|
||||
debounce func(f func())
|
||||
@@ -95,27 +92,6 @@ func (c *hugoBuilder) withConf(fn func(conf *commonConfig)) {
|
||||
fn(c.conf)
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) withConfOrOldConf(fn func(conf *commonConfig)) {
|
||||
c.confmu.Lock()
|
||||
defer c.confmu.Unlock()
|
||||
if c.conf != nil {
|
||||
fn(c.conf)
|
||||
} else if c.confOld != nil {
|
||||
fn(c.confOld)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) withConfOrOldConfE(fn func(conf *commonConfig) error) error {
|
||||
c.confmu.Lock()
|
||||
defer c.confmu.Unlock()
|
||||
if c.conf != nil {
|
||||
return fn(c.conf)
|
||||
} else if c.confOld != nil {
|
||||
return fn(c.confOld)
|
||||
}
|
||||
return errConfigNotSet
|
||||
}
|
||||
|
||||
type hugoBuilderErrState struct {
|
||||
mu sync.Mutex
|
||||
paused bool
|
||||
@@ -166,7 +142,7 @@ func (c *hugoBuilder) getDirList() ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hstrings.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
|
||||
return helpers.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) initCPUProfile() (func(), error) {
|
||||
@@ -179,7 +155,6 @@ func (c *hugoBuilder) initCPUProfile() (func(), error) {
|
||||
return nil, fmt.Errorf("failed to create CPU profile: %w", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("failed to start CPU profile: %w", err)
|
||||
}
|
||||
return func() {
|
||||
@@ -487,15 +462,7 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint
|
||||
infol.Logf("removing all files from destination that don't exist in static dirs")
|
||||
|
||||
syncer.DeleteFilter = func(f fsync.FileInfo) bool {
|
||||
name := f.Name()
|
||||
|
||||
// Keep .gitignore and .gitattributes anywhere
|
||||
if name == ".gitignore" || name == ".gitattributes" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Keep Hugo's original dot-directory behavior
|
||||
return f.IsDir() && strings.HasPrefix(name, ".")
|
||||
return f.IsDir() && strings.HasPrefix(f.Name(), ".")
|
||||
}
|
||||
}
|
||||
start := time.Now()
|
||||
@@ -548,14 +515,6 @@ func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFiles
|
||||
return langCount, nil
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) progressIntermediate() {
|
||||
terminal.ReportProgress(c.r.StdOut, terminal.ProgressIntermediate, 0)
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) progressHidden() {
|
||||
terminal.ReportProgress(c.r.StdOut, terminal.ProgressHidden, 0)
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
|
||||
var (
|
||||
g errgroup.Group
|
||||
@@ -704,20 +663,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
var n int
|
||||
for _, ev := range evs {
|
||||
keep := true
|
||||
// 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 {
|
||||
keep = false
|
||||
} else if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
|
||||
if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
|
||||
if _, err := os.Stat(ev.Name); err != nil {
|
||||
keep = false
|
||||
}
|
||||
@@ -859,7 +805,22 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
continue
|
||||
}
|
||||
|
||||
walkAdder := func(ctx context.Context, path string, f hugofs.FileMetaInfo) error {
|
||||
// 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 {
|
||||
if f.IsDir() {
|
||||
c.r.logger.Println("adding created directory to watchlist", path)
|
||||
if err := watcher.Add(path); err != nil {
|
||||
@@ -959,11 +920,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
|
||||
changed := c.changeDetector.changed()
|
||||
if c.changeDetector != nil {
|
||||
if len(changed) >= 10 {
|
||||
lrl.Logf("build changed %d files", len(changed))
|
||||
} else {
|
||||
lrl.Logf("build changed %d files: %q", len(changed), changed)
|
||||
}
|
||||
lrl.Logf("build changed %d files", len(changed))
|
||||
if len(changed) == 0 {
|
||||
// Nothing has changed.
|
||||
return
|
||||
@@ -1003,19 +960,16 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
lrl.Logf("no page to navigate to, force refresh")
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
} else if len(otherChanges) > 0 || len(cssChanges) > 0 {
|
||||
} 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 {
|
||||
} else if len(cssChanges) == 0 {
|
||||
lrl.Logf("force refresh")
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
} else {
|
||||
lrl.Logf("force refresh")
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
|
||||
if len(cssChanges) > 0 {
|
||||
@@ -1068,17 +1022,6 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
|
||||
if terminal.PrintANSIColors(os.Stdout) {
|
||||
defer c.progressHidden()
|
||||
// If the configuration takes a while to load, we want to show some progress.
|
||||
// This is typically loading of external modules.
|
||||
d := debounce.New(500 * time.Millisecond)
|
||||
d(func() {
|
||||
c.progressIntermediate()
|
||||
})
|
||||
defer d(func() {})
|
||||
}
|
||||
|
||||
cfg := config.New()
|
||||
cfg.Set("renderToMemory", c.r.renderToMemory)
|
||||
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
|
||||
@@ -1100,7 +1043,7 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
}
|
||||
cfg.Set("environment", c.r.environment)
|
||||
|
||||
cfg.Set("internal", hmaps.Params{
|
||||
cfg.Set("internal", maps.Params{
|
||||
"running": running,
|
||||
"watch": watch,
|
||||
"verbose": c.r.isVerbose(),
|
||||
@@ -1114,11 +1057,10 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
|
||||
if len(conf.configs.LoadingInfo.ConfigFiles) == 0 {
|
||||
//lint:ignore ST1005 end user message.
|
||||
return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new project.\nRun `hugo help new` for details.")
|
||||
return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\nRun `hugo help new` for details.")
|
||||
}
|
||||
|
||||
c.conf = conf
|
||||
c.confOld = conf
|
||||
if c.onConfigLoaded != nil {
|
||||
if err := c.onConfigLoaded(false); err != nil {
|
||||
return err
|
||||
@@ -1157,7 +1099,7 @@ func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...)
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1173,7 +1115,7 @@ func (c *hugoBuilder) rebuildSitesForChanges(ids []identity.Identity) (err error
|
||||
}
|
||||
whatChanged := &hugolib.WhatChanged{}
|
||||
whatChanged.Add(ids...)
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, WhatChanged: whatChanged, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()})
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, WhatChanged: whatChanged, RecentlyVisited: c.visitedURLs, ErrRecovery: c.errState.wasErr()})
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1182,9 +1124,8 @@ func (c *hugoBuilder) reloadConfig() error {
|
||||
c.r.resetLogs()
|
||||
c.r.configVersionID.Add(1)
|
||||
|
||||
if err := c.withConfOrOldConfE(func(conf *commonConfig) error {
|
||||
if err := c.withConfE(func(conf *commonConfig) error {
|
||||
oldConf := conf
|
||||
c.conf = nil
|
||||
newConf, err := c.r.ConfigFromConfig(configKey{counter: c.r.configVersionID.Load()}, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+12
-12
@@ -29,9 +29,9 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
@@ -49,7 +49,7 @@ func newImportCommand() *importCommand {
|
||||
name: "jekyll",
|
||||
short: "hugo import from Jekyll",
|
||||
long: `hugo import from Jekyll.
|
||||
|
||||
|
||||
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 2 {
|
||||
@@ -90,8 +90,8 @@ func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
|
||||
func (c *importCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Import a project from another system"
|
||||
cmd.Long = `Import a project from another system.
|
||||
cmd.Short = "Import a site from another system"
|
||||
cmd.Long = `Import a site from another system.
|
||||
|
||||
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
|
||||
|
||||
@@ -105,7 +105,7 @@ func (c *importCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
}
|
||||
|
||||
func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
|
||||
title := "My New Hugo Project"
|
||||
title := "My New Hugo Site"
|
||||
baseURL := "http://example.org/"
|
||||
|
||||
for key, value := range jekyllConfig {
|
||||
@@ -127,7 +127,7 @@ func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind
|
||||
in := map[string]any{
|
||||
"baseURL": baseURL,
|
||||
"title": title,
|
||||
"locale": "en-us",
|
||||
"languageCode": "en-us",
|
||||
"disablePathToLower": true,
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func (c *importCommand) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[st
|
||||
return postDirs, hasAnyPost
|
||||
}
|
||||
|
||||
func (c *importCommand) createProjectFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
|
||||
func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
|
||||
fs := &afero.OsFs{}
|
||||
if exists, _ := helpers.Exists(targetDir, fs); exists {
|
||||
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
|
||||
@@ -190,7 +190,7 @@ func (c *importCommand) createProjectFromJekyll(jekyllRoot, targetDir string, je
|
||||
}
|
||||
|
||||
func (c *importCommand) convertJekyllContent(m any, content string) (string, error) {
|
||||
metadata, _ := hmaps.ToStringMapE(m)
|
||||
metadata, _ := maps.ToStringMapE(m)
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
var resultLines []string
|
||||
@@ -246,7 +246,7 @@ func (c *importCommand) convertJekyllContent(m any, content string) (string, err
|
||||
}
|
||||
|
||||
func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
|
||||
metadata, err := hmaps.ToStringMapE(m)
|
||||
metadata, err := maps.ToStringMapE(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -419,7 +419,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
return errors.New("abort: jekyll root contains neither posts nor drafts")
|
||||
}
|
||||
|
||||
err = c.createProjectFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
|
||||
err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
|
||||
if err != nil {
|
||||
return newUserError(err)
|
||||
}
|
||||
@@ -427,7 +427,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
c.r.Println("Importing...")
|
||||
|
||||
fileCount := 0
|
||||
callback := func(ctx context.Context, path string, fi hugofs.FileMetaInfo) error {
|
||||
callback := func(path string, fi hugofs.FileMetaInfo) error {
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
@@ -463,7 +463,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
}
|
||||
|
||||
c.r.Println("Congratulations!", fileCount, "post(s) imported!")
|
||||
c.r.Println("Now, start Hugo by yourself:")
|
||||
c.r.Println("Now, start Hugo by yourself:\n")
|
||||
c.r.Println("cd " + args[1])
|
||||
c.r.Println("git init")
|
||||
c.r.Println("git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke themes/ananke")
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ func newListCommand() *listCommand {
|
||||
return err
|
||||
}
|
||||
|
||||
writer := csv.NewWriter(r.StdOut)
|
||||
writer := csv.NewWriter(r.Out)
|
||||
defer writer.Flush()
|
||||
|
||||
writer.Write([]string{
|
||||
|
||||
+25
-35
@@ -21,13 +21,12 @@ import (
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/modules/npm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const commonUsageMod = `
|
||||
Note that Hugo will always start out by resolving the components defined in the project
|
||||
Note that Hugo will always start out by resolving the components defined in the site
|
||||
configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided),
|
||||
Go Modules, or a folder inside the themes directory, in that order.
|
||||
|
||||
@@ -45,39 +44,33 @@ func newModCommands() *modCommands {
|
||||
|
||||
npmCommand := &simpleCommand{
|
||||
name: "npm",
|
||||
short: "Various npm helpers",
|
||||
short: "Various npm helpers.",
|
||||
long: `Various npm (Node package manager) helpers.`,
|
||||
commands: []simplecobra.Commander{
|
||||
&simpleCommand{
|
||||
name: "pack",
|
||||
short: "Merges module Node.js dependencies into an npm workspace",
|
||||
long: `Merges Node.js dependencies from all Hugo modules into a "packages/hugoautogen" npm workspace.
|
||||
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.
|
||||
|
||||
The merged dependencies are written to packages/hugoautogen/package.json, and the root package.json
|
||||
is updated with a "workspaces" entry pointing to "packages/hugoautogen".
|
||||
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
|
||||
with the base dependency set.
|
||||
|
||||
The source entries are read from either package.hugo.json or package.json in the module root, with package.hugo.json taking precedence if both exist.
|
||||
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
|
||||
|
||||
See [Node.js dependencies](/hugo-modules/nodejs-dependencies/) for more information.
|
||||
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
|
||||
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
|
||||
so this may/will change in future versions of Hugo.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
cfg := flagsToCfg(cd, nil)
|
||||
k := configKey{counter: r.configVersionID.Load(), skipNpmCheck: true}
|
||||
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
|
||||
conf, err := r.ConfigFromProvider(key, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hugolib.NewHugoSites(r.newDepsConfig(conf))
|
||||
})
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs, h.Configs.Modules)
|
||||
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -87,12 +80,12 @@ See [Node.js dependencies](/hugo-modules/nodejs-dependencies/) for more informat
|
||||
commands: []simplecobra.Commander{
|
||||
&simpleCommand{
|
||||
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.
|
||||
It will try to guess the module path, but you may help by passing it as an argument, e.g:
|
||||
|
||||
|
||||
hugo mod init github.com/gohugoio/testshortcodes
|
||||
|
||||
|
||||
Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module
|
||||
inside a subfolder on GitHub, as one example.
|
||||
`,
|
||||
@@ -118,7 +111,7 @@ See [Node.js dependencies](/hugo-modules/nodejs-dependencies/) for more informat
|
||||
},
|
||||
&simpleCommand{
|
||||
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.`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
@@ -136,7 +129,7 @@ See [Node.js dependencies](/hugo-modules/nodejs-dependencies/) for more informat
|
||||
},
|
||||
&simpleCommand{
|
||||
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).
|
||||
Note that for vendored modules, that is the version listed and not the one from go.mod.
|
||||
`,
|
||||
@@ -156,7 +149,7 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
},
|
||||
&simpleCommand{
|
||||
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.`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
@@ -173,7 +166,7 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
if all {
|
||||
modCache := h.ResourceSpec.FileCaches.ModulesCache()
|
||||
count, err := modCache.Prune(true)
|
||||
r.Printf("Deleted %d directories from module cache.", count)
|
||||
r.Printf("Deleted %d files from module cache.", count)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -182,7 +175,7 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
},
|
||||
&simpleCommand{
|
||||
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) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
@@ -197,7 +190,7 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
},
|
||||
&simpleCommand{
|
||||
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.
|
||||
If a module is vendored, that is where Hugo will look for it's dependencies.
|
||||
`,
|
||||
@@ -216,16 +209,16 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
|
||||
&simpleCommand{
|
||||
name: "get",
|
||||
short: "Resolves dependencies in your current Hugo project",
|
||||
short: "Resolves dependencies in your current Hugo Project.",
|
||||
long: `
|
||||
Resolves dependencies in your current Hugo project.
|
||||
Resolves dependencies in your current Hugo Project.
|
||||
|
||||
Some examples:
|
||||
|
||||
Install the latest version possible for a given module:
|
||||
|
||||
hugo mod get github.com/gohugoio/testshortcodes
|
||||
|
||||
|
||||
Install a specific version:
|
||||
|
||||
hugo mod get github.com/gohugoio/testshortcodes@v0.3.0
|
||||
@@ -300,10 +293,7 @@ Run "go help get" for more information. All flags available for "go get" is also
|
||||
return err
|
||||
}
|
||||
client := conf.configs.ModulesClient
|
||||
if err := client.Get(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return client.Get(args...)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
+22
-28
@@ -46,16 +46,14 @@ It will guess which kind of file to create based on the path provided.
|
||||
|
||||
You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
|
||||
If archetypes are provided in your theme or project, they will be used.
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
|
||||
Ensure you run this within the root directory of your project.`,
|
||||
Ensure you run this within the root directory of your site.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
cfg := flagsToCfg(cd, nil)
|
||||
cfg.Set("BuildFuture", true)
|
||||
h, err := r.Hugo(cfg)
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,11 +74,12 @@ Ensure you run this within the root directory of your project.`,
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "project",
|
||||
use: "project [path]",
|
||||
short: "Create a new project",
|
||||
long: `Create a new project at the specified path.`,
|
||||
aliases: []string{"site"},
|
||||
name: "site",
|
||||
use: "site [path]",
|
||||
short: "Create a new site (skeleton)",
|
||||
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 {
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
@@ -100,13 +99,13 @@ Ensure you run this within the root directory of your project.`,
|
||||
}
|
||||
sourceFs := conf.fs.Source
|
||||
|
||||
err = skeletons.CreateProject(createpath, sourceFs, force, format)
|
||||
err = skeletons.CreateSite(createpath, sourceFs, force, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Printf("Congratulations! Your new Hugo project was created in %s.\n\n", createpath)
|
||||
r.Println(c.newProjectNextStepsText(createpath, format))
|
||||
r.Printf("Congratulations! Your new Hugo site was created in %s.\n\n", createpath)
|
||||
r.Println(c.newSiteNextStepsText(createpath, format))
|
||||
|
||||
return nil
|
||||
},
|
||||
@@ -125,9 +124,11 @@ Ensure you run this within the root directory of your project.`,
|
||||
&simpleCommand{
|
||||
name: "theme",
|
||||
use: "theme [name]",
|
||||
short: "Create a new theme",
|
||||
long: `Create a new theme with the specified name in the ./themes directory.
|
||||
This generates a functional theme including template examples and sample content.`,
|
||||
short: "Create a new theme (skeleton)",
|
||||
long: `Create a new theme (skeleton) called [name] in ./themes.
|
||||
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 {
|
||||
if len(args) < 1 {
|
||||
return newUserError("theme name needs to be provided")
|
||||
@@ -143,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]))
|
||||
r.Println("Creating new theme in", createpath)
|
||||
|
||||
err = skeletons.CreateTheme(createpath, sourceFs, format)
|
||||
err = skeletons.CreateTheme(createpath, sourceFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -151,14 +152,7 @@ This generates a functional theme including template examples and sample content
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
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))
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -193,9 +187,9 @@ It will guess which kind of file to create based on the path provided.
|
||||
|
||||
You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
|
||||
If archetypes are provided in your theme or project, they will be used.
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
|
||||
Ensure you run this within the root directory of your project.`
|
||||
Ensure you run this within the root directory of your site.`
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
@@ -206,7 +200,7 @@ func (c *newCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *newCommand) newProjectNextStepsText(path string, format string) string {
|
||||
func (c *newCommand) newSiteNextStepsText(path string, format string) string {
|
||||
format = strings.ToLower(format)
|
||||
var nextStepsText bytes.Buffer
|
||||
|
||||
|
||||
+2
-4
@@ -28,14 +28,13 @@ func newReleaseCommand() simplecobra.Commander {
|
||||
step int
|
||||
skipPush bool
|
||||
try bool
|
||||
version string
|
||||
)
|
||||
|
||||
return &simpleCommand{
|
||||
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 {
|
||||
rel, err := releaser.New(skipPush, try, step, version)
|
||||
rel, err := releaser.New(skipPush, try, step)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -48,7 +47,6 @@ func newReleaseCommand() simplecobra.Commander {
|
||||
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
|
||||
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes")
|
||||
cmd.PersistentFlags().IntVarP(&step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
|
||||
cmd.PersistentFlags().StringVarP(&version, "version", "", "", "version to release (derived from branch name if not set)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("step", cobra.FixedCompletions([]string{"1", "2"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
},
|
||||
}
|
||||
|
||||
+85
-126
@@ -23,7 +23,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
@@ -33,7 +32,6 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -42,16 +40,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bep/mclib"
|
||||
"github.com/pkg/browser"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
@@ -61,6 +55,7 @@ import (
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/hugolib/filesystems"
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/gohugoio/hugo/transform"
|
||||
"github.com/gohugoio/hugo/transform/livereloadinject"
|
||||
"github.com/spf13/afero"
|
||||
@@ -87,14 +82,10 @@ const (
|
||||
configChangeGoWork = "go work file"
|
||||
)
|
||||
|
||||
const (
|
||||
hugoHeaderRedirect = "X-Hugo-Redirect"
|
||||
)
|
||||
|
||||
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 {
|
||||
visitedURLs = types.NewEvictingQueue[string](20)
|
||||
visitedURLs = types.NewEvictingStringQueue(20)
|
||||
}
|
||||
return &hugoBuilder{
|
||||
r: r,
|
||||
@@ -122,7 +113,7 @@ func newServerCommand() *serverCommand {
|
||||
commands: []simplecobra.Commander{
|
||||
&simpleCommand{
|
||||
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 {
|
||||
action := "-install"
|
||||
if uninstall {
|
||||
@@ -198,7 +189,9 @@ func (f *fileChangeDetector) PrepareNew() {
|
||||
}
|
||||
|
||||
f.prev = make(map[string]uint64)
|
||||
maps.Copy(f.prev, f.current)
|
||||
for k, v := range f.current {
|
||||
f.prev[k] = v
|
||||
}
|
||||
f.current = make(map[string]uint64)
|
||||
}
|
||||
|
||||
@@ -216,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
|
||||
for _, v := range in {
|
||||
if !f.irrelevantRe.MatchString(v) {
|
||||
filtered = append(filtered, v)
|
||||
}
|
||||
}
|
||||
sort.Strings(filtered)
|
||||
return filtered
|
||||
}
|
||||
|
||||
@@ -246,7 +238,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
logger := f.c.r.logger
|
||||
|
||||
if i == 0 {
|
||||
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment())
|
||||
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment)
|
||||
mainTarget := "disk"
|
||||
if f.c.r.renderToMemory {
|
||||
mainTarget = "memory"
|
||||
@@ -282,7 +274,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
}
|
||||
|
||||
port = 1313
|
||||
f.c.withConfOrOldConf(func(conf *commonConfig) {
|
||||
f.c.withConf(func(conf *commonConfig) {
|
||||
if lrport := conf.configs.GetFirstLanguageConfig().BaseURLLiveReload().Port(); lrport != 0 {
|
||||
port = lrport
|
||||
}
|
||||
@@ -312,71 +304,64 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
w.Header().Set(header.Key, header.Value)
|
||||
}
|
||||
|
||||
if canRedirect(requestURI, r) {
|
||||
if redirect := serverConfig.MatchRedirect(requestURI, r.Header); !redirect.IsZero() {
|
||||
doRedirect := true
|
||||
// This matches Netlify's behavior and is needed for SPA behavior.
|
||||
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
|
||||
if !redirect.Force {
|
||||
path := filepath.Clean(strings.TrimPrefix(requestURI, baseURL.Path()))
|
||||
if root != "" {
|
||||
path = filepath.Join(root, path)
|
||||
}
|
||||
var fs afero.Fs
|
||||
f.c.withConf(func(conf *commonConfig) {
|
||||
fs = conf.fs.PublishDirServer
|
||||
})
|
||||
|
||||
fi, err := fs.Stat(path)
|
||||
|
||||
if err == nil {
|
||||
if fi.IsDir() {
|
||||
// There will be overlapping directories, so we
|
||||
// need to check for a file.
|
||||
_, err = fs.Stat(filepath.Join(path, "index.html"))
|
||||
doRedirect = err != nil
|
||||
} else {
|
||||
doRedirect = false
|
||||
}
|
||||
}
|
||||
if redirect := serverConfig.MatchRedirect(requestURI); !redirect.IsZero() {
|
||||
// fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
|
||||
doRedirect := true
|
||||
// This matches Netlify's behavior and is needed for SPA behavior.
|
||||
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
|
||||
if !redirect.Force {
|
||||
path := filepath.Clean(strings.TrimPrefix(requestURI, baseURL.Path()))
|
||||
if root != "" {
|
||||
path = filepath.Join(root, path)
|
||||
}
|
||||
var fs afero.Fs
|
||||
f.c.withConf(func(conf *commonConfig) {
|
||||
fs = conf.fs.PublishDirServer
|
||||
})
|
||||
|
||||
if doRedirect {
|
||||
w.Header().Set(hugoHeaderRedirect, "true")
|
||||
switch redirect.Status {
|
||||
case 404:
|
||||
w.WriteHeader(404)
|
||||
file, err := fs.Open(strings.TrimPrefix(redirect.To, baseURL.Path()))
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
io.Copy(w, file)
|
||||
} else {
|
||||
fmt.Fprintln(w, "<h1>Page Not Found</h1>")
|
||||
}
|
||||
return
|
||||
case 200:
|
||||
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, baseURL.Path())); r2 != nil {
|
||||
requestURI = redirect.To
|
||||
r = r2
|
||||
}
|
||||
default:
|
||||
w.Header().Set("Content-Type", "")
|
||||
http.Redirect(w, r, redirect.To, redirect.Status)
|
||||
return
|
||||
fi, err := fs.Stat(path)
|
||||
|
||||
if err == nil {
|
||||
if fi.IsDir() {
|
||||
// There will be overlapping directories, so we
|
||||
// need to check for a file.
|
||||
_, err = fs.Stat(filepath.Join(path, "index.html"))
|
||||
doRedirect = err != nil
|
||||
} else {
|
||||
doRedirect = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if doRedirect {
|
||||
switch redirect.Status {
|
||||
case 404:
|
||||
w.WriteHeader(404)
|
||||
file, err := fs.Open(strings.TrimPrefix(redirect.To, baseURL.Path()))
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
io.Copy(w, file)
|
||||
} else {
|
||||
fmt.Fprintln(w, "<h1>Page Not Found</h1>")
|
||||
}
|
||||
return
|
||||
case 200:
|
||||
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, baseURL.Path())); r2 != nil {
|
||||
requestURI = redirect.To
|
||||
r = r2
|
||||
}
|
||||
default:
|
||||
w.Header().Set("Content-Type", "")
|
||||
http.Redirect(w, r, redirect.To, redirect.Status)
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if f.c.fastRenderMode && f.c.errState.buildErr() == nil {
|
||||
if isNavigation(requestURI, r) {
|
||||
// See issue 14240.
|
||||
// Hugo escapes the URL paths when generating them,
|
||||
// that may not be the case when we receive it back from the browser.
|
||||
// PathEscape will escape if it is not already escaped.
|
||||
requestURI = paths.PathEscape(requestURI)
|
||||
|
||||
if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") {
|
||||
if !f.c.visitedURLs.Contains(requestURI) {
|
||||
// If not already on stack, re-render that single page.
|
||||
if err := f.c.partialReRender(requestURI); err != nil {
|
||||
@@ -463,7 +448,6 @@ type serverCommand struct {
|
||||
// Flags.
|
||||
renderStaticToDisk bool
|
||||
navigateToChanged bool
|
||||
openBrowser bool
|
||||
serverAppend bool
|
||||
serverInterface string
|
||||
tlsCertFile string
|
||||
@@ -499,7 +483,9 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
|
||||
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
|
||||
|
||||
c.r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
|
||||
for _, group := range watchGroups {
|
||||
c.r.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
watcher, err := c.newWatcher(c.r.poll, watchDirs...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -523,7 +509,7 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
func (c *serverCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Start the embedded web server"
|
||||
cmd.Long = `Hugo provides its own webserver which builds and serves the project.
|
||||
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.
|
||||
|
||||
The ` + "`" + `hugo server` + "`" + ` command will by default write and serve files from disk, but
|
||||
@@ -531,8 +517,8 @@ you can render to memory by using the ` + "`" + `--renderToMemory` + "`" + ` fla
|
||||
faster in some cases, but it will consume more memory.
|
||||
|
||||
By default hugo will also watch your files for any changes you make and
|
||||
automatically rebuild the project. It will then live reload any open browser pages
|
||||
and push the latest content to them. As most Hugo projects are built in a fraction
|
||||
automatically rebuild the site. It will then live reload any open browser pages
|
||||
and push the latest content to them. As most Hugo sites are built in a fraction
|
||||
of a second, you will be able to save and see your changes nearly instantly.`
|
||||
cmd.Aliases = []string{"serve"}
|
||||
|
||||
@@ -553,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().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.openBrowser, "openBrowser", "O", false, "open the project 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.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
|
||||
cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
|
||||
@@ -764,7 +749,7 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
|
||||
c.serverPorts = make([]serverPortListener, len(conf.configs.Languages))
|
||||
}
|
||||
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)))
|
||||
if err == nil {
|
||||
c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
|
||||
@@ -848,7 +833,7 @@ func (c *serverCommand) partialReRender(urls ...string) (err error) {
|
||||
defer func() {
|
||||
c.errState.setWasErr(false)
|
||||
}()
|
||||
visited := types.NewEvictingQueue[string](len(urls))
|
||||
visited := types.NewEvictingStringQueue(len(urls))
|
||||
for _, url := range urls {
|
||||
visited.Add(url)
|
||||
}
|
||||
@@ -860,7 +845,7 @@ func (c *serverCommand) partialReRender(urls ...string) (err error) {
|
||||
}
|
||||
|
||||
// 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()})
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()})
|
||||
|
||||
return
|
||||
}
|
||||
@@ -885,7 +870,7 @@ func (c *serverCommand) serve() error {
|
||||
if isMultihost {
|
||||
for _, l := range conf.configs.ConfigLangs() {
|
||||
baseURLs = append(baseURLs, l.BaseURL())
|
||||
roots = append(roots, l.Language().(*langs.Language).Lang)
|
||||
roots = append(roots, l.Language().Lang)
|
||||
}
|
||||
} else {
|
||||
l := conf.configs.GetFirstLanguageConfig()
|
||||
@@ -903,16 +888,16 @@ func (c *serverCommand) serve() error {
|
||||
// To allow the en user to change the error template while the server is running, we use
|
||||
// the freshest template we can provide.
|
||||
var (
|
||||
errTempl *tplimpl.TemplInfo
|
||||
templHandler *tplimpl.TemplateStore
|
||||
errTempl tpl.Template
|
||||
templHandler tpl.TemplateHandler
|
||||
)
|
||||
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) {
|
||||
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
|
||||
if h == nil {
|
||||
return errTempl, templHandler
|
||||
}
|
||||
templHandler := h.GetTemplateStore()
|
||||
errTempl := templHandler.LookupByPath("/_server/error.html")
|
||||
if errTempl == nil {
|
||||
templHandler := h.Tmpl()
|
||||
errTempl, found := templHandler.Lookup("_server/error.html")
|
||||
if !found {
|
||||
panic("template server/error.html not found")
|
||||
}
|
||||
return errTempl, templHandler
|
||||
@@ -1013,13 +998,6 @@ func (c *serverCommand) serve() error {
|
||||
|
||||
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 {
|
||||
for {
|
||||
select {
|
||||
@@ -1040,6 +1018,7 @@ func (c *serverCommand) serve() error {
|
||||
defer cancel()
|
||||
wg2, ctx := errgroup.WithContext(ctx)
|
||||
for _, srv := range servers {
|
||||
srv := srv
|
||||
wg2.Go(func() error {
|
||||
return srv.Shutdown(ctx)
|
||||
})
|
||||
@@ -1160,17 +1139,18 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// chmodFilter is a ChmodFilter for static syncing.
|
||||
// Returns true to skip syncing permissions for directories and files without
|
||||
// owner-write permission. The primary use case is files from the module cache (0444).
|
||||
func chmodFilter(dst, src os.FileInfo) bool {
|
||||
if src.IsDir() {
|
||||
return true
|
||||
}
|
||||
return src.Mode().Perm()&0o200 == 0
|
||||
// Hugo publishes data from multiple sources, potentially
|
||||
// with overlapping directory structures. We cannot sync permissions
|
||||
// for directories as that would mean that we might end up with write-protected
|
||||
// directories inside /public.
|
||||
// One example of this would be syncing from the Go Module cache,
|
||||
// which have 0555 directories.
|
||||
return src.IsDir()
|
||||
}
|
||||
|
||||
func cleanErrorLog(content string) string {
|
||||
content = strings.ReplaceAll(content, "\n", " ")
|
||||
content = logReplacer.Replace(content)
|
||||
content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
|
||||
content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
|
||||
@@ -1238,24 +1218,3 @@ func formatByteCount(b uint64) string {
|
||||
return fmt.Sprintf("%.1f %cB",
|
||||
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, ".")
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ package collections
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hreflect"
|
||||
)
|
||||
|
||||
// Append appends from to a slice to and returns the resulting slice.
|
||||
@@ -27,7 +25,7 @@ func Append(to any, from ...any) (any, error) {
|
||||
if len(from) == 0 {
|
||||
return to, nil
|
||||
}
|
||||
tov, toIsNil := hreflect.Indirect(reflect.ValueOf(to))
|
||||
tov, toIsNil := indirect(reflect.ValueOf(to))
|
||||
|
||||
toIsNil = toIsNil || to == nil
|
||||
var tot reflect.Type
|
||||
@@ -102,7 +100,7 @@ func Append(to any, from ...any) (any, error) {
|
||||
fv := reflect.ValueOf(f)
|
||||
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
tov, _ := hreflect.Indirect(reflect.ValueOf(to))
|
||||
tov, _ := indirect(reflect.ValueOf(to))
|
||||
return appendToInterfaceSlice(tov, from...)
|
||||
}
|
||||
tov = reflect.Append(tov, fv)
|
||||
@@ -119,7 +117,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
|
||||
tos = append(tos, nil)
|
||||
continue
|
||||
}
|
||||
for i := range slice.Len() {
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
tos = append(tos, slice.Index(i).Interface())
|
||||
}
|
||||
}
|
||||
@@ -130,7 +128,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
|
||||
func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
|
||||
var tos []any
|
||||
|
||||
for i := range tov.Len() {
|
||||
for i := 0; i < tov.Len(); i++ {
|
||||
tos = append(tos, tov.Index(i).Interface())
|
||||
}
|
||||
|
||||
@@ -138,3 +136,17 @@ func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
|
||||
|
||||
return tos, nil
|
||||
}
|
||||
|
||||
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
|
||||
// TODO(bep) consolidate
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ func TestAppend(t *testing.T) {
|
||||
{[]string{"a", "b"}, []any{nil}, []any{"a", "b", 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"}},
|
||||
{[]string{"a", "b"}, []any{}, []string{"a", "b"}},
|
||||
} {
|
||||
|
||||
result, err := Append(test.start, test.addend...)
|
||||
|
||||
@@ -136,37 +136,3 @@ func TestSortedStringSlice(t *testing.T) {
|
||||
c.Assert(s.Count("z"), qt.Equals, 0)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+26
-81
@@ -13,87 +13,13 @@
|
||||
|
||||
package collections
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
"sync"
|
||||
import "sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hiter"
|
||||
)
|
||||
|
||||
// StackThreadSafe is a simple LIFO stack that is safe for concurrent use.
|
||||
type StackThreadSafe[T any] struct {
|
||||
items []T
|
||||
zero T
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewStackThreadSafe[T any]() *StackThreadSafe[T] {
|
||||
return &StackThreadSafe[T]{}
|
||||
}
|
||||
|
||||
func (s *StackThreadSafe[T]) Push(item T) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
func (s *StackThreadSafe[T]) Pop() (T, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
item := s.items[len(s.items)-1]
|
||||
s.items = s.items[:len(s.items)-1]
|
||||
return item, true
|
||||
}
|
||||
|
||||
func (s *StackThreadSafe[T]) Peek() (T, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
return s.items[len(s.items)-1], true
|
||||
}
|
||||
|
||||
func (s *StackThreadSafe[T]) Len() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
// All returns all items in the stack, from bottom to top.
|
||||
func (s *StackThreadSafe[T]) All() iter.Seq2[int, T] {
|
||||
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
|
||||
}
|
||||
|
||||
func (s *StackThreadSafe[T]) Drain() []T {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items := s.items
|
||||
s.items = nil
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *StackThreadSafe[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
|
||||
}
|
||||
|
||||
// Stack is a simple LIFO stack that is not safe for concurrent use.
|
||||
// Stack is a simple LIFO stack that is safe for concurrent use.
|
||||
type Stack[T any] struct {
|
||||
items []T
|
||||
zero T
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewStack[T any]() *Stack[T] {
|
||||
@@ -101,10 +27,14 @@ func NewStack[T any]() *Stack[T] {
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Push(item T) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Pop() (T, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
@@ -114,6 +44,8 @@ func (s *Stack[T]) Pop() (T, bool) {
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Peek() (T, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
@@ -121,15 +53,28 @@ func (s *Stack[T]) Peek() (T, bool) {
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Len() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) All() iter.Seq2[int, T] {
|
||||
return slices.All(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Drain() []T {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items := s.items
|
||||
s.items = nil
|
||||
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 = append(s.items[:i], s.items[i+1:]...)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -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 := NewStackThreadSafe[int]()
|
||||
|
||||
c.Assert(s, qt.IsNotNil)
|
||||
}
|
||||
|
||||
func TestStackBasic(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStackThreadSafe[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 := NewStackThreadSafe[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 := NewStackThreadSafe[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})
|
||||
}
|
||||
@@ -21,10 +21,6 @@ const (
|
||||
ErrRemoteGetCSV = "error-remote-getcsv"
|
||||
|
||||
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.
|
||||
@@ -43,7 +39,7 @@ const (
|
||||
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 {
|
||||
return name == ResourceTransformationFingerprint
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/gohugoio/hashstructure"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
)
|
||||
|
||||
@@ -39,47 +38,6 @@ func XXHashFromReader(r io.Reader) (uint64, int64, error) {
|
||||
return h.Sum64(), size, nil
|
||||
}
|
||||
|
||||
type Hasher interface {
|
||||
io.StringWriter
|
||||
io.Writer
|
||||
io.ReaderFrom
|
||||
Sum64() uint64
|
||||
}
|
||||
|
||||
type HashCloser interface {
|
||||
Hasher
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// XxHasher returns a Hasher that uses xxHash.
|
||||
// Remember to call Close when done.
|
||||
func XxHasher() HashCloser {
|
||||
h := getXxHashReadFrom()
|
||||
return struct {
|
||||
Hasher
|
||||
io.Closer
|
||||
}{
|
||||
Hasher: h,
|
||||
Closer: hugio.CloserFunc(func() error {
|
||||
putXxHashReadFrom(h)
|
||||
return 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()
|
||||
@@ -87,13 +45,11 @@ func XXHashFromString(s string) (uint64, error) {
|
||||
return h.Sum64(), nil
|
||||
}
|
||||
|
||||
// XxHashFromStringHexEncoded calculates the xxHash for the given strings
|
||||
// XxHashFromStringHexEncoded calculates the xxHash for the given string
|
||||
// and returns the hash as a hex encoded string.
|
||||
func XxHashFromStringHexEncoded(s ...string) string {
|
||||
func XxHashFromStringHexEncoded(f string) string {
|
||||
h := xxhash.New()
|
||||
for _, f := range s {
|
||||
h.WriteString(f)
|
||||
}
|
||||
h.WriteString(f)
|
||||
hash := h.Sum(nil)
|
||||
return hex.EncodeToString(hash)
|
||||
}
|
||||
@@ -114,13 +70,6 @@ func HashString(vs ...any) string {
|
||||
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{
|
||||
@@ -154,24 +103,16 @@ func HashUint64(vs ...any) uint64 {
|
||||
o = elements
|
||||
}
|
||||
|
||||
hash, err := Hash(o)
|
||||
hashOpts := getHashOpts()
|
||||
defer putHashOpts(hashOpts)
|
||||
|
||||
hash, err := hashstructure.Hash(o, hashOpts)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -37,9 +37,12 @@ func TestXxHashFromReaderPara(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range 10 {
|
||||
wg.Go(func() {
|
||||
for j := range 100 {
|
||||
for i := 0; i < 10; i++ {
|
||||
i := i
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
s := strings.Repeat("Hello ", i+j+1*42)
|
||||
r := strings.NewReader(s)
|
||||
got, size, err := XXHashFromReader(r)
|
||||
@@ -48,7 +51,7 @@ func TestXxHashFromReaderPara(t *testing.T) {
|
||||
expect, _ := XXHashFromString(s)
|
||||
c.Assert(got, qt.Equals, expect)
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
@@ -72,8 +75,8 @@ func TestXxHashFromStringHexEncoded(t *testing.T) {
|
||||
|
||||
func BenchmarkXXHashFromReader(b *testing.B) {
|
||||
r := strings.NewReader("Hello World")
|
||||
|
||||
for b.Loop() {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
XXHashFromReader(r)
|
||||
r.Seek(0, 0)
|
||||
}
|
||||
@@ -81,16 +84,16 @@ func BenchmarkXXHashFromReader(b *testing.B) {
|
||||
|
||||
func BenchmarkXXHashFromString(b *testing.B) {
|
||||
s := "Hello World"
|
||||
|
||||
for b.Loop() {
|
||||
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"
|
||||
|
||||
for b.Loop() {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
XxHashFromStringHexEncoded(s)
|
||||
}
|
||||
}
|
||||
@@ -133,20 +136,9 @@ func BenchmarkHashString(b *testing.B) {
|
||||
|
||||
for _, test := range tests {
|
||||
b.Run(fmt.Sprintf("n%d", len(test)), func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
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
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
HashString(m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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 hcontext
|
||||
|
||||
import "context"
|
||||
|
||||
// ContextDispatcher is a generic interface for setting and getting values from a context.
|
||||
type ContextDispatcher[T any] interface {
|
||||
Set(ctx context.Context, value T) context.Context
|
||||
Get(ctx context.Context) T
|
||||
}
|
||||
|
||||
// NewContextDispatcher creates a new ContextDispatcher with the given key.
|
||||
func NewContextDispatcher[T any, R comparable](key R) ContextDispatcher[T] {
|
||||
return keyInContext[T, R]{
|
||||
id: key,
|
||||
}
|
||||
}
|
||||
|
||||
type keyInContext[T any, R comparable] struct {
|
||||
zero T
|
||||
id R
|
||||
}
|
||||
|
||||
func (f keyInContext[T, R]) Get(ctx context.Context) T {
|
||||
v := ctx.Value(f.id)
|
||||
if v == nil {
|
||||
return f.zero
|
||||
}
|
||||
return v.(T)
|
||||
}
|
||||
|
||||
func (f keyInContext[T, R]) Set(ctx context.Context, value T) context.Context {
|
||||
return context.WithValue(ctx, f.id, value)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright 2025 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 hdebug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
)
|
||||
|
||||
// Printf is a debug print function that should be removed before committing code to the repository.
|
||||
func Printf(format string, args ...any) {
|
||||
panicIfRealCI()
|
||||
if len(args) == 1 && !strings.Contains(format, "%") {
|
||||
format = format + ": %v"
|
||||
}
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format = format + "\n"
|
||||
}
|
||||
fmt.Printf(format, args...)
|
||||
}
|
||||
|
||||
func AssertNotNil(a ...any) {
|
||||
panicIfRealCI()
|
||||
for _, v := range a {
|
||||
if types.IsNil(v) {
|
||||
panic("hdebug.AssertNotNil: value is nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Panicf(format string, args ...any) {
|
||||
panicIfRealCI()
|
||||
// fmt.Println(stack())
|
||||
if len(args) == 1 && !strings.Contains(format, "%") {
|
||||
format = format + ": %v"
|
||||
}
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format = format + "\n"
|
||||
}
|
||||
panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func panicIfRealCI() {
|
||||
if htesting.IsRealCI() {
|
||||
panic("This debug statement should be removed before committing code!")
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,10 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
|
||||
}
|
||||
|
||||
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 {
|
||||
ectx.LinesPos = 2
|
||||
@@ -160,7 +163,10 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
|
||||
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]
|
||||
|
||||
|
||||
+10
-18
@@ -17,13 +17,22 @@ package herrors
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PrintStackTrace prints the current stacktrace to w.
|
||||
func PrintStackTrace(w io.Writer) {
|
||||
buf := make([]byte, 1<<16)
|
||||
runtime.Stack(buf, true)
|
||||
fmt.Fprintf(w, "%s", buf)
|
||||
}
|
||||
|
||||
// ErrorSender is a, typically, non-blocking error handler.
|
||||
type ErrorSender interface {
|
||||
SendError(err error)
|
||||
@@ -36,9 +45,7 @@ type ErrorSender interface {
|
||||
func Recover(args ...any) {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("ERR:", r)
|
||||
buf := make([]byte, 64<<10)
|
||||
buf = buf[:runtime.Stack(buf, false)]
|
||||
args = append(args, "stacktrace from panic: \n"+string(buf), "\n")
|
||||
args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
|
||||
fmt.Println(args...)
|
||||
}
|
||||
}
|
||||
@@ -126,21 +133,6 @@ func IsNotExist(err error) bool {
|
||||
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`)
|
||||
|
||||
const deferredPrefix = "__hdeferred/"
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
godartsassv1 "github.com/bep/godartsass"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
"github.com/bep/golibsass/libsass/libsasserrors"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
@@ -110,11 +112,11 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
|
||||
|
||||
fe.errorContext = ectx
|
||||
|
||||
if ectx.Position.LineNumber > 0 && ectx.Position.LineNumber > fe.position.LineNumber {
|
||||
if ectx.Position.LineNumber > 0 {
|
||||
fe.position.LineNumber = ectx.Position.LineNumber
|
||||
}
|
||||
|
||||
if ectx.Position.ColumnNumber > 0 && ectx.Position.ColumnNumber > fe.position.ColumnNumber {
|
||||
if ectx.Position.ColumnNumber > 0 {
|
||||
fe.position.ColumnNumber = ectx.Position.ColumnNumber
|
||||
}
|
||||
|
||||
@@ -151,6 +153,8 @@ func (e *fileError) causeString() string {
|
||||
// Avoid repeating the file info in the error message.
|
||||
case godartsass.SassError:
|
||||
return v.Message
|
||||
case godartsassv1.SassError:
|
||||
return v.Message
|
||||
case libsasserrors.Error:
|
||||
return v.Message
|
||||
default:
|
||||
@@ -177,7 +181,6 @@ func NewFileErrorFromName(err error, name string) FileError {
|
||||
// Filetype is used to determine the Chroma lexer to use.
|
||||
fileType, pos := extractFileTypePos(err)
|
||||
pos.Filename = name
|
||||
|
||||
if fileType == "" {
|
||||
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
|
||||
}
|
||||
@@ -235,9 +238,7 @@ func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher L
|
||||
return NewFileErrorFromName(err, realFilename)
|
||||
}
|
||||
defer f.Close()
|
||||
fe := NewFileErrorFromName(err, realFilename)
|
||||
fe = fe.UpdateContent(f, linematcher)
|
||||
return fe
|
||||
return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
|
||||
}
|
||||
|
||||
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
|
||||
@@ -261,79 +262,16 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
|
||||
return f, realFilename, nil
|
||||
}
|
||||
|
||||
// Cause returns the underlying error, that is,
|
||||
// it unwraps errors until it finds one that does not implement
|
||||
// the Unwrap method.
|
||||
// For a shallow variant, see Unwrap.
|
||||
// Cause returns the underlying error or itself if it does not implement Unwrap.
|
||||
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 {
|
||||
return u
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// UnwrapFileErrors returns all FileError contained in err.
|
||||
func UnwrapFileErrors(err error) []FileError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errs := Errors(err)
|
||||
var fileErrors []FileError
|
||||
for _, e := range errs {
|
||||
if v, ok := e.(FileError); ok {
|
||||
fileErrors = append(fileErrors, v)
|
||||
}
|
||||
fileErrors = append(fileErrors, UnwrapFileErrors(errors.Unwrap(e))...)
|
||||
}
|
||||
return fileErrors
|
||||
}
|
||||
|
||||
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
|
||||
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
|
||||
errs := UnwrapFileErrors(err)
|
||||
var n int
|
||||
for _, e := range errs {
|
||||
if e.ErrorContext() != nil {
|
||||
errs[n] = e
|
||||
n++
|
||||
}
|
||||
}
|
||||
return errs[:n]
|
||||
}
|
||||
|
||||
// Errors returns the list of errors contained in err.
|
||||
func Errors(err error) []error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
type unwrapper interface {
|
||||
Unwrap() []error
|
||||
}
|
||||
if u, ok := err.(unwrapper); ok {
|
||||
return u.Unwrap()
|
||||
}
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
func extractFileTypePos(err error) (string, text.Position) {
|
||||
err = Unwrap(err)
|
||||
err = Cause(err)
|
||||
|
||||
var fileType string
|
||||
|
||||
@@ -368,9 +306,13 @@ func extractFileTypePos(err error) (string, text.Position) {
|
||||
}
|
||||
|
||||
// Look in the error message for the line number.
|
||||
if lno, col := commonLineNumberExtractor(err); lno > 0 {
|
||||
pos.ColumnNumber = col
|
||||
pos.LineNumber = lno
|
||||
for _, handle := range lineNumberExtractors {
|
||||
lno, col := handle(err)
|
||||
if lno > 0 {
|
||||
pos.ColumnNumber = col
|
||||
pos.LineNumber = lno
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if fileType == "" && pos.Filename != "" {
|
||||
@@ -394,6 +336,30 @@ func UnwrapFileError(err error) FileError {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnwrapFileErrors tries to unwrap all FileError.
|
||||
func UnwrapFileErrors(err error) []FileError {
|
||||
var errs []FileError
|
||||
for err != nil {
|
||||
if v, ok := err.(FileError); ok {
|
||||
errs = append(errs, v)
|
||||
}
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
|
||||
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
|
||||
var errs []FileError
|
||||
for err != nil {
|
||||
if v, ok := err.(FileError); ok && v.ErrorContext() != nil {
|
||||
errs = append(errs, v)
|
||||
}
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func extractOffsetAndType(e error) (int, string) {
|
||||
switch v := e.(type) {
|
||||
case *json.UnmarshalTypeError:
|
||||
@@ -422,7 +388,14 @@ func extractPosition(e error) (pos text.Position) {
|
||||
case godartsass.SassError:
|
||||
span := v.Span
|
||||
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.Offset = start.Offset
|
||||
pos.ColumnNumber = start.Column
|
||||
|
||||
@@ -32,15 +32,15 @@ func TestNewFileError(t *testing.T) {
|
||||
fe := NewFileErrorFromName(errors.New("bar"), "foo.html")
|
||||
c.Assert(fe.Error(), qt.Equals, `"foo.html:1:1": bar`)
|
||||
|
||||
var lines strings.Builder
|
||||
lines := ""
|
||||
for i := 1; i <= 100; i++ {
|
||||
lines.WriteString(fmt.Sprintf("line %d\n", i))
|
||||
lines += fmt.Sprintf("line %d\n", i)
|
||||
}
|
||||
|
||||
fe.UpdatePosition(text.Position{LineNumber: 32, ColumnNumber: 2})
|
||||
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:2": bar`)
|
||||
fe.UpdatePosition(text.Position{LineNumber: 0, ColumnNumber: 0, Offset: 212})
|
||||
fe.UpdateContent(strings.NewReader(lines.String()), nil)
|
||||
fe.UpdateContent(strings.NewReader(lines), nil)
|
||||
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:0": bar`)
|
||||
errorContext := fe.ErrorContext()
|
||||
c.Assert(errorContext, qt.IsNotNil)
|
||||
|
||||
@@ -19,27 +19,17 @@ import (
|
||||
)
|
||||
|
||||
var lineNumberExtractors = []lineNumberExtractor{
|
||||
// YAML parse errors.
|
||||
newLineNumberErrHandlerFromRegexp(`\[(\d+):(\d+)\]`),
|
||||
|
||||
// Template/shortcode parse errors
|
||||
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
|
||||
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
|
||||
|
||||
// YAML parse errors
|
||||
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
|
||||
|
||||
// i18n bundle errors
|
||||
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
|
||||
}
|
||||
|
||||
func commonLineNumberExtractor(e error) (int, int) {
|
||||
for _, handler := range lineNumberExtractors {
|
||||
lno, col := handler(e)
|
||||
if lno > 0 {
|
||||
return lno, col
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
type lineNumberExtractor func(e error) (int, int)
|
||||
|
||||
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
|
||||
|
||||
+33
-88
@@ -26,9 +26,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/security"
|
||||
)
|
||||
@@ -88,7 +86,7 @@ var WithEnviron = func(env []string) func(c *commandeer) {
|
||||
}
|
||||
|
||||
// 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, workingDir string) *Exec {
|
||||
var baseEnviron []string
|
||||
for _, v := range os.Environ() {
|
||||
k, _ := config.SplitEnvVar(v)
|
||||
@@ -98,11 +96,9 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
|
||||
}
|
||||
|
||||
return &Exec{
|
||||
sc: cfg,
|
||||
workingDir: workingDir,
|
||||
infol: log.InfoCommand("exec"),
|
||||
baseEnviron: baseEnviron,
|
||||
newNPXRunnerCache: hmaps.NewCache[string, func(arg ...any) (Runner, error)](),
|
||||
sc: cfg,
|
||||
workingDir: workingDir,
|
||||
baseEnviron: baseEnviron,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,18 +108,28 @@ func IsNotFound(err error) bool {
|
||||
return errors.As(err, ¬FoundErr)
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Exec struct {
|
||||
sc security.Config
|
||||
workingDir string
|
||||
infol logg.LevelLogger
|
||||
|
||||
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
|
||||
baseEnviron []string
|
||||
|
||||
newNPXRunnerCache *hmaps.Cache[string, func(arg ...any) (Runner, error)]
|
||||
npxInit sync.Once
|
||||
npxAvailable bool
|
||||
npxInit sync.Once
|
||||
npxAvailable bool
|
||||
}
|
||||
|
||||
func (e *Exec) New(name string, arg ...any) (Runner, error) {
|
||||
@@ -149,86 +155,25 @@ func (e *Exec) new(name string, fullyQualifiedName string, arg ...any) (Runner,
|
||||
return cm.command(arg...)
|
||||
}
|
||||
|
||||
type binaryLocation int
|
||||
|
||||
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) {
|
||||
if err := e.sc.CheckAllowedExec(name); err != nil {
|
||||
return nil, err
|
||||
// npx is slow, so first try the common case.
|
||||
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
|
||||
_, err := safeexec.LookPath(nodeBinFilename)
|
||||
if err == nil {
|
||||
return e.new(name, nodeBinFilename, 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...)
|
||||
}
|
||||
},
|
||||
e.checkNpx()
|
||||
if e.npxAvailable {
|
||||
r, err := e.npx(name, arg...)
|
||||
if err == nil {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
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...)
|
||||
return e.New(name, arg...)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -333,7 +278,7 @@ func (c *commandeer) command(arg ...any) (*cmdWrapper, error) {
|
||||
bin = c.fullyQualifiedName
|
||||
} else {
|
||||
var err error
|
||||
bin, err = exec.LookPath(c.name)
|
||||
bin, err = safeexec.LookPath(c.name)
|
||||
if err != nil {
|
||||
return nil, &NotFoundError{
|
||||
name: c.name,
|
||||
@@ -371,7 +316,7 @@ func InPath(binaryName string) bool {
|
||||
if strings.Contains(binaryName, "/") {
|
||||
panic("binary name should not contain any slash")
|
||||
}
|
||||
_, err := exec.LookPath(binaryName)
|
||||
_, err := safeexec.LookPath(binaryName)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -381,7 +326,7 @@ func LookPath(binaryName string) string {
|
||||
if strings.Contains(binaryName, "/") {
|
||||
panic("binary name should not contain any slash")
|
||||
}
|
||||
s, err := exec.LookPath(binaryName)
|
||||
s, err := safeexec.LookPath(binaryName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright 2025 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 himage provides some high level image types and interfaces.
|
||||
package himage
|
||||
|
||||
import "image"
|
||||
|
||||
// AnimatedImage represents an animated image.
|
||||
// This is currently supported for GIF and WebP images.
|
||||
type AnimatedImage interface {
|
||||
image.Image // The first frame.
|
||||
GetRaw() any // *gif.GIF or *WEBP.
|
||||
GetLoopCount() int // Number of times to loop the animation. 0 means infinite.
|
||||
ImageFrames
|
||||
}
|
||||
|
||||
// ImageFrames provides access to the frames of an animated image.
|
||||
type ImageFrames interface {
|
||||
GetFrames() []image.Image
|
||||
|
||||
// Frame durations in milliseconds.
|
||||
// Note that Gif frame durations are in 100ths of a second,
|
||||
// so they need to be multiplied by 10 to get milliseconds and vice versa.
|
||||
GetFrameDurations() []int
|
||||
|
||||
SetFrames(frames []image.Image)
|
||||
SetWidthHeight(width, height int)
|
||||
}
|
||||
|
||||
// ImageConfigProvider provides access to the image.Config of an image.
|
||||
type ImageConfigProvider interface {
|
||||
GetImageConfig() image.Config
|
||||
}
|
||||
|
||||
// FrameDurationsToGifDelays converts frame durations in milliseconds to
|
||||
// GIF delays in 100ths of a second.
|
||||
func FrameDurationsToGifDelays(frameDurations []int) []int {
|
||||
delays := make([]int, len(frameDurations))
|
||||
for i, fd := range frameDurations {
|
||||
delays[i] = fd / 10
|
||||
if delays[i] == 0 && fd > 0 {
|
||||
delays[i] = 1
|
||||
}
|
||||
}
|
||||
return delays
|
||||
}
|
||||
|
||||
// GifDelaysToFrameDurations converts GIF delays in 100ths of a second to
|
||||
// frame durations in milliseconds.
|
||||
func GifDelaysToFrameDurations(delays []int) []int {
|
||||
frameDurations := make([]int, len(delays))
|
||||
for i, d := range delays {
|
||||
frameDurations[i] = d * 10
|
||||
}
|
||||
return frameDurations
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package hiter
|
||||
|
||||
// Common iterator functions.
|
||||
// Some of these are are based on this discsussion: https://github.com/golang/go/issues/61898
|
||||
|
||||
import "iter"
|
||||
|
||||
// Concat returns an iterator over the concatenation of the sequences.
|
||||
// Any nil sequences are ignored.
|
||||
func Concat[V any](seqs ...iter.Seq[V]) iter.Seq[V] {
|
||||
return func(yield func(V) bool) {
|
||||
for _, seq := range seqs {
|
||||
if seq == nil {
|
||||
continue
|
||||
}
|
||||
for e := range seq {
|
||||
if !yield(e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concat2 returns an iterator over the concatenation of the sequences.
|
||||
// Any nil sequences are ignored.
|
||||
func Concat2[K, V any](seqs ...iter.Seq2[K, V]) iter.Seq2[K, V] {
|
||||
return func(yield func(K, V) bool) {
|
||||
for _, seq := range seqs {
|
||||
if seq == nil {
|
||||
continue
|
||||
}
|
||||
for k, v := range seq {
|
||||
if !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lock returns an iterator that locks before iterating and unlocks after.
|
||||
func Lock[V any](seq iter.Seq[V], lock, unlock func()) iter.Seq[V] {
|
||||
return func(yield func(V) bool) {
|
||||
lock()
|
||||
defer unlock()
|
||||
for e := range seq {
|
||||
if !yield(e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lock2 returns an iterator that locks before iterating and unlocks after.
|
||||
func Lock2[K, V any](seq iter.Seq2[K, V], lock, unlock func()) iter.Seq2[K, V] {
|
||||
return func(yield func(K, V) bool) {
|
||||
lock()
|
||||
defer unlock()
|
||||
for k, v := range seq {
|
||||
if !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright 2026 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 hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestCacheSize(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cache := NewCacheWithOptions[string, string](CacheOptions{Size: 10})
|
||||
|
||||
for i := range 30 {
|
||||
cache.Set(string(rune('a'+i)), "value")
|
||||
}
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
|
||||
for i := 20; i < 50; i++ {
|
||||
cache.GetOrCreate(string(rune('a'+i)), func() (string, error) {
|
||||
return "value", nil
|
||||
})
|
||||
}
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
|
||||
for i := 100; i < 200; i++ {
|
||||
cache.SetIfAbsent(string(rune('a'+i)), "value")
|
||||
}
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
|
||||
cache.InitAndGet("foo", func(
|
||||
get func(key string) (string, bool), set func(key string, value string),
|
||||
) error {
|
||||
for i := 50; i < 100; i++ {
|
||||
set(string(rune('a'+i)), "value")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright 2026 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 hmaps
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewMap[K comparable, T any]() *Map[K, T] {
|
||||
return &Map[K, T]{
|
||||
m: make(map[K]T),
|
||||
}
|
||||
}
|
||||
|
||||
// Map is a thread safe map backed by a Go map.
|
||||
type Map[K comparable, T any] struct {
|
||||
m map[K]T
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Get gets the value for the given key.
|
||||
// It returns the zero value of T if the key is not found.
|
||||
func (m *Map[K, T]) Get(key K) T {
|
||||
v, _ := m.Lookup(key)
|
||||
return v
|
||||
}
|
||||
|
||||
// Lookup looks up the given key in the map.
|
||||
// It returns the value and a boolean indicating whether the key was found.
|
||||
func (m *Map[K, T]) Lookup(key K) (T, bool) {
|
||||
m.mu.RLock()
|
||||
v, found := m.m[key]
|
||||
m.mu.RUnlock()
|
||||
return v, found
|
||||
}
|
||||
|
||||
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
||||
func (m *Map[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||
v, found := m.Lookup(key)
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
v, found = m.m[key]
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
v, err := create()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
m.m[key] = v
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Set sets the given key to the given value.
|
||||
func (m *Map[K, T]) Set(key K, value T) {
|
||||
m.mu.Lock()
|
||||
m.m[key] = value
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Delete deletes the given key from the map.
|
||||
// It returns true if the key was found and deleted, false otherwise.
|
||||
func (m *Map[K, T]) Delete(key K) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, found := m.m[key]; found {
|
||||
delete(m.m, key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithWriteLock executes the given function with a write lock on the map.
|
||||
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T) error) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return f(m.m)
|
||||
}
|
||||
|
||||
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
|
||||
// It returns true if the value was set, false otherwise.
|
||||
func (m *Map[K, T]) SetIfAbsent(key K, value T) bool {
|
||||
m.mu.RLock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.mu.RUnlock()
|
||||
return m.doSetIfAbsent(key, value)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map[K, T]) doSetIfAbsent(key K, value T) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.m[key] = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// All returns an iterator over all key/value pairs in the map.
|
||||
// A read lock is held during the iteration.
|
||||
func (m *Map[K, T]) All() iter.Seq2[K, T] {
|
||||
return func(yield func(K, T) bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for k, v := range m.m {
|
||||
if !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright 2026 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 hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := NewMap[string, int]()
|
||||
|
||||
m.Set("b", 42)
|
||||
v, found := m.Lookup("b")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 42)
|
||||
v = m.Get("b")
|
||||
c.Assert(v, qt.Equals, 42)
|
||||
v, found = m.Lookup("c")
|
||||
c.Assert(found, qt.Equals, false)
|
||||
c.Assert(v, qt.Equals, 0)
|
||||
v = m.Get("c")
|
||||
c.Assert(v, qt.Equals, 0)
|
||||
v, err := m.GetOrCreate("d", func() (int, error) {
|
||||
return 100, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
v, found = m.Lookup("d")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
|
||||
v, err = m.GetOrCreate("d", func() (int, error) {
|
||||
return 200, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
|
||||
wasSet := m.SetIfAbsent("e", 300)
|
||||
c.Assert(wasSet, qt.Equals, true)
|
||||
v, found = m.Lookup("e")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 300)
|
||||
|
||||
wasSet = m.SetIfAbsent("e", 400)
|
||||
c.Assert(wasSet, qt.Equals, false)
|
||||
v, found = m.Lookup("e")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 300)
|
||||
|
||||
m.WithWriteLock(func(m map[string]int) error {
|
||||
m["f"] = 500
|
||||
return nil
|
||||
})
|
||||
v, found = m.Lookup("f")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 500)
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
// Copyright 2026 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 hmaps
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright 2026 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 hmaps
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright 2026 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 hmaps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
)
|
||||
|
||||
type OrderedIntSet struct {
|
||||
keys []int
|
||||
values *bitset.BitSet
|
||||
}
|
||||
|
||||
// NewOrderedIntSet creates a new OrderedIntSet.
|
||||
// Note that this is backed by https://github.com/bits-and-blooms/bitset
|
||||
func NewOrderedIntSet(vals ...int) *OrderedIntSet {
|
||||
m := &OrderedIntSet{
|
||||
keys: make([]int, 0, len(vals)),
|
||||
values: bitset.New(uint(len(vals))),
|
||||
}
|
||||
for _, v := range vals {
|
||||
m.Set(v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Set sets the value for the given key.
|
||||
// Note that insertion order is not affected if a key is re-inserted into the set.
|
||||
func (m *OrderedIntSet) Set(key int) {
|
||||
if m == nil {
|
||||
panic("nil OrderedIntSet")
|
||||
}
|
||||
keyu := uint(key)
|
||||
if m.values.Test(keyu) {
|
||||
return
|
||||
}
|
||||
m.values.Set(keyu)
|
||||
m.keys = append(m.keys, key)
|
||||
}
|
||||
|
||||
// SetFrom sets the values from another OrderedIntSet.
|
||||
func (m *OrderedIntSet) SetFrom(other *OrderedIntSet) {
|
||||
if m == nil || other == nil {
|
||||
return
|
||||
}
|
||||
for _, key := range other.keys {
|
||||
m.Set(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Clone() *OrderedIntSet {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
newSet := &OrderedIntSet{
|
||||
keys: slices.Clone(m.keys),
|
||||
values: m.values.Clone(),
|
||||
}
|
||||
return newSet
|
||||
}
|
||||
|
||||
// Next returns the next key in the set possibly including the given key.
|
||||
// It returns -1 if the key is not found or if there are no keys greater than the given key.
|
||||
func (m *OrderedIntSet) Next(i int) int {
|
||||
n, ok := m.values.NextSet(uint(i))
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return int(n)
|
||||
}
|
||||
|
||||
// The reason we don't use iter.Seq is https://github.com/golang/go/issues/69015
|
||||
// This is 70% faster than using iter.Seq2[int, int] for the keys.
|
||||
// It returns false if the iteration was stopped early.
|
||||
func (m *OrderedIntSet) ForEachKey(yield func(int) bool) bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
for _, key := range m.keys {
|
||||
if !yield(key) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Has(key int) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
return m.values.Test(uint(key))
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Len() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return len(m.keys)
|
||||
}
|
||||
|
||||
// KeysSorted returns the keys in sorted order.
|
||||
func (m *OrderedIntSet) KeysSorted() []int {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
keys := slices.Clone(m.keys)
|
||||
slices.Sort(keys)
|
||||
return m.keys
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) String() string {
|
||||
if m == nil {
|
||||
return "[]"
|
||||
}
|
||||
return fmt.Sprintf("%v", m.keys)
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Values() *bitset.BitSet {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m.values
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) IsSuperSet(other *OrderedIntSet) bool {
|
||||
if m == nil || other == nil {
|
||||
return false
|
||||
}
|
||||
return m.values.IsSuperSet(other.values)
|
||||
}
|
||||
|
||||
// Words returns the bitset as array of 64-bit words, giving direct access to the internal representation.
|
||||
// It is not a copy, so changes to the returned slice will affect the bitset.
|
||||
// It is meant for advanced users.
|
||||
func (m *OrderedIntSet) Words() []uint64 {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m.values.Words()
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// Copyright 2026 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 hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestOrderedIntSet(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := NewOrderedIntSet(2, 1, 3, 7)
|
||||
|
||||
c.Assert(m.Len(), qt.Equals, 4)
|
||||
c.Assert(m.Has(1), qt.Equals, true)
|
||||
c.Assert(m.Has(4), qt.Equals, false)
|
||||
c.Assert(m.String(), qt.Equals, "[2 1 3 7]")
|
||||
m.Set(4)
|
||||
c.Assert(m.Len(), qt.Equals, 5)
|
||||
c.Assert(m.Has(4), qt.Equals, true)
|
||||
c.Assert(m.Next(0), qt.Equals, 1)
|
||||
c.Assert(m.Next(1), qt.Equals, 1)
|
||||
c.Assert(m.Next(2), qt.Equals, 2)
|
||||
c.Assert(m.Next(3), qt.Equals, 3)
|
||||
c.Assert(m.Next(4), qt.Equals, 4)
|
||||
c.Assert(m.Next(7), qt.Equals, 7)
|
||||
c.Assert(m.Next(8), qt.Equals, -1)
|
||||
c.Assert(m.String(), qt.Equals, "[2 1 3 7 4]")
|
||||
|
||||
var nilset *OrderedIntSet
|
||||
c.Assert(nilset.Len(), qt.Equals, 0)
|
||||
c.Assert(nilset.Has(1), qt.Equals, false)
|
||||
c.Assert(nilset.String(), qt.Equals, "[]")
|
||||
|
||||
var collected []int
|
||||
m.ForEachKey(func(key int) bool {
|
||||
collected = append(collected, key)
|
||||
return true
|
||||
})
|
||||
c.Assert(collected, qt.DeepEquals, []int{2, 1, 3, 7, 4})
|
||||
}
|
||||
|
||||
func BenchmarkOrderedIntSet(b *testing.B) {
|
||||
smallSet := NewOrderedIntSet()
|
||||
for i := range 8 {
|
||||
smallSet.Set(i)
|
||||
}
|
||||
mediumSet := NewOrderedIntSet()
|
||||
for i := range 64 {
|
||||
mediumSet.Set(i)
|
||||
}
|
||||
largeSet := NewOrderedIntSet()
|
||||
for i := range 1024 {
|
||||
largeSet.Set(i)
|
||||
}
|
||||
|
||||
b.Run("New", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
NewOrderedIntSet(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Has small", func(b *testing.B) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
smallSet.Has(i % 32)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Has medium", func(b *testing.B) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
mediumSet.Has(i % 32)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Next", func(b *testing.B) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
mediumSet.Next(i % 32)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEachKey small", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
smallSet.ForEachKey(func(key int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEachKey medium", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
mediumSet.ForEachKey(func(key int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEachKey large", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
largeSet.ForEachKey(func(key int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
// Copyright 2025 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 hreflect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
typeInt64 = reflect.TypeFor[int64]()
|
||||
typeFloat64 = reflect.TypeFor[float64]()
|
||||
typeString = reflect.TypeFor[string]()
|
||||
)
|
||||
|
||||
// ToInt64 converts v to int64 if possible, returning an error if not.
|
||||
func ToInt64E(v reflect.Value) (int64, error) {
|
||||
if v, ok := ConvertIfPossible(v, typeInt64); ok {
|
||||
return v.Int(), nil
|
||||
}
|
||||
return 0, errConvert(v, "int64")
|
||||
}
|
||||
|
||||
// ToInt64 converts v to int64 if possible. It panics if the conversion is not possible.
|
||||
func ToInt64(v reflect.Value) int64 {
|
||||
vv, err := ToInt64E(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return vv
|
||||
}
|
||||
|
||||
// ToFloat64E converts v to float64 if possible, returning an error if not.
|
||||
func ToFloat64E(v reflect.Value) (float64, error) {
|
||||
if v, ok := ConvertIfPossible(v, typeFloat64); ok {
|
||||
return v.Float(), nil
|
||||
}
|
||||
return 0, errConvert(v, "float64")
|
||||
}
|
||||
|
||||
// ToFloat64 converts v to float64 if possible, panicking if not.
|
||||
func ToFloat64(v reflect.Value) float64 {
|
||||
vv, err := ToFloat64E(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return vv
|
||||
}
|
||||
|
||||
// ToStringE converts v to string if possible, returning an error if not.
|
||||
func ToStringE(v reflect.Value) (string, error) {
|
||||
vv, err := ToStringValueE(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return vv.String(), nil
|
||||
}
|
||||
|
||||
func ToStringValueE(v reflect.Value) (reflect.Value, error) {
|
||||
if v, ok := ConvertIfPossible(v, typeString); ok {
|
||||
return v, nil
|
||||
}
|
||||
return reflect.Value{}, errConvert(v, "string")
|
||||
}
|
||||
|
||||
// ToString converts v to string if possible, panicking if not.
|
||||
func ToString(v reflect.Value) string {
|
||||
vv, err := ToStringE(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return vv
|
||||
}
|
||||
|
||||
func errConvert(v reflect.Value, s string) error {
|
||||
return fmt.Errorf("unable to convert value of type %q to %q", v.Type().String(), s)
|
||||
}
|
||||
|
||||
// ConvertIfPossible tries to convert val to typ if possible.
|
||||
// This is currently only implemented for int kinds,
|
||||
// added to handle the move to a new YAML library which produces uint64 for unsigned integers.
|
||||
// We can expand on this later if needed.
|
||||
// This conversion is lossless.
|
||||
// See Issue 14079.
|
||||
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
|
||||
switch val.Kind() {
|
||||
case reflect.Pointer, reflect.Interface:
|
||||
if val.IsNil() {
|
||||
// Return typ's zero value.
|
||||
return reflect.Zero(typ), true
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Type().AssignableTo(typ) {
|
||||
// No conversion needed.
|
||||
return val, true
|
||||
}
|
||||
|
||||
if IsInt(typ.Kind()) {
|
||||
return convertToIntIfPossible(val, typ)
|
||||
}
|
||||
if IsFloat(typ.Kind()) {
|
||||
return convertToFloatIfPossible(val, typ)
|
||||
}
|
||||
if IsUint(typ.Kind()) {
|
||||
return convertToUintIfPossible(val, typ)
|
||||
}
|
||||
if IsString(typ.Kind()) && IsString(val.Kind()) {
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
func convertToUintIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
|
||||
if IsInt(val.Kind()) {
|
||||
i := val.Int()
|
||||
if i < 0 {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
u := uint64(i)
|
||||
if typ.OverflowUint(u) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(u).Convert(typ), true
|
||||
}
|
||||
if IsUint(val.Kind()) {
|
||||
if typ.OverflowUint(val.Uint()) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
if IsFloat(val.Kind()) {
|
||||
f := val.Float()
|
||||
if f < 0 || f > float64(math.MaxUint64) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
if f != math.Trunc(f) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
u := uint64(f)
|
||||
if typ.OverflowUint(u) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(u).Convert(typ), true
|
||||
}
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
func convertToFloatIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
|
||||
if IsInt(val.Kind()) {
|
||||
i := val.Int()
|
||||
f := float64(i)
|
||||
if typ.OverflowFloat(f) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(f).Convert(typ), true
|
||||
}
|
||||
if IsUint(val.Kind()) {
|
||||
u := val.Uint()
|
||||
f := float64(u)
|
||||
if typ.OverflowFloat(f) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(f).Convert(typ), true
|
||||
}
|
||||
if IsFloat(val.Kind()) {
|
||||
if typ.OverflowFloat(val.Float()) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
func convertToIntIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
|
||||
if IsInt(val.Kind()) {
|
||||
if typ.OverflowInt(val.Int()) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
if IsUint(val.Kind()) {
|
||||
if val.Uint() > uint64(math.MaxInt64) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
if typ.OverflowInt(int64(val.Uint())) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
if IsFloat(val.Kind()) {
|
||||
f := val.Float()
|
||||
if f < float64(math.MinInt64) || f > float64(math.MaxInt64) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
if f != math.Trunc(f) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
if typ.OverflowInt(int64(f)) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(int64(f)).Convert(typ), true
|
||||
|
||||
}
|
||||
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
// Copyright 2025 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 hreflect
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/htesting/hqt"
|
||||
)
|
||||
|
||||
func TestToFuncs(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(ToInt64(reflect.ValueOf(int(42))), qt.Equals, int64(42))
|
||||
c.Assert(ToFloat64(reflect.ValueOf(float32(3.14))), hqt.IsSameFloat64, float64(3.14))
|
||||
c.Assert(ToString(reflect.ValueOf("hello")), qt.Equals, "hello")
|
||||
}
|
||||
|
||||
func TestConvertIfPossible(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
value any
|
||||
typ any
|
||||
expected any
|
||||
ok bool
|
||||
}{
|
||||
// From uint to int.
|
||||
{
|
||||
name: "uint64(math.MaxUint64) to int16",
|
||||
value: uint64(math.MaxUint64),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
|
||||
{
|
||||
name: "uint64(math.MaxUint64) to int64",
|
||||
value: uint64(math.MaxUint64),
|
||||
typ: int64(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "uint64(math.MaxInt16) to int16",
|
||||
value: uint64(math.MaxInt16),
|
||||
typ: int64(0),
|
||||
ok: true,
|
||||
expected: int64(math.MaxInt16),
|
||||
},
|
||||
// From int to int.
|
||||
{
|
||||
name: "int64(math.MaxInt64) to int16",
|
||||
value: int64(math.MaxInt64),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "int64(math.MaxInt16) to int",
|
||||
value: int64(math.MaxInt16),
|
||||
typ: int(0),
|
||||
ok: true,
|
||||
expected: int(math.MaxInt16),
|
||||
},
|
||||
|
||||
{
|
||||
name: "int64(math.MaxInt16) to int",
|
||||
value: int64(math.MaxInt16),
|
||||
typ: int(0),
|
||||
ok: true,
|
||||
expected: int(math.MaxInt16),
|
||||
},
|
||||
// From float64 to int.
|
||||
{
|
||||
name: "float64(1.5) to int",
|
||||
value: float64(1.5),
|
||||
typ: int(0),
|
||||
ok: false, // loss of precision
|
||||
},
|
||||
{
|
||||
name: "float64(1.0) to int",
|
||||
value: float64(1.0),
|
||||
typ: int(0),
|
||||
ok: true,
|
||||
expected: int(1),
|
||||
},
|
||||
{
|
||||
name: "float64(math.MaxInt16+1) to int16",
|
||||
value: float64(math.MaxInt16 + 1),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "float64(math.MaxFloat64) to int64",
|
||||
value: float64(math.MaxFloat64),
|
||||
typ: int64(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "float64(32767) to int16",
|
||||
value: float64(32767),
|
||||
typ: int16(0),
|
||||
ok: true,
|
||||
expected: int16(32767),
|
||||
},
|
||||
// From float32 to int.
|
||||
{
|
||||
name: "float32(1.5) to int",
|
||||
value: float32(1.5),
|
||||
typ: int(0),
|
||||
ok: false, // loss of precision
|
||||
},
|
||||
{
|
||||
name: "float32(1.0) to int",
|
||||
value: float32(1.0),
|
||||
typ: int(0),
|
||||
ok: true,
|
||||
expected: int(1),
|
||||
},
|
||||
{
|
||||
name: "float32(math.MaxFloat32) to int16",
|
||||
value: float32(math.MaxFloat32),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "float32(math.MaxFloat32) to int64",
|
||||
value: float32(math.MaxFloat32),
|
||||
typ: int64(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "float32(math.MaxInt16) to int16",
|
||||
value: float32(math.MaxInt16),
|
||||
typ: int16(0),
|
||||
ok: true,
|
||||
expected: int16(32767),
|
||||
},
|
||||
{
|
||||
name: "float32(math.MaxInt16+1) to int16",
|
||||
value: float32(math.MaxInt16 + 1),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
// Int to float.
|
||||
{
|
||||
name: "int16(32767) to float32",
|
||||
value: int16(32767),
|
||||
typ: float32(0),
|
||||
ok: true,
|
||||
expected: float32(32767),
|
||||
},
|
||||
{
|
||||
name: "int64(32767) to float32",
|
||||
value: int64(32767),
|
||||
typ: float32(0),
|
||||
ok: true,
|
||||
expected: float32(32767),
|
||||
},
|
||||
{
|
||||
name: "int64(math.MaxInt64) to float32",
|
||||
value: int64(math.MaxInt64),
|
||||
typ: float32(0),
|
||||
ok: true,
|
||||
expected: float32(math.MaxInt64),
|
||||
},
|
||||
{
|
||||
name: "int64(math.MaxInt64) to float64",
|
||||
value: int64(math.MaxInt64),
|
||||
typ: float64(0),
|
||||
ok: true,
|
||||
expected: float64(math.MaxInt64),
|
||||
},
|
||||
// Int to uint.
|
||||
{
|
||||
name: "int16(32767) to uint16",
|
||||
value: int16(32767),
|
||||
typ: uint16(0),
|
||||
ok: true,
|
||||
expected: uint16(32767),
|
||||
},
|
||||
{
|
||||
name: "int16(32767) to uint8",
|
||||
value: int16(32767),
|
||||
typ: uint8(0),
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "float64(3.14) to uint64",
|
||||
value: float64(3.14),
|
||||
typ: uint64(0),
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "float64(3.0) to uint64",
|
||||
value: float64(3.0),
|
||||
typ: uint64(0),
|
||||
ok: true,
|
||||
expected: uint64(3),
|
||||
},
|
||||
// From uint to float.
|
||||
{
|
||||
name: "uint64(math.MaxInt16) to float64",
|
||||
value: uint64(math.MaxInt16),
|
||||
typ: float64(0),
|
||||
ok: true,
|
||||
expected: float64(math.MaxInt16),
|
||||
},
|
||||
// Float to float.
|
||||
{
|
||||
name: "float64(3.14) to float32",
|
||||
value: float64(3.14),
|
||||
typ: float32(0),
|
||||
ok: true,
|
||||
expected: float32(3.14),
|
||||
},
|
||||
{
|
||||
name: "float32(3.14) to float64",
|
||||
value: float32(3.14),
|
||||
typ: float64(0),
|
||||
ok: true,
|
||||
expected: float64(3.14),
|
||||
},
|
||||
{
|
||||
name: "float64(3.14) to float64",
|
||||
value: float64(3.14),
|
||||
typ: float64(0),
|
||||
ok: true,
|
||||
expected: float64(3.14),
|
||||
},
|
||||
} {
|
||||
|
||||
v, ok := ConvertIfPossible(reflect.ValueOf(test.value), reflect.TypeOf(test.typ))
|
||||
c.Assert(ok, qt.Equals, test.ok, qt.Commentf("test case: %s", test.name))
|
||||
if test.ok {
|
||||
c.Assert(v.Interface(), hqt.IsSameNumber, test.expected, qt.Commentf("test case: %s", test.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertIfPossibleMisc(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
type s string
|
||||
|
||||
var (
|
||||
i = int32(42)
|
||||
i64 = int64(i)
|
||||
iv any = i
|
||||
ip = &i
|
||||
inil any = (*int32)(nil)
|
||||
shello = s("hello")
|
||||
)
|
||||
|
||||
convertOK := func(v any, typ any) any {
|
||||
rv, ok := ConvertIfPossible(reflect.ValueOf(v), reflect.TypeOf(typ))
|
||||
c.Assert(ok, qt.IsTrue)
|
||||
return rv.Interface()
|
||||
}
|
||||
|
||||
c.Assert(convertOK(shello, ""), qt.Equals, "hello")
|
||||
c.Assert(convertOK(ip, int64(0)), qt.Equals, i64)
|
||||
c.Assert(convertOK(iv, int64(0)), qt.Equals, i64)
|
||||
c.Assert(convertOK(inil, int64(0)), qt.Equals, int64(0))
|
||||
}
|
||||
|
||||
func BenchmarkToInt64(b *testing.B) {
|
||||
v := reflect.ValueOf(int(42))
|
||||
for b.Loop() {
|
||||
ToInt64(v)
|
||||
}
|
||||
}
|
||||
+39
-141
@@ -1,4 +1,6 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -20,16 +22,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
// IsInterfaceOrPointer returns whether the given kind is an interface or a pointer.
|
||||
func IsInterfaceOrPointer(kind reflect.Kind) bool {
|
||||
return kind == reflect.Interface || kind == reflect.Pointer
|
||||
}
|
||||
|
||||
// TODO(bep) replace the private versions in /tpl with these.
|
||||
// IsNumber returns whether the given kind is a number.
|
||||
func IsNumber(kind reflect.Kind) bool {
|
||||
@@ -66,11 +63,6 @@ func IsFloat(kind reflect.Kind) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsString returns whether the given kind is a string.
|
||||
func IsString(kind reflect.Kind) bool {
|
||||
return kind == reflect.String
|
||||
}
|
||||
|
||||
// IsTruthful returns whether in represents a truthful value.
|
||||
// See IsTruthfulValue
|
||||
func IsTruthful(in any) bool {
|
||||
@@ -82,29 +74,7 @@ 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.TypeFor[types.Zeroer]()
|
||||
|
||||
var isZeroCache sync.Map
|
||||
|
||||
func implementsIsZero(tp reflect.Type) bool {
|
||||
v, ok := isZeroCache.Load(tp)
|
||||
if ok {
|
||||
return v.(bool)
|
||||
}
|
||||
implements := tp.Implements(zeroType)
|
||||
isZeroCache.Store(tp, implements)
|
||||
return implements
|
||||
}
|
||||
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
|
||||
|
||||
// IsTruthfulValue returns whether the given value has a meaningful truth value.
|
||||
// This is based on template.IsTrue in Go's stdlib, but also considers
|
||||
@@ -114,18 +84,14 @@ func implementsIsZero(tp reflect.Type) bool {
|
||||
// Based on:
|
||||
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
|
||||
func IsTruthfulValue(val reflect.Value) (truth bool) {
|
||||
val, isNil := Indirect(val)
|
||||
val = indirectInterface(val)
|
||||
|
||||
if !val.IsValid() {
|
||||
// Something like: var x any, never set. It's a form of nil.
|
||||
// Something like var x interface{}, never set. It's a form of nil.
|
||||
return
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Pointer && isNil {
|
||||
return
|
||||
}
|
||||
|
||||
if implementsIsZero(val.Type()) {
|
||||
if val.Type().Implements(zeroType) {
|
||||
return !val.Interface().(types.Zeroer).IsZero()
|
||||
}
|
||||
|
||||
@@ -136,7 +102,7 @@ func IsTruthfulValue(val reflect.Value) (truth bool) {
|
||||
truth = val.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
truth = val.Complex() != 0
|
||||
case reflect.Chan, reflect.Func, reflect.Pointer, reflect.UnsafePointer, reflect.Interface:
|
||||
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
|
||||
truth = !val.IsNil()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
truth = val.Int() != 0
|
||||
@@ -158,31 +124,15 @@ type methodKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var (
|
||||
methodIndexCache sync.Map
|
||||
methodCache sync.Map
|
||||
)
|
||||
|
||||
// GetMethodByNameForType returns the method with the given name for the given type,
|
||||
// or a zero Method if no such method exists.
|
||||
// It panics if tp is an interface type.
|
||||
// It caches the lookup.
|
||||
func GetMethodByNameForType(tp reflect.Type, name string) reflect.Method {
|
||||
if tp.Kind() == reflect.Interface {
|
||||
// Func field is nil for interface types.
|
||||
panic("not supported for interface types")
|
||||
}
|
||||
k := methodKey{tp, name}
|
||||
v, found := methodCache.Load(k)
|
||||
if found {
|
||||
return v.(reflect.Method)
|
||||
}
|
||||
m, _ := tp.MethodByName(name)
|
||||
methodCache.Store(k, m)
|
||||
return m
|
||||
type methods struct {
|
||||
sync.RWMutex
|
||||
cache map[methodKey]int
|
||||
}
|
||||
|
||||
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the lookup.
|
||||
var methodCache = &methods{cache: make(map[methodKey]int)}
|
||||
|
||||
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
|
||||
// type lookup.
|
||||
func GetMethodByName(v reflect.Value, name string) reflect.Value {
|
||||
index := GetMethodIndexByName(v.Type(), name)
|
||||
|
||||
@@ -197,16 +147,22 @@ func GetMethodByName(v reflect.Value, name string) reflect.Value {
|
||||
// -1 if no such method exists.
|
||||
func GetMethodIndexByName(tp reflect.Type, name string) int {
|
||||
k := methodKey{tp, name}
|
||||
v, found := methodIndexCache.Load(k)
|
||||
methodCache.RLock()
|
||||
index, found := methodCache.cache[k]
|
||||
methodCache.RUnlock()
|
||||
if found {
|
||||
return v.(int)
|
||||
return index
|
||||
}
|
||||
|
||||
methodCache.Lock()
|
||||
defer methodCache.Unlock()
|
||||
|
||||
m, ok := tp.MethodByName(name)
|
||||
index := m.Index
|
||||
index = m.Index
|
||||
if !ok {
|
||||
index = -1
|
||||
}
|
||||
methodIndexCache.Store(k, index)
|
||||
methodCache.cache[k] = index
|
||||
|
||||
if !ok {
|
||||
return -1
|
||||
@@ -216,8 +172,8 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
|
||||
}
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeFor[time.Time]()
|
||||
asTimeProviderType = reflect.TypeFor[htime.AsTimeProvider]()
|
||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||
asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem()
|
||||
)
|
||||
|
||||
// IsTime returns whether tp is a time.Time type or if it can be converted into one
|
||||
@@ -240,7 +196,7 @@ func IsValid(v reflect.Value) bool {
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return !v.IsNil()
|
||||
}
|
||||
|
||||
@@ -267,28 +223,6 @@ func AsTime(v reflect.Value, loc *time.Location) (time.Time, bool) {
|
||||
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
|
||||
}
|
||||
|
||||
// CallMethodByName calls the method with the given name on v.
|
||||
func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
|
||||
fn := v.MethodByName(name)
|
||||
var args []reflect.Value
|
||||
@@ -306,56 +240,20 @@ func CallMethodByName(cxt context.Context, name string, v reflect.Value) []refle
|
||||
return fn.Call(args)
|
||||
}
|
||||
|
||||
// Indirect unwraps interfaces and pointers until it finds a non-interface/pointer value.
|
||||
// If a nil is encountered, the second return value is true.
|
||||
// If a pointer to a struct is encountered, it is not unwrapped.
|
||||
func Indirect(v reflect.Value) (vv reflect.Value, isNil bool) {
|
||||
for ; IsInterfaceOrPointer(v.Kind()); v = v.Elem() {
|
||||
if IsNil(v) {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() != reflect.Interface {
|
||||
// A pointer.
|
||||
if v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
if v.Elem().Kind() == reflect.Struct {
|
||||
// Avoid unwrapping pointers to structs.
|
||||
break
|
||||
}
|
||||
}
|
||||
// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
|
||||
func indirectInterface(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Interface {
|
||||
return v
|
||||
}
|
||||
return v, false
|
||||
if v.IsNil() {
|
||||
return reflect.Value{}
|
||||
}
|
||||
return v.Elem()
|
||||
}
|
||||
|
||||
// IndirectElem is like Indirect, but if the final value is a pointer, it unwraps it.
|
||||
func IndirectElem(v reflect.Value) (vv reflect.Value, isNil bool) {
|
||||
vv, isNil = Indirect(v)
|
||||
if isNil {
|
||||
return vv, isNil
|
||||
}
|
||||
if vv.Kind() == reflect.Pointer {
|
||||
vv = vv.Elem()
|
||||
}
|
||||
return vv, isNil
|
||||
}
|
||||
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
|
||||
// IsNil reports whether v is nil.
|
||||
// Based on reflect.Value.IsNil, but also considers invalid values as nil.
|
||||
func IsNil(v reflect.Value) bool {
|
||||
if !v.IsValid() {
|
||||
return true
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||
return v.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var contextInterface = reflect.TypeFor[context.Context]()
|
||||
|
||||
var isContextCache = hmaps.NewCache[reflect.Type, bool]()
|
||||
var isContextCache = maps.NewCache[reflect.Type, bool]()
|
||||
|
||||
type k string
|
||||
|
||||
|
||||
+16
-153
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2019 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.
|
||||
@@ -20,32 +20,15 @@ import (
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/htesting/hqt"
|
||||
)
|
||||
|
||||
type zeroStruct struct {
|
||||
zero bool
|
||||
}
|
||||
|
||||
func (z zeroStruct) IsZero() bool {
|
||||
return z.zero
|
||||
}
|
||||
|
||||
func TestIsTruthful(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var nilpointerZero *zeroStruct
|
||||
|
||||
c.Assert(IsTruthful(true), qt.Equals, true)
|
||||
c.Assert(IsTruthful(false), qt.Equals, false)
|
||||
c.Assert(IsTruthful(time.Now()), qt.Equals, true)
|
||||
c.Assert(IsTruthful(time.Time{}), qt.Equals, false)
|
||||
c.Assert(IsTruthful(&zeroStruct{zero: false}), qt.Equals, true)
|
||||
c.Assert(IsTruthful(&zeroStruct{zero: true}), qt.Equals, false)
|
||||
c.Assert(IsTruthful(zeroStruct{zero: false}), qt.Equals, true)
|
||||
c.Assert(IsTruthful(zeroStruct{zero: true}), qt.Equals, false)
|
||||
c.Assert(IsTruthful(nil), qt.Equals, false)
|
||||
c.Assert(IsTruthful(nilpointerZero), qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestGetMethodByName(t *testing.T) {
|
||||
@@ -67,103 +50,18 @@ func TestIsContextType(t *testing.T) {
|
||||
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})
|
||||
}
|
||||
|
||||
type testIndirectStruct struct {
|
||||
S string
|
||||
}
|
||||
|
||||
func (t *testIndirectStruct) GetS() string {
|
||||
return t.S
|
||||
}
|
||||
|
||||
func (t testIndirectStruct) Foo() string {
|
||||
return "bar"
|
||||
}
|
||||
|
||||
type testIndirectStructNoMethods struct {
|
||||
S string
|
||||
}
|
||||
|
||||
func TestIsNil(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var (
|
||||
nilPtr *testIndirectStruct
|
||||
nilIface any = nil
|
||||
nonNilIface any = &testIndirectStruct{S: "hello"}
|
||||
)
|
||||
|
||||
c.Assert(IsNil(reflect.ValueOf(nilPtr)), qt.Equals, true)
|
||||
c.Assert(IsNil(reflect.ValueOf(nilIface)), qt.Equals, true)
|
||||
c.Assert(IsNil(reflect.ValueOf(nonNilIface)), qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestIndirectInterface(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var (
|
||||
structWithMethods = testIndirectStruct{S: "hello"}
|
||||
structWithMethodsPointer = &testIndirectStruct{S: "hello"}
|
||||
structWithMethodsPointerAny any = structWithMethodsPointer
|
||||
structPointerToPointer = &structWithMethodsPointer
|
||||
structNoMethodsPtr = &testIndirectStructNoMethods{S: "no methods"}
|
||||
structNoMethods = testIndirectStructNoMethods{S: "no methods"}
|
||||
intValue = 32
|
||||
intPtr = &intValue
|
||||
nilPtr *testIndirectStruct
|
||||
nilIface any = nil
|
||||
)
|
||||
|
||||
ind := func(v any) any {
|
||||
c.Helper()
|
||||
vv, isNil := Indirect(reflect.ValueOf(v))
|
||||
c.Assert(isNil, qt.IsFalse)
|
||||
return vv.Interface()
|
||||
}
|
||||
|
||||
c.Assert(ind(intValue), hqt.IsSameType, 32)
|
||||
c.Assert(ind(intPtr), hqt.IsSameType, 32)
|
||||
c.Assert(ind(structNoMethodsPtr), hqt.IsSameType, structNoMethodsPtr)
|
||||
c.Assert(ind(structWithMethods), hqt.IsSameType, structWithMethods)
|
||||
c.Assert(ind(structNoMethods), hqt.IsSameType, structNoMethods)
|
||||
c.Assert(ind(structPointerToPointer), hqt.IsSameType, &testIndirectStruct{})
|
||||
c.Assert(ind(structWithMethodsPointer), hqt.IsSameType, &testIndirectStruct{})
|
||||
c.Assert(ind(structWithMethodsPointerAny), hqt.IsSameType, structWithMethodsPointer)
|
||||
|
||||
vv, isNil := Indirect(reflect.ValueOf(nilPtr))
|
||||
c.Assert(isNil, qt.IsTrue)
|
||||
c.Assert(vv, qt.Equals, reflect.ValueOf(nilPtr))
|
||||
|
||||
vv, isNil = Indirect(reflect.ValueOf(nilIface))
|
||||
c.Assert(isNil, qt.IsFalse)
|
||||
c.Assert(vv, qt.Equals, reflect.ValueOf(nilIface))
|
||||
}
|
||||
|
||||
func BenchmarkIsContextType(b *testing.B) {
|
||||
const size = 1000
|
||||
type k string
|
||||
b.Run("value", func(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
ctxs := make([]reflect.Type, size)
|
||||
for i := range size {
|
||||
ctxs := make([]reflect.Type, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i))
|
||||
}
|
||||
|
||||
for i := 0; b.Loop(); i++ {
|
||||
idx := i % size
|
||||
if !IsContextType(ctxs[idx]) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsContextType(ctxs[i]) {
|
||||
b.Fatal("not context")
|
||||
}
|
||||
}
|
||||
@@ -171,7 +69,7 @@ func BenchmarkIsContextType(b *testing.B) {
|
||||
|
||||
b.Run("background", func(b *testing.B) {
|
||||
var ctxt reflect.Type = reflect.TypeOf(context.Background())
|
||||
for b.Loop() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsContextType(ctxt) {
|
||||
b.Fatal("not context")
|
||||
}
|
||||
@@ -179,25 +77,14 @@ func BenchmarkIsContextType(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkIsTruthFulValue(b *testing.B) {
|
||||
var (
|
||||
stringHugo = reflect.ValueOf("Hugo")
|
||||
stringEmpty = reflect.ValueOf("")
|
||||
zero = reflect.ValueOf(time.Time{})
|
||||
timeNow = reflect.ValueOf(time.Now())
|
||||
boolTrue = reflect.ValueOf(true)
|
||||
boolFalse = reflect.ValueOf(false)
|
||||
nilPointer = reflect.ValueOf((*zeroStruct)(nil))
|
||||
)
|
||||
func BenchmarkIsTruthFul(b *testing.B) {
|
||||
v := reflect.ValueOf("Hugo")
|
||||
|
||||
for b.Loop() {
|
||||
IsTruthfulValue(stringHugo)
|
||||
IsTruthfulValue(stringEmpty)
|
||||
IsTruthfulValue(zero)
|
||||
IsTruthfulValue(timeNow)
|
||||
IsTruthfulValue(boolTrue)
|
||||
IsTruthfulValue(boolFalse)
|
||||
IsTruthfulValue(nilPointer)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsTruthfulValue(v) {
|
||||
b.Fatal("not truthful")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,38 +110,14 @@ func (t *testStruct) Method5() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func BenchmarkGetMethodByNameForType(b *testing.B) {
|
||||
tp := reflect.TypeFor[*testStruct]()
|
||||
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
|
||||
|
||||
for b.Loop() {
|
||||
for _, method := range methods {
|
||||
_ = GetMethodByNameForType(tp, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetMethodByName(b *testing.B) {
|
||||
v := reflect.ValueOf(&testStruct{})
|
||||
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
|
||||
|
||||
for b.Loop() {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, method := range methods {
|
||||
_ = GetMethodByName(v, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+14
-70
@@ -16,8 +16,6 @@ package hstrings
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -52,7 +50,12 @@ func (s StringEqualFold) Eq(s2 any) bool {
|
||||
|
||||
// EqualAny returns whether a string is equal to any of the given strings.
|
||||
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.
|
||||
@@ -97,22 +100,17 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
|
||||
return reCache.getOrCompileRegexp(pattern)
|
||||
}
|
||||
|
||||
// HasAnyPrefix checks if the string s has any of the prefixes given.
|
||||
func HasAnyPrefix(s string, prefixes ...string) bool {
|
||||
for _, p := range prefixes {
|
||||
if strings.HasPrefix(s, p) {
|
||||
// InSlice checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
func InSlice(arr []string, el string) bool {
|
||||
for _, v := range arr {
|
||||
if v == el {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InSlice checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
func InSlice(arr []string, el string) bool {
|
||||
return slices.Contains(arr, el)
|
||||
}
|
||||
|
||||
// InSlicEqualFold checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
// It uses strings.EqualFold to compare.
|
||||
@@ -139,61 +137,7 @@ func ToString(v any) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// UniqueStrings returns a new slice with any duplicates removed.
|
||||
func UniqueStrings(s []string) []string {
|
||||
unique := make([]string, 0, len(s))
|
||||
for i, val := range s {
|
||||
var seen bool
|
||||
for j := range i {
|
||||
if s[j] == val {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
unique = append(unique, val)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
// UniqueStringsReuse returns a slice with any duplicates removed.
|
||||
// It will modify the input slice.
|
||||
func UniqueStringsReuse(s []string) []string {
|
||||
result := s[:0]
|
||||
for i, val := range s {
|
||||
var seen bool
|
||||
|
||||
for j := range i {
|
||||
if s[j] == val {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !seen {
|
||||
result = append(result, val)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UniqueStringsSorted returns a sorted slice with any duplicates removed.
|
||||
// It will modify the input slice.
|
||||
func UniqueStringsSorted(s []string) []string {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
ss := sort.StringSlice(s)
|
||||
ss.Sort()
|
||||
i := 0
|
||||
for j := 1; j < len(s); j++ {
|
||||
if !ss.Less(i, j) {
|
||||
continue
|
||||
}
|
||||
i++
|
||||
s[i] = s[j]
|
||||
}
|
||||
|
||||
return s[:i+1]
|
||||
type Tuple struct {
|
||||
First string
|
||||
Second string
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -14,7 +14,6 @@
|
||||
package hstrings
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
@@ -44,91 +43,14 @@ func TestGetOrCompileRegexp(t *testing.T) {
|
||||
c.Assert(re.MatchString("123"), qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestUniqueStrings(t *testing.T) {
|
||||
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
|
||||
output := UniqueStrings(in)
|
||||
expected := []string{"a", "b", "c", "", "d"}
|
||||
if !reflect.DeepEqual(output, expected) {
|
||||
t.Errorf("Expected %#v, got %#v\n", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueStringsReuse(t *testing.T) {
|
||||
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
|
||||
output := UniqueStringsReuse(in)
|
||||
expected := []string{"a", "b", "c", "", "d"}
|
||||
if !reflect.DeepEqual(output, expected) {
|
||||
t.Errorf("Expected %#v, got %#v\n", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueStringsSorted(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
in := []string{"a", "a", "b", "c", "b", "", "a", "", "d"}
|
||||
output := UniqueStringsSorted(in)
|
||||
expected := []string{"", "a", "b", "c", "d"}
|
||||
c.Assert(output, qt.DeepEquals, expected)
|
||||
c.Assert(UniqueStringsSorted(nil), qt.IsNil)
|
||||
}
|
||||
|
||||
// Note that these cannot use b.Loop() because of golang/go#27217.
|
||||
func BenchmarkUniqueStrings(b *testing.B) {
|
||||
input := []string{"a", "b", "d", "e", "d", "h", "a", "i"}
|
||||
|
||||
b.Run("Safe", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
result := UniqueStrings(input)
|
||||
if len(result) != 6 {
|
||||
b.Fatalf("invalid count: %d", len(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Reuse slice", func(b *testing.B) {
|
||||
inputs := make([][]string, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := make([]string, len(input))
|
||||
copy(inputc, input)
|
||||
inputs[i] = inputc
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := inputs[i]
|
||||
|
||||
result := UniqueStringsReuse(inputc)
|
||||
if len(result) != 6 {
|
||||
b.Fatalf("invalid count: %d", len(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Reuse slice sorted", func(b *testing.B) {
|
||||
inputs := make([][]string, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := make([]string, len(input))
|
||||
copy(inputc, input)
|
||||
inputs[i] = inputc
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := inputs[i]
|
||||
|
||||
result := UniqueStringsSorted(inputc)
|
||||
if len(result) != 6 {
|
||||
b.Fatalf("invalid count: %d", len(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkGetOrCompileRegexp(b *testing.B) {
|
||||
for b.Loop() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
GetOrCompileRegexp(`\d+`)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompileRegexp(b *testing.B) {
|
||||
for b.Loop() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
regexp.MustCompile(`\d+`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
// Copyright 2025 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 hsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// OnceMore is similar to sync.Once.
|
||||
//
|
||||
// Additional features are:
|
||||
// * it can be reset, so the action can be repeated if needed
|
||||
// * it has methods to check if it's done or in progress
|
||||
type OnceMore struct {
|
||||
_ doNotCopy
|
||||
done atomic.Bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (t *OnceMore) Do(f func()) {
|
||||
if t.Done() {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
// Double check
|
||||
if t.Done() {
|
||||
return
|
||||
}
|
||||
|
||||
defer t.done.Store(true)
|
||||
f()
|
||||
}
|
||||
|
||||
func (t *OnceMore) Done() bool {
|
||||
return t.done.Load()
|
||||
}
|
||||
|
||||
func (t *OnceMore) Reset() {
|
||||
t.mu.Lock()
|
||||
t.done.Store(false)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
type ValueResetter[T any] struct {
|
||||
reset func()
|
||||
f func(context.Context) T
|
||||
}
|
||||
|
||||
func (v *ValueResetter[T]) Value(ctx context.Context) T {
|
||||
return v.f(ctx)
|
||||
}
|
||||
|
||||
func (v *ValueResetter[T]) Reset() {
|
||||
v.reset()
|
||||
}
|
||||
|
||||
// OnceMoreValue returns a function that invokes f only once and returns the value
|
||||
// returned by f. The returned function may be called concurrently.
|
||||
//
|
||||
// If f panics, the returned function will panic with the same value on every call.
|
||||
func OnceMoreValue[T any](f func(context.Context) T) ValueResetter[T] {
|
||||
v := struct {
|
||||
f func(context.Context) T
|
||||
once OnceMore
|
||||
ok bool
|
||||
p any
|
||||
result T
|
||||
}{
|
||||
f: f,
|
||||
}
|
||||
ff := func(ctx context.Context) T {
|
||||
v.once.Do(func() {
|
||||
v.ok = false
|
||||
defer func() {
|
||||
v.p = recover()
|
||||
if !v.ok {
|
||||
panic(v.p)
|
||||
}
|
||||
}()
|
||||
v.result = v.f(ctx)
|
||||
v.ok = true
|
||||
})
|
||||
if !v.ok {
|
||||
panic(v.p)
|
||||
}
|
||||
return v.result
|
||||
}
|
||||
|
||||
return ValueResetter[T]{
|
||||
reset: v.once.Reset,
|
||||
f: ff,
|
||||
}
|
||||
}
|
||||
|
||||
type FuncResetter struct {
|
||||
f func(context.Context) error
|
||||
reset func()
|
||||
}
|
||||
|
||||
func (v *FuncResetter) Do(ctx context.Context) error {
|
||||
return v.f(ctx)
|
||||
}
|
||||
|
||||
func (v *FuncResetter) Reset() {
|
||||
v.reset()
|
||||
}
|
||||
|
||||
func OnceMoreFunc(f func(context.Context) error) FuncResetter {
|
||||
v := struct {
|
||||
f func(context.Context) error
|
||||
once OnceMore
|
||||
ok bool
|
||||
err error
|
||||
p any
|
||||
}{
|
||||
f: f,
|
||||
}
|
||||
ff := func(ctx context.Context) error {
|
||||
v.once.Do(func() {
|
||||
v.ok = false
|
||||
defer func() {
|
||||
v.p = recover()
|
||||
if !v.ok {
|
||||
panic(v.p)
|
||||
}
|
||||
}()
|
||||
v.err = v.f(ctx)
|
||||
v.ok = true
|
||||
})
|
||||
if !v.ok {
|
||||
panic(v.p)
|
||||
}
|
||||
return v.err
|
||||
}
|
||||
|
||||
return FuncResetter{
|
||||
f: ff,
|
||||
reset: v.once.Reset,
|
||||
}
|
||||
}
|
||||
|
||||
type doNotCopy struct{}
|
||||
|
||||
// Lock is a no-op used by -copylocks checker from `go vet`.
|
||||
func (*doNotCopy) Lock() {}
|
||||
func (*doNotCopy) Unlock() {}
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright 2025 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 hsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestOnceMoreValue(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var counter int
|
||||
f := func(context.Context) int {
|
||||
counter++
|
||||
return counter
|
||||
}
|
||||
|
||||
omf := OnceMoreValue(f)
|
||||
for range 10 {
|
||||
c.Assert(omf.Value(context.Background()), qt.Equals, 1)
|
||||
}
|
||||
omf.Reset()
|
||||
for range 10 {
|
||||
c.Assert(omf.Value(context.Background()), qt.Equals, 2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnceMoreFunc(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var counter int
|
||||
f := func(context.Context) error {
|
||||
counter++
|
||||
return nil
|
||||
}
|
||||
|
||||
omf := OnceMoreFunc(f)
|
||||
for range 10 {
|
||||
c.Assert(omf.Do(context.Background()), qt.IsNil)
|
||||
c.Assert(counter, qt.Equals, 1)
|
||||
}
|
||||
omf.Reset()
|
||||
for range 10 {
|
||||
c.Assert(omf.Do(context.Background()), qt.IsNil)
|
||||
c.Assert(counter, qt.Equals, 2)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOnceMoreValue(b *testing.B) {
|
||||
var counter int
|
||||
f := func(context.Context) int {
|
||||
counter++
|
||||
return counter
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
omf := OnceMoreValue(f)
|
||||
for range 10 {
|
||||
omf.Value(context.Background())
|
||||
}
|
||||
omf.Reset()
|
||||
for range 10 {
|
||||
omf.Value(context.Background())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,9 @@ func TestApplyWithContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- config.toml --
|
||||
defaultContentLanguage = 'it'
|
||||
-- layouts/home.html --
|
||||
-- layouts/index.html --
|
||||
{{ $dates := slice
|
||||
"2022-01-03"
|
||||
"2022-02-01"
|
||||
|
||||
+18
-18
@@ -18,10 +18,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/golocales"
|
||||
|
||||
"github.com/bep/clocks"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/locales"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -78,9 +78,9 @@ var (
|
||||
Clock = clocks.System()
|
||||
)
|
||||
|
||||
func NewTimeFormatter(ltr golocales.Translator) TimeFormatter {
|
||||
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
|
||||
if ltr == nil {
|
||||
panic("must provide a golocales.Translator")
|
||||
panic("must provide a locales.Translator")
|
||||
}
|
||||
return TimeFormatter{
|
||||
ltr: ltr,
|
||||
@@ -89,7 +89,7 @@ func NewTimeFormatter(ltr golocales.Translator) TimeFormatter {
|
||||
|
||||
// TimeFormatter is locale aware.
|
||||
type TimeFormatter struct {
|
||||
ltr golocales.Translator
|
||||
ltr locales.Translator
|
||||
}
|
||||
|
||||
func (f TimeFormatter) Format(t time.Time, layout string) string {
|
||||
@@ -101,39 +101,39 @@ func (f TimeFormatter) Format(t time.Time, layout string) string {
|
||||
// It may be one of Hugo's custom layouts.
|
||||
switch strings.ToLower(layout[1:]) {
|
||||
case "date_full":
|
||||
return f.ltr.FormatDateFull(t)
|
||||
return f.ltr.FmtDateFull(t)
|
||||
case "date_long":
|
||||
return f.ltr.FormatDateLong(t)
|
||||
return f.ltr.FmtDateLong(t)
|
||||
case "date_medium":
|
||||
return f.ltr.FormatDateMedium(t)
|
||||
return f.ltr.FmtDateMedium(t)
|
||||
case "date_short":
|
||||
return f.ltr.FormatDateShort(t)
|
||||
return f.ltr.FmtDateShort(t)
|
||||
case "time_full":
|
||||
return f.ltr.FormatTimeFull(t)
|
||||
return f.ltr.FmtTimeFull(t)
|
||||
case "time_long":
|
||||
return f.ltr.FormatTimeLong(t)
|
||||
return f.ltr.FmtTimeLong(t)
|
||||
case "time_medium":
|
||||
return f.ltr.FormatTimeMedium(t)
|
||||
return f.ltr.FmtTimeMedium(t)
|
||||
case "time_short":
|
||||
return f.ltr.FormatTimeShort(t)
|
||||
return f.ltr.FmtTimeShort(t)
|
||||
}
|
||||
}
|
||||
|
||||
s := t.Format(layout)
|
||||
|
||||
monthIdx := t.Month() - 1 // time.Month is 1-based, but our month name slices are 0-based.
|
||||
monthIdx := t.Month() - 1 // Month() starts at 1.
|
||||
dayIdx := t.Weekday()
|
||||
|
||||
if strings.Contains(layout, "January") {
|
||||
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthsWide()[monthIdx])
|
||||
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
|
||||
} else if strings.Contains(layout, "Jan") {
|
||||
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthsAbbreviated()[monthIdx])
|
||||
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthAbbreviated(t.Month()))
|
||||
}
|
||||
|
||||
if strings.Contains(layout, "Monday") {
|
||||
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdaysWide()[dayIdx])
|
||||
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
|
||||
} else if strings.Contains(layout, "Mon") {
|
||||
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdaysAbbreviated()[dayIdx])
|
||||
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday()))
|
||||
}
|
||||
|
||||
return s
|
||||
|
||||
+21
-22
@@ -17,9 +17,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bep/golocales"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
translators "github.com/gohugoio/localescompressed"
|
||||
)
|
||||
|
||||
func TestTimeFormatter(t *testing.T) {
|
||||
@@ -35,7 +34,7 @@ func TestTimeFormatter(t *testing.T) {
|
||||
mondayNovemberFirst = mondayNovemberFirst.Add(33 * time.Second)
|
||||
|
||||
c.Run("Norsk nynorsk", func(c *qt.C) {
|
||||
f := NewTimeFormatter(golocales.New("nn"))
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
|
||||
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "onsdag juni 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "on. juni 6 2018")
|
||||
@@ -43,35 +42,35 @@ func TestTimeFormatter(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("Custom layouts Norsk nynorsk", func(c *qt.C) {
|
||||
f := NewTimeFormatter(golocales.New("nn"))
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
|
||||
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "onsdag 6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.18")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.2018")
|
||||
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "kl. 02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "02:09:37")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "02:09")
|
||||
})
|
||||
|
||||
c.Run("Custom layouts English", func(c *qt.C) {
|
||||
f := NewTimeFormatter(golocales.New("en"))
|
||||
f := NewTimeFormatter(translators.GetTranslator("en"))
|
||||
|
||||
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "Wednesday, June 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "June 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "Jun 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "6/6/18")
|
||||
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37\u202fam UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37\u202fam UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37\u202fam")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09\u202fam")
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37 am UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37 am UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37 am")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09 am")
|
||||
})
|
||||
|
||||
c.Run("English", func(c *qt.C) {
|
||||
f := NewTimeFormatter(golocales.New("en"))
|
||||
f := NewTimeFormatter(translators.GetTranslator("en"))
|
||||
|
||||
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "Wednesday Jun 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "Wed June 6 2018")
|
||||
@@ -79,31 +78,31 @@ func TestTimeFormatter(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("Weekdays German", func(c *qt.C) {
|
||||
tr := golocales.New("de")
|
||||
tr := translators.GetTranslator("de")
|
||||
f := NewTimeFormatter(tr)
|
||||
|
||||
// Issue #9107
|
||||
for i, weekDayWideGerman := range []string{"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"} {
|
||||
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
|
||||
c.Assert(tr.WeekdaysWide()[date.Weekday()], qt.Equals, weekDayWideGerman)
|
||||
c.Assert(tr.WeekdayWide(date.Weekday()), qt.Equals, weekDayWideGerman)
|
||||
c.Assert(f.Format(date, "Monday"), qt.Equals, weekDayWideGerman)
|
||||
}
|
||||
|
||||
for i, weekDayAbbreviatedGerman := range []string{"Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa.", "So."} {
|
||||
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
|
||||
c.Assert(tr.WeekdaysAbbreviated()[date.Weekday()], qt.Equals, weekDayAbbreviatedGerman)
|
||||
c.Assert(tr.WeekdayAbbreviated(date.Weekday()), qt.Equals, weekDayAbbreviatedGerman)
|
||||
c.Assert(f.Format(date, "Mon"), qt.Equals, weekDayAbbreviatedGerman)
|
||||
}
|
||||
})
|
||||
|
||||
c.Run("Months German", func(c *qt.C) {
|
||||
tr := golocales.New("de")
|
||||
tr := translators.GetTranslator("de")
|
||||
f := NewTimeFormatter(tr)
|
||||
|
||||
// Issue #9107
|
||||
for i, monthWideNorway := range []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli"} {
|
||||
date := jan06.Add(time.Duration(i*24*31) * time.Hour)
|
||||
c.Assert(tr.MonthsWide()[date.Month()-1], qt.Equals, monthWideNorway)
|
||||
c.Assert(tr.MonthWide(date.Month()), qt.Equals, monthWideNorway)
|
||||
c.Assert(f.Format(date, "January"), qt.Equals, monthWideNorway)
|
||||
}
|
||||
})
|
||||
@@ -113,7 +112,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
|
||||
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
|
||||
|
||||
b.Run("Native", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := june06.Format("Monday Jan 2 2006")
|
||||
if got != "Wednesday Jun 6 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
@@ -122,9 +121,9 @@ func BenchmarkTimeFormatter(b *testing.B) {
|
||||
})
|
||||
|
||||
b.Run("Localized", func(b *testing.B) {
|
||||
f := NewTimeFormatter(golocales.New("nn"))
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := f.Format(june06, "Monday Jan 2 2006")
|
||||
if got != "onsdag juni 6 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
@@ -133,9 +132,9 @@ func BenchmarkTimeFormatter(b *testing.B) {
|
||||
})
|
||||
|
||||
b.Run("Localized Custom", func(b *testing.B) {
|
||||
f := NewTimeFormatter(golocales.New("nn"))
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := f.Format(june06, ":date_medium")
|
||||
if got != "6. juni 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
|
||||
@@ -46,18 +46,18 @@ func TestHasBytesWriter(t *testing.T) {
|
||||
return strings.Repeat("ab cfo", r.Intn(33))
|
||||
}
|
||||
|
||||
for range 22 {
|
||||
for i := 0; i < 22; i++ {
|
||||
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)
|
||||
|
||||
h, w = neww()
|
||||
fmt.Fprint(w, rndStr()+"abc __f")
|
||||
fmt.Fprint(w, "oo bar"+rndStr())
|
||||
fmt.Fprintf(w, rndStr()+"abc __f")
|
||||
fmt.Fprintf(w, "oo bar"+rndStr())
|
||||
c.Assert(h.Patterns[0].Match, qt.Equals, true)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
+11
-52
@@ -19,44 +19,19 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReadSeeker wraps io.Reader and io.Seeker.
|
||||
type ReadSeeker interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
}
|
||||
|
||||
// ReadSeekCloser is implemented by afero.File. We use this as the common type for
|
||||
// content in Resource objects, even for strings.
|
||||
type ReadSeekCloser interface {
|
||||
io.ReadSeeker
|
||||
ReadSeeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Sizer provides the size of, typically, a io.Reader.
|
||||
// As implemented by e.g. os.File and io.SectionReader.
|
||||
type Sizer interface {
|
||||
Size() int64
|
||||
}
|
||||
|
||||
type SizeReader interface {
|
||||
io.Reader
|
||||
Sizer
|
||||
}
|
||||
|
||||
// ToSizeReader converts the given io.Reader to a SizeReader.
|
||||
// Note that if r is not a SizeReader, the entire content will be read into memory
|
||||
func ToSizeReader(r io.Reader) (SizeReader, error) {
|
||||
if sr, ok := r.(SizeReader); ok {
|
||||
return sr, nil
|
||||
}
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.NewReader(b), nil
|
||||
}
|
||||
|
||||
// CloserFunc is an adapter to allow the use of ordinary functions as io.Closers.
|
||||
type CloserFunc func() error
|
||||
|
||||
func (f CloserFunc) Close() error {
|
||||
return f()
|
||||
}
|
||||
|
||||
// ReadSeekCloserProvider provides a ReadSeekCloser.
|
||||
type ReadSeekCloserProvider interface {
|
||||
ReadSeekCloser() (ReadSeekCloser, error)
|
||||
@@ -64,7 +39,7 @@ type ReadSeekCloserProvider interface {
|
||||
|
||||
// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close.
|
||||
type readSeekerNopCloser struct {
|
||||
io.ReadSeeker
|
||||
ReadSeeker
|
||||
}
|
||||
|
||||
// Close does nothing.
|
||||
@@ -73,7 +48,7 @@ func (r readSeekerNopCloser) Close() error {
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
|
||||
func NewReadSeekerNoOpCloser(r io.ReadSeeker) ReadSeekCloser {
|
||||
func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekCloser {
|
||||
return readSeekerNopCloser{r}
|
||||
}
|
||||
|
||||
@@ -99,29 +74,13 @@ type StringReader interface {
|
||||
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.
|
||||
func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser {
|
||||
return readSeekerNopCloser{bytes.NewReader(content)}
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloserFromReader creates a new ReadSeekerNoOpCloser from the given io.Reader.
|
||||
// If the given io.Reader is not an io.ReadSeeker, the entire content will be read into memory.
|
||||
func NewReadSeekerNoOpCloserFromReader(r io.Reader) (readSeekerNopCloser, error) {
|
||||
var rs io.ReadSeeker
|
||||
if s, ok := r.(io.ReadSeeker); ok {
|
||||
rs = s
|
||||
} else {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return readSeekerNopCloser{rs}, err
|
||||
}
|
||||
rs = bytes.NewReader(b)
|
||||
}
|
||||
return readSeekerNopCloser{rs}, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser {
|
||||
return func() (ReadSeekCloser, error) {
|
||||
|
||||
+140
-110
@@ -16,6 +16,7 @@ package hugo
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
@@ -24,14 +25,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bep/helpers/contexthelpers"
|
||||
godartsassv1 "github.com/bep/godartsass"
|
||||
"github.com/bep/logg"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hcontext"
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/version"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -54,35 +55,87 @@ var (
|
||||
vendorInfo string
|
||||
)
|
||||
|
||||
// BuildInfo holds build information extracted from runtime/debug.
|
||||
type BuildInfo struct {
|
||||
Revision string
|
||||
RevisionTime string
|
||||
GoVersion string
|
||||
// HugoInfo contains information about the current Hugo environment
|
||||
type HugoInfo struct {
|
||||
CommitHash string
|
||||
BuildDate string
|
||||
|
||||
// The build environment.
|
||||
// Defaults are "production" (hugo) and "development" (hugo server).
|
||||
// This can also be set by the user.
|
||||
// It can be any string, but it will be all lower case.
|
||||
Environment string
|
||||
|
||||
// version of go that the Hugo binary was built with
|
||||
GoVersion string
|
||||
|
||||
conf ConfigProvider
|
||||
deps []*Dependency
|
||||
|
||||
// Context gives access to some of the context scoped variables.
|
||||
Context Context
|
||||
}
|
||||
|
||||
// GetBuildInfo returns the build info for the current binary.
|
||||
func GetBuildInfo() *BuildInfo {
|
||||
bi := getBuildInfo()
|
||||
if bi == nil {
|
||||
return nil
|
||||
}
|
||||
return &BuildInfo{
|
||||
Revision: bi.Revision,
|
||||
RevisionTime: bi.RevisionTime,
|
||||
GoVersion: bi.GoVersion,
|
||||
}
|
||||
// Version returns the current version as a comparable version string.
|
||||
func (i HugoInfo) Version() VersionString {
|
||||
return CurrentVersion.Version()
|
||||
}
|
||||
|
||||
type contextKey uint8
|
||||
// Generator a Hugo meta generator HTML tag.
|
||||
func (i HugoInfo) Generator() template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s">`, CurrentVersion.String()))
|
||||
}
|
||||
|
||||
const (
|
||||
contextKeyMarkupScope contextKey = iota
|
||||
)
|
||||
// IsDevelopment reports whether the current running environment is "development".
|
||||
func (i HugoInfo) IsDevelopment() bool {
|
||||
return i.Environment == EnvironmentDevelopment
|
||||
}
|
||||
|
||||
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
|
||||
// IsProduction reports whether the current running environment is "production".
|
||||
func (i HugoInfo) IsProduction() bool {
|
||||
return i.Environment == EnvironmentProduction
|
||||
}
|
||||
|
||||
// IsServer reports whether the built-in server is running.
|
||||
func (i HugoInfo) IsServer() bool {
|
||||
return i.conf.Running()
|
||||
}
|
||||
|
||||
// IsExtended reports whether the Hugo binary is the extended version.
|
||||
func (i HugoInfo) IsExtended() bool {
|
||||
return IsExtended
|
||||
}
|
||||
|
||||
// WorkingDir returns the project working directory.
|
||||
func (i HugoInfo) WorkingDir() string {
|
||||
return i.conf.WorkingDir()
|
||||
}
|
||||
|
||||
// Deps gets a list of dependencies for this Hugo build.
|
||||
func (i HugoInfo) Deps() []*Dependency {
|
||||
return i.deps
|
||||
}
|
||||
|
||||
// Deprecated: Use hugo.IsMultihost instead.
|
||||
func (i HugoInfo) IsMultiHost() bool {
|
||||
Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0")
|
||||
return i.conf.IsMultihost()
|
||||
}
|
||||
|
||||
// IsMultihost reports whether each configured language has a unique baseURL.
|
||||
func (i HugoInfo) IsMultihost() bool {
|
||||
return i.conf.IsMultihost()
|
||||
}
|
||||
|
||||
// IsMultilingual reports whether there are two or more configured languages.
|
||||
func (i HugoInfo) IsMultilingual() bool {
|
||||
return i.conf.IsMultilingual()
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
|
||||
var markupScope = hcontext.NewContextDispatcher[string](contextKey("markupScope"))
|
||||
|
||||
// Context gives access to some of the context scoped variables.
|
||||
type Context struct{}
|
||||
|
||||
func (c Context) MarkupScope(ctx context.Context) string {
|
||||
@@ -99,6 +152,43 @@ func GetMarkupScope(ctx context.Context) string {
|
||||
return markupScope.Get(ctx)
|
||||
}
|
||||
|
||||
// ConfigProvider represents the config options that are relevant for HugoInfo.
|
||||
type ConfigProvider interface {
|
||||
Environment() string
|
||||
Running() bool
|
||||
WorkingDir() string
|
||||
IsMultihost() bool
|
||||
IsMultilingual() bool
|
||||
}
|
||||
|
||||
// NewInfo creates a new Hugo Info object.
|
||||
func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
|
||||
if conf.Environment() == "" {
|
||||
panic("environment not set")
|
||||
}
|
||||
var (
|
||||
commitHash string
|
||||
buildDate string
|
||||
goVersion string
|
||||
)
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi != nil {
|
||||
commitHash = bi.Revision
|
||||
buildDate = bi.RevisionTime
|
||||
goVersion = bi.GoVersion
|
||||
}
|
||||
|
||||
return HugoInfo{
|
||||
CommitHash: commitHash,
|
||||
BuildDate: buildDate,
|
||||
Environment: conf.Environment(),
|
||||
conf: conf,
|
||||
deps: deps,
|
||||
GoVersion: goVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// GetExecEnviron creates and gets the common os/exec environment used in the
|
||||
// external programs we interact with via os/exec, e.g. postcss.
|
||||
func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []string {
|
||||
@@ -206,18 +296,19 @@ func GetDependencyList() []string {
|
||||
|
||||
// GetDependencyListNonGo returns a list of non-Go dependencies.
|
||||
func GetDependencyListNonGo() []string {
|
||||
deps := []string{formatDep("github.com/webmproject/libwebp", "v1.6.0")} // via WASM. TODO(bep) get versions from the plugin setup.
|
||||
var deps []string
|
||||
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
deps,
|
||||
formatDep("github.com/sass/libsass", "3.6.6"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.3.2"),
|
||||
)
|
||||
}
|
||||
|
||||
if dartSass := dartSassVersion(); dartSass.ProtocolVersion != "" {
|
||||
dartSassPath := "github.com/sass/dart-sass-embedded"
|
||||
if IsDartSassGeV2() {
|
||||
if IsDartSassV2() {
|
||||
dartSassPath = "github.com/sass/dart-sass"
|
||||
}
|
||||
deps = append(deps,
|
||||
@@ -264,15 +355,22 @@ type Dependency struct {
|
||||
}
|
||||
|
||||
func dartSassVersion() godartsass.DartSassVersion {
|
||||
if DartSassBinaryName == "" || !IsDartSassGeV2() {
|
||||
if DartSassBinaryName == "" {
|
||||
return godartsass.DartSassVersion{}
|
||||
}
|
||||
v, _ := godartsass.Version(DartSassBinaryName)
|
||||
return v
|
||||
if IsDartSassV2() {
|
||||
v, _ := godartsass.Version(DartSassBinaryName)
|
||||
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.
|
||||
// TODO(bep) find a better place for this.
|
||||
// TODO(beop) find a better place for this.
|
||||
var DartSassBinaryName string
|
||||
|
||||
func init() {
|
||||
@@ -297,10 +395,7 @@ var (
|
||||
dartSassBinaryNamesV2 = []string{"dart-sass", "sass"}
|
||||
)
|
||||
|
||||
// TODO(bep) we eventually want to remove this, but keep it for a while to throw an informative error.
|
||||
// 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.
|
||||
func IsDartSassV2() bool {
|
||||
return !strings.Contains(DartSassBinaryName, "embedded")
|
||||
}
|
||||
|
||||
@@ -312,101 +407,36 @@ func IsDartSassGeV2() bool {
|
||||
// 2. Their theme to work for at least the last few Hugo versions.
|
||||
func Deprecate(item, alternative string, version string) {
|
||||
level := deprecationLogLevelFromVersion(version)
|
||||
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(item, alternative, version, level)
|
||||
}
|
||||
|
||||
// DeprecateLevel informs about a deprecation logging at the given level.
|
||||
func deprecateLevelWithLogger(item, alternative, version string, level logg.Level, log logg.Logger) {
|
||||
//if strings.Contains(item, "module.mounts.lang") || strings.Contains(item, "includeFiles") {
|
||||
// hdebug.Panicf("Deprecated")
|
||||
//}
|
||||
func DeprecateLevel(item, alternative, version string, level logg.Level) {
|
||||
var msg string
|
||||
if level == logg.LevelError {
|
||||
// Useful to debug deprecation errors that needs to be removedor fixed. Comment out when done debugging.
|
||||
// hdebug.Panicf("deprecation error: %s was removed in Hugo %s. %s", item, version, alternative)
|
||||
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 {
|
||||
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 who don't update Hugo that often to see the warnings and errors before we remove the feature.
|
||||
func deprecationLogLevelFromVersion(ver string) logg.Level {
|
||||
from := version.MustParseVersion(ver)
|
||||
from := MustParseVersion(ver)
|
||||
to := CurrentVersion
|
||||
minorDiff := to.Minor - from.Minor
|
||||
switch {
|
||||
case minorDiff >= 15:
|
||||
// Start failing the build after about 15 months.
|
||||
case minorDiff >= 12:
|
||||
// Start failing the build after about a year.
|
||||
return logg.LevelError
|
||||
case minorDiff >= 3:
|
||||
// Start printing warnings after about 3 months.
|
||||
case minorDiff >= 6:
|
||||
// Start printing warnings after about six months.
|
||||
return logg.LevelWarn
|
||||
default:
|
||||
return logg.LevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
// BuildVersionString creates a version string. This is what you see when
|
||||
// running "hugo version".
|
||||
func BuildVersionString() string {
|
||||
// program := "Hugo Static Site Generator"
|
||||
program := "hugo"
|
||||
|
||||
version := "v" + CurrentVersion.String()
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi == nil {
|
||||
return version
|
||||
}
|
||||
if bi.Revision != "" {
|
||||
version += "-" + bi.Revision
|
||||
}
|
||||
if IsExtended {
|
||||
version += "+extended"
|
||||
}
|
||||
if IsWithdeploy {
|
||||
version += "+withdeploy"
|
||||
}
|
||||
|
||||
osArch := bi.GoOS + "/" + bi.GoArch
|
||||
|
||||
date := bi.RevisionTime
|
||||
if date == "" {
|
||||
// Accept vendor-specified build date if .git/ is unavailable.
|
||||
date = buildDate
|
||||
}
|
||||
if date == "" {
|
||||
date = "unknown"
|
||||
}
|
||||
|
||||
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
|
||||
program, version, osArch, date)
|
||||
|
||||
if vendorInfo != "" {
|
||||
versionString += " VendorInfo=" + vendorInfo
|
||||
}
|
||||
|
||||
return versionString
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ baseURL = 'https://en.example.org/'
|
||||
---
|
||||
title: home
|
||||
---
|
||||
-- layouts/home.html --
|
||||
-- layouts/index.html --
|
||||
multilingual={{ hugo.IsMultilingual }}
|
||||
multihost={{ hugo.IsMultihost }}
|
||||
`
|
||||
@@ -75,39 +75,3 @@ multihost={{ hugo.IsMultihost }}
|
||||
"multihost=false",
|
||||
)
|
||||
}
|
||||
|
||||
func TestHugoSites(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
defaultContentLanguage = 'fr'
|
||||
defaultContentLanguageInSubdir = true
|
||||
defaultContentVersionInSubdir = true
|
||||
defaultContentRoleInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
weight = 2
|
||||
[languages.de]
|
||||
weight = 3
|
||||
[roles]
|
||||
[roles.guest]
|
||||
weight = 1
|
||||
[roles.member]
|
||||
weight = 2
|
||||
[versions]
|
||||
[versions.'v1.0.0']
|
||||
weight = 1
|
||||
[versions.'v2.0.0']
|
||||
weight = 2
|
||||
-- layouts/home.html --
|
||||
{{ range hugo.Sites }}{{ .Language.Name }}-{{ .Role.Name }}-{{ .Version.Name }}|{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/guest/v1.0.0/en/index.html", "en-guest-v1.0.0|en-member-v1.0.0|en-guest-v2.0.0|en-member-v2.0.0|fr-guest-v1.0.0|fr-member-v1.0.0|fr-guest-v2.0.0|fr-member-v2.0.0|de-guest-v1.0.0|de-member-v1.0.0|de-guest-v2.0.0|de-member-v2.0.0|")
|
||||
}
|
||||
|
||||
+65
-19
@@ -15,47 +15,93 @@ package hugo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestHugoInfo(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
|
||||
hugoInfo := NewInfo(conf, nil)
|
||||
|
||||
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
|
||||
c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi != nil {
|
||||
c.Assert(hugoInfo.CommitHash, qt.Equals, bi.Revision)
|
||||
c.Assert(hugoInfo.BuildDate, qt.Equals, bi.RevisionTime)
|
||||
c.Assert(hugoInfo.GoVersion, qt.Equals, bi.GoVersion)
|
||||
}
|
||||
c.Assert(hugoInfo.Environment, qt.Equals, "production")
|
||||
c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.IsDevelopment(), qt.Equals, false)
|
||||
c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
|
||||
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
|
||||
c.Assert(hugoInfo.IsServer(), qt.Equals, false)
|
||||
|
||||
devHugoInfo := NewInfo(testConfig{environment: "development", running: true}, nil)
|
||||
c.Assert(devHugoInfo.IsDevelopment(), qt.Equals, true)
|
||||
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
|
||||
c.Assert(devHugoInfo.IsServer(), qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestDeprecationLogLevelFromVersion(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError)
|
||||
ver := CurrentVersion
|
||||
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)
|
||||
ver.Minor -= 4
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
|
||||
ver.Minor -= 13
|
||||
ver.Minor -= 6
|
||||
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.136.0.
|
||||
c.Assert(deprecationLogLevelFromVersion("0.141.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")
|
||||
|
||||
var hugoCtx Context
|
||||
c.Assert(hugoCtx.MarkupScope(ctx), qt.Equals, "foo")
|
||||
c.Assert(GetMarkupScope(ctx), qt.Equals, "foo")
|
||||
c.Assert(info.Context.MarkupScope(ctx), qt.Equals, "foo")
|
||||
}
|
||||
|
||||
func TestGetBuildInfo(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
bi := GetBuildInfo()
|
||||
// In test mode, build info may or may not be available.
|
||||
if bi != nil {
|
||||
c.Assert(bi.GoVersion, qt.Not(qt.Equals), "")
|
||||
}
|
||||
type testConfig struct {
|
||||
environment string
|
||||
running bool
|
||||
workingDir string
|
||||
multihost bool
|
||||
multilingual bool
|
||||
}
|
||||
|
||||
func (c testConfig) Environment() string {
|
||||
return c.environment
|
||||
}
|
||||
|
||||
func (c testConfig) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c testConfig) WorkingDir() string {
|
||||
return c.workingDir
|
||||
}
|
||||
|
||||
func (c testConfig) IsMultihost() bool {
|
||||
return c.multihost
|
||||
}
|
||||
|
||||
func (c testConfig) IsMultilingual() bool {
|
||||
return c.multilingual
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build extended
|
||||
// +build extended
|
||||
|
||||
package hugo
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !extended
|
||||
// +build !extended
|
||||
|
||||
package hugo
|
||||
|
||||
|
||||
@@ -1,18 +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.
|
||||
|
||||
//go:build !withdeploy
|
||||
|
||||
package hugo
|
||||
|
||||
var IsWithdeploy = false
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 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.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package version
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -37,9 +37,6 @@ type Version struct {
|
||||
// HugoVersionSuffix is the suffix used in the Hugo version string.
|
||||
// It will be blank for release versions.
|
||||
Suffix string
|
||||
|
||||
// Set when parsed from a string,
|
||||
source string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -47,17 +44,6 @@ var (
|
||||
_ compare.Comparer = (*VersionString)(nil)
|
||||
)
|
||||
|
||||
// IsAlphaBetaOrRC returns whether this version is an alpha, beta, or release candidate.
|
||||
func (v Version) IsAlphaBetaOrRC() bool {
|
||||
s := strings.ToLower(v.Suffix)
|
||||
// e.g. "alpha.1", "beta.2", "rc.3"
|
||||
return strings.Contains(s, "alpha.") || strings.Contains(s, "beta.") || strings.Contains(s, "rc.")
|
||||
}
|
||||
|
||||
func (v Version) IsZero() bool {
|
||||
return v.Major == 0 && v.Minor == 0 && v.PatchLevel == 0 && v.Suffix == ""
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return version(v.Major, v.Minor, v.PatchLevel, v.Suffix)
|
||||
}
|
||||
@@ -69,7 +55,7 @@ func (v Version) Version() VersionString {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h Version) Compare(other any) int {
|
||||
return CompareVersions(h, other)
|
||||
return compareVersions(h, other)
|
||||
}
|
||||
|
||||
// VersionString represents a Hugo version string.
|
||||
@@ -81,7 +67,7 @@ func (h VersionString) String() string {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other any) int {
|
||||
return CompareVersions(h.Version(), other)
|
||||
return compareVersions(h.Version(), other)
|
||||
}
|
||||
|
||||
func (h VersionString) Version() Version {
|
||||
@@ -97,26 +83,19 @@ func (h VersionString) Eq(other any) bool {
|
||||
return s == h.String()
|
||||
}
|
||||
|
||||
var versionSuffixes = []string{"-test", "-DEV"}
|
||||
|
||||
// ParseVersion parses a version string.
|
||||
func ParseVersion(s string) (Version, error) {
|
||||
s = strings.TrimPrefix(strings.TrimSpace(s), "v")
|
||||
var vv Version
|
||||
hyphen := strings.Index(s, "-")
|
||||
if hyphen > 0 {
|
||||
suffix := s[hyphen:]
|
||||
if len(suffix) > 1 {
|
||||
if suffix[0] == '-' {
|
||||
suffix = suffix[1:]
|
||||
}
|
||||
if len(suffix) > 0 {
|
||||
vv.Suffix = suffix
|
||||
s = s[:hyphen]
|
||||
}
|
||||
for _, suffix := range versionSuffixes {
|
||||
if strings.HasSuffix(s, suffix) {
|
||||
vv.Suffix = suffix
|
||||
s = strings.TrimSuffix(s, suffix)
|
||||
}
|
||||
vv.Suffix = suffix
|
||||
}
|
||||
|
||||
vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s)
|
||||
vv.source = s
|
||||
|
||||
return vv, nil
|
||||
}
|
||||
@@ -155,70 +134,104 @@ func (v Version) NextPatchLevel(level int) Version {
|
||||
return prev
|
||||
}
|
||||
|
||||
func version(major, minor, patch int, suffix string) string {
|
||||
if suffix != "" {
|
||||
if suffix[0] != '-' {
|
||||
suffix = "-" + suffix
|
||||
}
|
||||
// BuildVersionString creates a version string. This is what you see when
|
||||
// running "hugo version".
|
||||
func BuildVersionString() string {
|
||||
// program := "Hugo Static Site Generator"
|
||||
program := "hugo"
|
||||
|
||||
version := "v" + CurrentVersion.String()
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi == nil {
|
||||
return version
|
||||
}
|
||||
if bi.Revision != "" {
|
||||
version += "-" + bi.Revision
|
||||
}
|
||||
if IsExtended {
|
||||
version += "+extended"
|
||||
}
|
||||
|
||||
osArch := bi.GoOS + "/" + bi.GoArch
|
||||
|
||||
date := bi.RevisionTime
|
||||
if date == "" {
|
||||
// Accept vendor-specified build date if .git/ is unavailable.
|
||||
date = buildDate
|
||||
}
|
||||
if date == "" {
|
||||
date = "unknown"
|
||||
}
|
||||
|
||||
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
|
||||
program, version, osArch, date)
|
||||
|
||||
if vendorInfo != "" {
|
||||
versionString += " VendorInfo=" + vendorInfo
|
||||
}
|
||||
|
||||
return versionString
|
||||
}
|
||||
|
||||
func version(major, minor, patch int, suffix string) string {
|
||||
if patch > 0 || minor > 53 {
|
||||
return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, suffix)
|
||||
}
|
||||
return fmt.Sprintf("%d.%d%s", major, minor, suffix)
|
||||
}
|
||||
|
||||
// CompareVersion compares v1 with v2.
|
||||
// It returns -1 if the v2 is less than, 0 if equal and 1 if greater than
|
||||
// v1.
|
||||
func CompareVersions(v1 Version, v2 any) int {
|
||||
// CompareVersion compares the given version string or number against the
|
||||
// running Hugo version.
|
||||
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
|
||||
// the running version.
|
||||
func CompareVersion(version any) int {
|
||||
return compareVersions(CurrentVersion, version)
|
||||
}
|
||||
|
||||
func compareVersions(inVersion Version, in any) int {
|
||||
var c int
|
||||
switch d := v2.(type) {
|
||||
switch d := in.(type) {
|
||||
case float64:
|
||||
c = compareFloatWithVersion(d, v1)
|
||||
c = compareFloatWithVersion(d, inVersion)
|
||||
case float32:
|
||||
c = compareFloatWithVersion(float64(d), v1)
|
||||
c = compareFloatWithVersion(float64(d), inVersion)
|
||||
case int:
|
||||
c = compareFloatWithVersion(float64(d), v1)
|
||||
c = compareFloatWithVersion(float64(d), inVersion)
|
||||
case int32:
|
||||
c = compareFloatWithVersion(float64(d), v1)
|
||||
c = compareFloatWithVersion(float64(d), inVersion)
|
||||
case int64:
|
||||
c = compareFloatWithVersion(float64(d), v1)
|
||||
c = compareFloatWithVersion(float64(d), inVersion)
|
||||
case Version:
|
||||
if v1.IsZero() && d.IsZero() {
|
||||
// Fall back to source comparison.
|
||||
return strings.Compare(v1.source, d.source)
|
||||
if d.Major == inVersion.Major && d.Minor == inVersion.Minor && d.PatchLevel == inVersion.PatchLevel {
|
||||
return strings.Compare(inVersion.Suffix, d.Suffix)
|
||||
}
|
||||
if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel {
|
||||
return strings.Compare(v1.Suffix, d.Suffix)
|
||||
}
|
||||
if d.Major > v1.Major {
|
||||
if d.Major > inVersion.Major {
|
||||
return 1
|
||||
} else if d.Major < v1.Major {
|
||||
} else if d.Major < inVersion.Major {
|
||||
return -1
|
||||
}
|
||||
if d.Minor > v1.Minor {
|
||||
if d.Minor > inVersion.Minor {
|
||||
return 1
|
||||
} else if d.Minor < v1.Minor {
|
||||
} else if d.Minor < inVersion.Minor {
|
||||
return -1
|
||||
}
|
||||
if d.PatchLevel > v1.PatchLevel {
|
||||
if d.PatchLevel > inVersion.PatchLevel {
|
||||
return 1
|
||||
} else if d.PatchLevel < v1.PatchLevel {
|
||||
} else if d.PatchLevel < inVersion.PatchLevel {
|
||||
return -1
|
||||
}
|
||||
default:
|
||||
s, err := cast.ToStringE(v2)
|
||||
s, err := cast.ToStringE(in)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
v2v, _ := ParseVersion(s)
|
||||
if v1.IsZero() && v2v.IsZero() {
|
||||
// Fall back to source comparison.
|
||||
return strings.Compare(v1.source, v2v.source)
|
||||
v, err := ParseVersion(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return v1.Compare(v2v)
|
||||
return inVersion.Compare(v)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 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.
|
||||
@@ -13,13 +13,11 @@
|
||||
|
||||
package hugo
|
||||
|
||||
import "github.com/gohugoio/hugo/common/version"
|
||||
|
||||
// CurrentVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentVersion = version.Version{
|
||||
var CurrentVersion = Version{
|
||||
Major: 0,
|
||||
Minor: 159,
|
||||
PatchLevel: 2,
|
||||
Suffix: "",
|
||||
Minor: 137,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2015 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.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package version
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -50,45 +50,30 @@ func TestHugoVersion(t *testing.T) {
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
parseIgnoreErr := func(s string) Version {
|
||||
v, _ := ParseVersion(s)
|
||||
return v
|
||||
}
|
||||
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
|
||||
|
||||
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "v1.0.0"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("v1.0.0"), "foobar"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "foobar"), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("foobar"), parseIgnoreErr("foobar")), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("a"), "b"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("a"), parseIgnoreErr("b")), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("b"), "a"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("b"), parseIgnoreErr("a")), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
|
||||
}
|
||||
|
||||
func TestParseHugoVersion(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(MustParseVersion("v2.3.2").String(), qt.Equals, "2.3.2")
|
||||
c.Assert(MustParseVersion("0.25").String(), qt.Equals, "0.25")
|
||||
c.Assert(MustParseVersion("0.25.2").String(), qt.Equals, "0.25.2")
|
||||
c.Assert(MustParseVersion("0.25-test").String(), qt.Equals, "0.25-test")
|
||||
@@ -18,19 +18,18 @@ package loggers
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
)
|
||||
|
||||
// newNoAnsiEscapeHandler creates a new noAnsiEscapeHandler
|
||||
func newNoAnsiEscapeHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noAnsiEscapeHandler {
|
||||
// newNoColoursHandler creates a new NoColoursHandler
|
||||
func newNoColoursHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noColoursHandler {
|
||||
if predicate == nil {
|
||||
predicate = func(e *logg.Entry) bool { return true }
|
||||
}
|
||||
return &noAnsiEscapeHandler{
|
||||
return &noColoursHandler{
|
||||
noLevelPrefix: noLevelPrefix,
|
||||
outWriter: outWriter,
|
||||
errWriter: errWriter,
|
||||
@@ -38,15 +37,15 @@ func newNoAnsiEscapeHandler(outWriter, errWriter io.Writer, noLevelPrefix bool,
|
||||
}
|
||||
}
|
||||
|
||||
type noAnsiEscapeHandler struct {
|
||||
type noColoursHandler struct {
|
||||
mu sync.Mutex
|
||||
outWriter io.Writer
|
||||
errWriter io.Writer
|
||||
outWriter io.Writer // Defaults to os.Stdout.
|
||||
errWriter io.Writer // Defaults to os.Stderr.
|
||||
predicate func(*logg.Entry) bool
|
||||
noLevelPrefix bool
|
||||
}
|
||||
|
||||
func (h *noAnsiEscapeHandler) HandleLog(e *logg.Entry) error {
|
||||
func (h *noColoursHandler) HandleLog(e *logg.Entry) error {
|
||||
if !h.predicate(e) {
|
||||
return nil
|
||||
}
|
||||
@@ -72,12 +71,10 @@ func (h *noAnsiEscapeHandler) HandleLog(e *logg.Entry) error {
|
||||
prefix = prefix + ": "
|
||||
}
|
||||
|
||||
msg := stripANSI(e.Message)
|
||||
|
||||
if h.noLevelPrefix {
|
||||
fmt.Fprintf(w, "%s%s", prefix, msg)
|
||||
fmt.Fprintf(w, "%s%s", prefix, e.Message)
|
||||
} 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 {
|
||||
@@ -91,10 +88,3 @@ func (h *noAnsiEscapeHandler) HandleLog(e *logg.Entry) error {
|
||||
|
||||
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, "")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
+23
-40
@@ -20,7 +20,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
@@ -39,8 +38,8 @@ var (
|
||||
// Options defines options for the logger.
|
||||
type Options struct {
|
||||
Level logg.Level
|
||||
StdOut io.Writer
|
||||
StdErr io.Writer
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
DistinctLevel logg.Level
|
||||
StoreErrors bool
|
||||
HandlerPost func(e *logg.Entry) error
|
||||
@@ -49,22 +48,21 @@ type Options struct {
|
||||
|
||||
// New creates a new logger with the given options.
|
||||
func New(opts Options) Logger {
|
||||
if opts.StdOut == nil {
|
||||
opts.StdOut = os.Stdout
|
||||
if opts.Stdout == nil {
|
||||
opts.Stdout = os.Stdout
|
||||
}
|
||||
if opts.StdErr == nil {
|
||||
opts.StdErr = os.Stderr
|
||||
if opts.Stderr == nil {
|
||||
opts.Stderr = os.Stdout
|
||||
}
|
||||
|
||||
if opts.Level == 0 {
|
||||
opts.Level = logg.LevelWarn
|
||||
}
|
||||
|
||||
var logHandler logg.Handler
|
||||
if terminal.PrintANSIColors(os.Stderr) {
|
||||
logHandler = newDefaultHandler(opts.StdErr, opts.StdErr)
|
||||
if terminal.PrintANSIColors(os.Stdout) {
|
||||
logHandler = newDefaultHandler(opts.Stdout, opts.Stderr)
|
||||
} else {
|
||||
logHandler = newNoAnsiEscapeHandler(opts.StdErr, opts.StdErr, false, nil)
|
||||
logHandler = newNoColoursHandler(opts.Stdout, opts.Stderr, false, nil)
|
||||
}
|
||||
|
||||
errorsw := &strings.Builder{}
|
||||
@@ -97,7 +95,7 @@ func New(opts Options) Logger {
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
@@ -124,11 +122,8 @@ func New(opts Options) Logger {
|
||||
)
|
||||
|
||||
l := logger.WithLevel(opts.Level)
|
||||
logMu := &sync.Mutex{}
|
||||
|
||||
reset := func() {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
logCounters.mu.Lock()
|
||||
defer logCounters.mu.Unlock()
|
||||
logCounters.counters = make(map[logg.Level]int)
|
||||
@@ -139,12 +134,10 @@ func New(opts Options) Logger {
|
||||
}
|
||||
|
||||
return &logAdapter{
|
||||
mu: logMu,
|
||||
logCounters: logCounters,
|
||||
errors: errorsw,
|
||||
reset: reset,
|
||||
stdOut: opts.StdOut,
|
||||
stdErr: opts.StdErr,
|
||||
out: opts.Stdout,
|
||||
level: opts.Level,
|
||||
logger: logger,
|
||||
tracel: l.WithLevel(logg.LevelTrace),
|
||||
@@ -160,6 +153,8 @@ func NewDefault() Logger {
|
||||
opts := Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: logg.LevelWarn,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
}
|
||||
return New(opts)
|
||||
}
|
||||
@@ -168,6 +163,8 @@ func NewTrace() Logger {
|
||||
opts := Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: logg.LevelTrace,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
}
|
||||
return New(opts)
|
||||
}
|
||||
@@ -178,7 +175,6 @@ func LevelLoggerToWriter(l logg.LevelLogger) io.Writer {
|
||||
|
||||
type Logger interface {
|
||||
Debug() logg.LevelLogger
|
||||
DebugCommand(command string) logg.LevelLogger
|
||||
Debugf(format string, v ...any)
|
||||
Debugln(v ...any)
|
||||
Error() logg.LevelLogger
|
||||
@@ -193,8 +189,7 @@ type Logger interface {
|
||||
Level() logg.Level
|
||||
LoggCount(logg.Level) int
|
||||
Logger() logg.Logger
|
||||
StdOut() io.Writer
|
||||
StdErr() io.Writer
|
||||
Out() io.Writer
|
||||
Printf(format string, v ...any)
|
||||
Println(v ...any)
|
||||
PrintTimerIfDelayed(start time.Time, name string)
|
||||
@@ -209,12 +204,10 @@ type Logger interface {
|
||||
}
|
||||
|
||||
type logAdapter struct {
|
||||
mu *sync.Mutex
|
||||
logCounters *logLevelCounter
|
||||
errors *strings.Builder
|
||||
reset func()
|
||||
stdOut io.Writer
|
||||
stdErr io.Writer
|
||||
out io.Writer
|
||||
level logg.Level
|
||||
logger logg.Logger
|
||||
tracel logg.LevelLogger
|
||||
@@ -228,10 +221,6 @@ func (l *logAdapter) Debug() logg.LevelLogger {
|
||||
return l.debugl
|
||||
}
|
||||
|
||||
func (l *logAdapter) DebugCommand(command string) logg.LevelLogger {
|
||||
return l.debugl.WithField(FieldNameCmd, command)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debugf(format string, v ...any) {
|
||||
l.debugl.Logf(format, v...)
|
||||
}
|
||||
@@ -270,12 +259,8 @@ func (l *logAdapter) Logger() logg.Logger {
|
||||
return l.logger
|
||||
}
|
||||
|
||||
func (l *logAdapter) StdOut() io.Writer {
|
||||
return l.stdOut
|
||||
}
|
||||
|
||||
func (l *logAdapter) StdErr() io.Writer {
|
||||
return l.stdErr
|
||||
func (l *logAdapter) Out() io.Writer {
|
||||
return l.out
|
||||
}
|
||||
|
||||
// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
|
||||
@@ -286,7 +271,7 @@ func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
if milli < 500 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(l.stdErr, "%s in %v ms\n", name, milli)
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Printf(format string, v ...any) {
|
||||
@@ -294,11 +279,11 @@ func (l *logAdapter) Printf(format string, v ...any) {
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format += "\n"
|
||||
}
|
||||
fmt.Fprintf(l.stdOut, format, v...)
|
||||
fmt.Fprintf(l.out, format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Println(v ...any) {
|
||||
fmt.Fprintln(l.stdOut, v...)
|
||||
fmt.Fprintln(l.out, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Reset() {
|
||||
@@ -334,8 +319,6 @@ func (l *logAdapter) Errorln(v ...any) {
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errors() string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.errors.String()
|
||||
}
|
||||
|
||||
@@ -352,7 +335,7 @@ func (l *logAdapter) Warnidf(id, format string, v ...any) {
|
||||
}
|
||||
|
||||
func (l *logAdapter) idfInfoStatement(what, id, format string) string {
|
||||
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your project configuration:\nignoreLogs = ['%s']", what, id)
|
||||
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your site configuration:\nignoreLogs = ['%s']", what, id)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Trace(s logg.StringFunc) {
|
||||
|
||||
@@ -31,13 +31,13 @@ func TestLogDistinct(t *testing.T) {
|
||||
opts := loggers.Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
StoreErrors: true,
|
||||
StdOut: io.Discard,
|
||||
StdErr: io.Discard,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
for range 10 {
|
||||
for i := 0; i < 10; i++ {
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Warnln("warn 1")
|
||||
@@ -54,8 +54,8 @@ func TestHookLast(t *testing.T) {
|
||||
HandlerPost: func(e *logg.Entry) error {
|
||||
panic(e.Message)
|
||||
},
|
||||
StdOut: io.Discard,
|
||||
StdErr: io.Discard,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
@@ -70,8 +70,8 @@ func TestOptionStoreErrors(t *testing.T) {
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
StdErr: &sb,
|
||||
StdOut: &sb,
|
||||
Stderr: &sb,
|
||||
Stdout: &sb,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
@@ -131,13 +131,13 @@ func TestReset(t *testing.T) {
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
StdOut: io.Discard,
|
||||
StdErr: io.Discard,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
for range 3 {
|
||||
for i := 0; i < 3; i++ {
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Errorln("error 1")
|
||||
|
||||
@@ -21,15 +21,7 @@ import (
|
||||
"github.com/bep/logg"
|
||||
)
|
||||
|
||||
// SetGlobalLogger sets the global logger.
|
||||
// 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) {
|
||||
func InitGlobalLogger(level logg.Level, panicOnWarnings bool) {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
var logHookLast func(e *logg.Entry) error
|
||||
@@ -58,5 +50,5 @@ func Log() Logger {
|
||||
var log Logger
|
||||
|
||||
func init() {
|
||||
initGlobalLogger(logg.LevelWarn, false)
|
||||
InitGlobalLogger(logg.LevelWarn, false)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -11,36 +11,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hmaps
|
||||
package maps
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
import "sync"
|
||||
|
||||
// Cache is a simple thread safe cache backed by a map.
|
||||
type Cache[K comparable, T any] struct {
|
||||
m map[K]T
|
||||
opts CacheOptions
|
||||
hasBeenInitialized bool
|
||||
m map[K]T
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// CacheOptions are the options for the Cache.
|
||||
type CacheOptions struct {
|
||||
// If set, the cache will not grow beyond this size.
|
||||
Size uint64
|
||||
}
|
||||
|
||||
var defaultCacheOptions = CacheOptions{}
|
||||
|
||||
// NewCache creates a new Cache with default options.
|
||||
// NewCache creates a new Cache.
|
||||
func NewCache[K comparable, T any]() *Cache[K, T] {
|
||||
return &Cache[K, T]{m: make(map[K]T), opts: defaultCacheOptions}
|
||||
}
|
||||
|
||||
// NewCacheWithOptions creates a new Cache with the given options.
|
||||
func NewCacheWithOptions[K comparable, T any](opts CacheOptions) *Cache[K, T] {
|
||||
return &Cache[K, T]{m: make(map[K]T), opts: opts}
|
||||
return &Cache[K, T]{m: make(map[K]T)}
|
||||
}
|
||||
|
||||
// Delete deletes the given key from the cache.
|
||||
@@ -51,13 +34,8 @@ func (c *Cache[K, T]) Get(key K) (T, bool) {
|
||||
return zero, false
|
||||
}
|
||||
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]
|
||||
c.RUnlock()
|
||||
return v, found
|
||||
}
|
||||
|
||||
@@ -79,90 +57,23 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
c.clearIfNeeded()
|
||||
c.m[key] = v
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Cache[K, T]) Set(key K, value T) {
|
||||
c.Lock()
|
||||
c.set(key, value)
|
||||
c.m[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]) clearIfNeeded() {
|
||||
if c.opts.Size > 0 && uint64(len(c.m)) >= c.opts.Size {
|
||||
// clear the map
|
||||
clear(c.m)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, T]) set(key K, value T) {
|
||||
c.clearIfNeeded()
|
||||
c.m[key] = value
|
||||
}
|
||||
|
||||
// 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) bool) {
|
||||
func (c *Cache[K, T]) ForEeach(f func(K, T)) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
for k, v := range c.m {
|
||||
if !f(k, v) {
|
||||
return
|
||||
}
|
||||
f(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +81,6 @@ 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
|
||||
}
|
||||
@@ -183,8 +93,7 @@ func (c *Cache[K, T]) Len() int {
|
||||
|
||||
func (c *Cache[K, T]) Reset() {
|
||||
c.Lock()
|
||||
clear(c.m)
|
||||
c.hasBeenInitialized = false
|
||||
c.m = make(map[K]T)
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 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.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hmaps
|
||||
package maps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -98,8 +98,6 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
|
||||
return v, nil
|
||||
case Params:
|
||||
return []map[string]any{v}, nil
|
||||
case map[string]any:
|
||||
return []map[string]any{v}, nil
|
||||
case []any:
|
||||
var s []map[string]any
|
||||
for _, entry := range v {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user