mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 15:58:53 +00:00
Compare commits
347 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b289b17c43 | |||
| 85c1727748 | |||
| 641403f7de | |||
| 24cc25552f | |||
| a2ca95629a | |||
| 367fe526e2 | |||
| 5de3913ed4 | |||
| 7b7a0f3624 | |||
| cb7a4339b7 | |||
| 157d3703c3 | |||
| 9c2f8ec61b | |||
| bd77944ba2 | |||
| c1f4228d61 | |||
| 503dcf6252 | |||
| 5dd0ba00f7 | |||
| 2e835d0841 | |||
| 2c77719cd6 | |||
| d89b9d891c | |||
| 8721a35202 | |||
| fd38aee694 | |||
| d25f7ec172 | |||
| 304a7e5e74 | |||
| a024bc7d76 | |||
| c054e18827 | |||
| 34dcac53bf | |||
| ee438606dd | |||
| fd8b0fbf8a | |||
| 9b5f786df8 | |||
| e6feb9e0be | |||
| 5e4ffa0e89 | |||
| 0cdcc2b584 | |||
| a591c4406a | |||
| 2d86a0512c | |||
| f02da7fbce | |||
| 22ee0918f3 | |||
| b50ab04031 | |||
| b772f0e3d2 | |||
| d58c0198d3 | |||
| c2fb221209 | |||
| 4245a4514d | |||
| f0c1852978 | |||
| a352e69b02 | |||
| 1dd27be81a | |||
| eb7a5aabaa | |||
| 029d1e0ced | |||
| e865d59844 | |||
| 377287a614 | |||
| 0270364a34 | |||
| 760c13a7ac | |||
| 3bd73d262d | |||
| 7104de83ce | |||
| 835579b338 | |||
| 05e067ced8 | |||
| ee48d9692a | |||
| db28695ff5 | |||
| 778f0d9002 | |||
| 13b208e2f7 | |||
| 329b2342f0 | |||
| 33b46d8a41 | |||
| 6c68142cc1 | |||
| e08d9af21e | |||
| cd7dc7a372 | |||
| c939c33fd3 | |||
| bb7b3d3cdb | |||
| 873a5cda1a | |||
| f42a4b6af5 | |||
| a5637831cd | |||
| 68586c891c | |||
| 51bb2fedab | |||
| 43307b07f8 | |||
| 346b60358d | |||
| 7f0f50b133 | |||
| 77a8e347bc | |||
| f704d75699 | |||
| 9d765e1b99 | |||
| 1f746a8724 | |||
| 6aa72acaf9 | |||
| 9885e7020d | |||
| 91101d24f2 | |||
| 637995ba8f | |||
| f1de5d2a04 | |||
| 4b0c194fb3 | |||
| 8de4ffb294 | |||
| 1f5a15aba0 | |||
| 8897113666 | |||
| e91d3cff98 | |||
| e7bd51698e | |||
| 1fad3832a9 | |||
| 8b52626915 | |||
| 81a7b63900 | |||
| 5c5508817f | |||
| 88ecc3b753 | |||
| 1191467c05 | |||
| 60c24fc5ee | |||
| 41137077bc | |||
| 5bb1564575 | |||
| 76993369a0 | |||
| de7137cc35 | |||
| a2edf04c27 | |||
| 956f915f77 | |||
| a2a4166f6d | |||
| 39f582f9f4 | |||
| 2501de7aab | |||
| 06cc8673fa | |||
| 71fae99189 | |||
| 8af04745fb | |||
| dde9d9d544 | |||
| 892b49110e | |||
| c5a63a3b4f | |||
| 61d3d20129 | |||
| b13d0a68e6 | |||
| d9594a96fc | |||
| 4a5cc2dd26 | |||
| 9cad8d38ca | |||
| cfa0801815 | |||
| f024a5050e | |||
| 80704bc9a1 | |||
| 3682027a51 | |||
| 44628616af | |||
| 0fc86783ee | |||
| e9fbadacc3 | |||
| 2a7bb1c7c9 | |||
| b7b49fb0f8 | |||
| 5f2adad2c7 | |||
| a837976731 | |||
| 0918e087ec | |||
| 4ea94c451d | |||
| 5d2cbee989 | |||
| 723e3f4342 | |||
| d913f46a8b | |||
| 2db43f841c | |||
| 46ce1f191b | |||
| aae02ca612 | |||
| 83cec785cf | |||
| 4e52be8b90 | |||
| 7888ac585c | |||
| eb1dbe0709 | |||
| 77824d704c | |||
| ec0caaec7c | |||
| 845b8885de | |||
| 43385d6aeb | |||
| a9b0b95ef4 | |||
| e229f4b387 | |||
| 020253904f | |||
| 4a5e94087b | |||
| 48a7aee961 | |||
| 6c583e3227 | |||
| 5d64b492f4 | |||
| b3f32949cb | |||
| 55ecd3a90e | |||
| 3f35721fb2 | |||
| 5c80cb0d20 | |||
| a5e5be234c | |||
| 565c30eac9 | |||
| 48dd6a918a | |||
| 744b8566ec | |||
| 7de5317aef | |||
| 4cbd4ef991 | |||
| a32c889a7b | |||
| 852d868549 | |||
| 1e34e5b26d | |||
| 641d2616c7 | |||
| a834bb9f7e | |||
| 4f130f6e4f | |||
| 9dfa112617 | |||
| ec1933f79d | |||
| 75ad9cdaab | |||
| e293e7ca6d | |||
| 157d86414d | |||
| 6be253000f | |||
| 947e4e66b5 | |||
| 989b299b55 | |||
| 5f897868c0 | |||
| e477373487 | |||
| b47376586a | |||
| 5ab38de363 | |||
| 7b69218489 | |||
| 34373407b7 | |||
| 3afe91d4b1 | |||
| 54398f8d57 | |||
| b8c15f245b | |||
| d0dc518844 | |||
| b529859008 | |||
| 487bb96474 | |||
| 2f6864387c | |||
| c1dc35dd71 | |||
| fc3d1cbadb | |||
| 7e130e34f2 | |||
| 88b7868fbd | |||
| dea158c885 | |||
| 467444ef64 | |||
| 770f548b47 | |||
| 0ab81896d9 | |||
| e00fdae456 | |||
| 21299a7a67 | |||
| aa3dd197f7 | |||
| 5a50eee9da | |||
| 8d017a60fb | |||
| 8fcd3c1487 | |||
| 2d4c0c6c8d | |||
| 59a55a1f9e | |||
| dacef0df92 | |||
| 8d92042ab9 | |||
| e917401c71 | |||
| eb298144b6 | |||
| 3b6eaf9b1f | |||
| 1fd845eee4 | |||
| 61e2ce2a49 | |||
| 2d503f82ac | |||
| 3a6b2e6b14 | |||
| bbbfeb340a | |||
| 838bd312b1 | |||
| 6bde8abaad | |||
| 66dd6ecab2 | |||
| ad43d137d5 | |||
| f7fc6ccd59 | |||
| d4de780edc | |||
| 2c54c32986 | |||
| 7a2f04ee8c | |||
| 7921777da1 | |||
| 33e964d40b | |||
| 5e3133a7d8 | |||
| 58a3c91a7f | |||
| ac6962d284 | |||
| 23d21b0d16 | |||
| 588c9019cf | |||
| ce9cf882a5 | |||
| 46e17053c8 | |||
| cb6580d008 | |||
| 8aba6dc661 | |||
| a7df536a52 | |||
| de0df119b5 | |||
| 3477d9fcec | |||
| e79ee0d516 | |||
| 2b97a2a8bf | |||
| 35afe6fe2a | |||
| ad82998d54 | |||
| 95e2d5beb8 | |||
| 2c3efc8106 | |||
| df8bd4af4f | |||
| 17e15b2148 | |||
| ca4fc587c3 | |||
| 4faaaf9c2f | |||
| 94b46c36b4 | |||
| 2b0b3b8584 | |||
| 801035bb7a | |||
| b7861e586e | |||
| 59c1158135 | |||
| fcdc454cc5 | |||
| 6cf23bf882 | |||
| 62a96cef7f | |||
| 30d9aea860 | |||
| 1f23b4949c | |||
| 5fc1639035 | |||
| 889308dd85 | |||
| 72352f205a | |||
| e3304db617 | |||
| 1ec6fa36c0 | |||
| 89bd025ebf | |||
| 62567d3820 | |||
| e10915f80a | |||
| ec3890affe | |||
| 46cccb021b | |||
| cb6e27b32a | |||
| 5bbe95f9c5 | |||
| 31d19b505d | |||
| bfa2fd683e | |||
| 88d598a049 | |||
| 352be5ba87 | |||
| d37606d2c2 | |||
| f5e54d9c7d | |||
| 42f37b4e98 | |||
| e971b7d866 | |||
| ad985550a4 | |||
| b5852d0e68 | |||
| b5801d8b6a | |||
| 64d1865c1e | |||
| e4ad0c5271 | |||
| a2f666b586 | |||
| b1b3bbcdbd | |||
| 1cfe9741b9 | |||
| 2939270a3b | |||
| 5db27b19c1 | |||
| b7d62d76c5 | |||
| 6e1c5b61b3 | |||
| e7d0757f95 | |||
| 6b5e117a12 | |||
| 4985be1a4a | |||
| db653a8485 | |||
| 05bbd2c515 | |||
| 21366e0411 | |||
| ea633dd809 | |||
| 57151a5e91 | |||
| 5bb22b6b4e | |||
| 41f69a7255 | |||
| 4a79956276 | |||
| 039845804f | |||
| 9b635522e2 | |||
| 3223a65c9b | |||
| bc6fc47727 | |||
| ffb41d1111 | |||
| 5b0b663ec3 | |||
| 3f68309148 | |||
| ab03588db9 | |||
| 0450d69fc6 | |||
| 1158e63072 | |||
| d1ba52f3c3 | |||
| d0dca65625 | |||
| f30603c47f | |||
| 12c9ce34bb | |||
| 47b055589c | |||
| e56ea40666 | |||
| 4c02a52f7c | |||
| 578442f892 | |||
| 0d390d7eb8 | |||
| da72ac2db9 | |||
| 0ea796dad1 | |||
| 1e690c0f23 | |||
| a619deec18 | |||
| 5fb333b9f5 | |||
| e07028cb90 | |||
| 22a9f3fc98 | |||
| e363964f2f | |||
| c260cb28a9 | |||
| e079145373 | |||
| 5b442b3cce | |||
| 2bc27657d8 | |||
| 28f621d4a7 | |||
| fe7e137e28 | |||
| 1c74abd260 | |||
| 3d6baedaec | |||
| 84ee00bbc2 | |||
| 2f89169baa | |||
| 66a3a11001 | |||
| 8f2eac0195 | |||
| b570a5f118 | |||
| 39fd3b5570 | |||
| aacff7aff4 | |||
| 77df7bbbff | |||
| ae3815898f | |||
| 504a23184f | |||
| e651d29801 | |||
| dec8cd4ada | |||
| 0c453420e6 | |||
| 469124823c | |||
| 96afea4acc | |||
| 6d97ee711e |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22300.20000
|
||||
- image: bepsays/ci-hugoreleaser:1.22400.20000
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
@@ -14,9 +14,7 @@ jobs:
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
steps:
|
||||
- &remote-docker
|
||||
setup_remote_docker:
|
||||
version: 20.10.14
|
||||
- setup_remote_docker
|
||||
- checkout:
|
||||
path: hugo
|
||||
- &git-config
|
||||
@@ -60,7 +58,7 @@ jobs:
|
||||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22300.20000
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
+10
-79
@@ -1,10 +1,9 @@
|
||||
name: Build Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
pull_request:
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
@@ -14,18 +13,8 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
@@ -35,9 +24,6 @@ jobs:
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
|
||||
@@ -49,69 +35,14 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
|
||||
with:
|
||||
context: .
|
||||
push: ${{ startsWith(github.ref, 'refs/tags') }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
if: ${{ startsWith(github.ref, 'refs/tags') }}
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
if: ${{ startsWith(github.ref, 'refs/tags') }}
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
@@ -6,18 +6,18 @@ name: Test
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
SASS_VERSION: 1.63.2
|
||||
DART_SASS_SHA_LINUX: 3ea33c95ad5c35fda6e9a0956199eef38a398f496cfb8750e02479d7d1dd42af
|
||||
DART_SASS_SHA_MACOS: 11c70f259836b250b44a9cb57fed70e030f21f45069b467d371685855f1eb4f0
|
||||
DART_SASS_SHA_WINDOWS: cd8cd36a619dd8e27f93d3186c52d70eb7d69472aa6c85f5094b29693e773f64
|
||||
SASS_VERSION: 1.80.3
|
||||
DART_SASS_SHA_LINUX: 7c933edbad0a7d389192c5b79393485c088bd2c4398e32f5754c32af006a9ffd
|
||||
DART_SASS_SHA_MACOS: 79e060b0e131c3bb3c16926bafc371dc33feab122bfa8c01aa337a072097967b
|
||||
DART_SASS_SHA_WINDOWS: 0bc4708b37cd1bac4740e83ac5e3176e66b774f77fd5dd364da5b5cfc9bfb469
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go-version: [1.23.x, 1.24.x]
|
||||
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
@@ -112,17 +112,17 @@ jobs:
|
||||
sass --version;
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
HUGO_BUILD_TAGS: extended,withdeploy
|
||||
- if: matrix.os == 'windows-latest'
|
||||
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
|
||||
name: Test
|
||||
run: |
|
||||
mage -v test;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
HUGO_BUILD_TAGS: extended,withdeploy
|
||||
- name: Build tags
|
||||
run: |
|
||||
go install -tags extended,nodeploy
|
||||
go install -tags extended
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Build for dragonfly
|
||||
run: |
|
||||
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
|
||||
*.test
|
||||
imports.*
|
||||
imports.*
|
||||
dist/
|
||||
public/
|
||||
.DS_Store
|
||||
@@ -93,6 +93,7 @@ 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*.
|
||||
|
||||
+78
-24
@@ -2,44 +2,98 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
FROM golang:1.21-alpine AS build
|
||||
ARG GO_VERSION="1.23.2"
|
||||
ARG ALPINE_VERSION="3.20"
|
||||
ARG DART_SASS_VERSION="1.79.3"
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=extended .
|
||||
ARG HUGO_BUILD_TAGS
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS gobuild
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS gorun
|
||||
|
||||
ARG CGO=1
|
||||
ENV CGO_ENABLED=${CGO}
|
||||
ENV GOOS=linux
|
||||
ENV GO111MODULE=on
|
||||
|
||||
FROM gobuild AS build
|
||||
|
||||
RUN apk add clang lld
|
||||
|
||||
# Set up cross-compilation helpers
|
||||
COPY --from=xx / /
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
RUN xx-apk add musl-dev gcc g++
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "none" or "withdeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=withdeploy .
|
||||
#
|
||||
# We build the extended version by default.
|
||||
ARG HUGO_BUILD_TAGS="extended"
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOPROXY=https://proxy.golang.org
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
ENV GOMODCACHE=/go/pkg/mod
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
WORKDIR /go/src/github.com/gohugoio/hugo
|
||||
|
||||
COPY . /go/src/github.com/gohugoio/hugo/
|
||||
# For --mount=type=cache the value of target is the default cache id, so
|
||||
# for the go mod cache it would be good if we could share it with other Go images using the same setup,
|
||||
# but the go build cache needs to be per platform.
|
||||
# See this comment: https://github.com/moby/buildkit/issues/1706#issuecomment-702238282
|
||||
RUN --mount=target=. \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build,id=go-build-$TARGETPLATFORM <<EOT
|
||||
set -ex
|
||||
xx-go build -tags "$HUGO_BUILD_TAGS" -ldflags "-s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=docker" -o /usr/bin/hugo
|
||||
xx-verify /usr/bin/hugo
|
||||
EOT
|
||||
|
||||
# gcc/g++ are required to build SASS libraries for extended version
|
||||
RUN apk update && \
|
||||
apk add --no-cache gcc g++ musl-dev git && \
|
||||
go install github.com/magefile/mage
|
||||
# dart-sass downloads the dart-sass runtime dependency
|
||||
FROM alpine:${ALPINE_VERSION} AS dart-sass
|
||||
ARG TARGETARCH
|
||||
ARG DART_SASS_VERSION
|
||||
ARG DART_ARCH=${TARGETARCH/amd64/x64}
|
||||
WORKDIR /out
|
||||
ADD https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-${DART_ARCH}.tar.gz .
|
||||
RUN tar -xf dart-sass-${DART_SASS_VERSION}-linux-${DART_ARCH}.tar.gz
|
||||
|
||||
RUN mage hugo && mage install
|
||||
FROM gorun AS final
|
||||
|
||||
# ---
|
||||
COPY --from=build /usr/bin/hugo /usr/bin/hugo
|
||||
|
||||
FROM alpine:3.18
|
||||
# libc6-compat are required for extended libraries (libsass, libwebp).
|
||||
RUN apk add --no-cache \
|
||||
libc6-compat \
|
||||
git \
|
||||
runuser \
|
||||
nodejs \
|
||||
npm
|
||||
|
||||
COPY --from=build /go/bin/hugo /usr/bin/hugo
|
||||
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 && \
|
||||
# See https://github.com/gohugoio/hugo/issues/9810
|
||||
runuser -u hugo -- git config --global core.quotepath false
|
||||
|
||||
# libc6-compat & libstdc++ are required for extended SASS libraries
|
||||
# ca-certificates are required to fetch outside resources (like Twitter oEmbeds)
|
||||
RUN apk update && \
|
||||
apk add --no-cache ca-certificates libc6-compat libstdc++ git
|
||||
USER hugo:hugo
|
||||
VOLUME /project
|
||||
WORKDIR /project
|
||||
ENV HUGO_CACHEDIR=/cache
|
||||
ENV PATH="/var/hugo/bin:$PATH"
|
||||
|
||||
VOLUME /site
|
||||
WORKDIR /site
|
||||
COPY scripts/docker/entrypoint.sh /entrypoint.sh
|
||||
COPY --from=dart-sass /out/dart-sass /var/hugo/bin/dart-sass
|
||||
|
||||
# Update PATH to reflect the new dependencies.
|
||||
# For more complex setups, we should probably find a way to
|
||||
# delegate this to the script itself, but this will have to do for now.
|
||||
# Also, the dart-sass binary is a little special, other binaries can be put/linked
|
||||
# directly in /var/hugo/bin.
|
||||
ENV PATH="/var/hugo/bin/dart-sass:$PATH"
|
||||
|
||||
# Expose port for live server
|
||||
EXPOSE 1313
|
||||
|
||||
ENTRYPOINT ["hugo"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["--help"]
|
||||
|
||||
@@ -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> | <a rel="me" href="https://x.com/gohugoiov2">X</a>
|
||||
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -67,9 +67,26 @@ See the [features] section of the documentation for a comprehensive summary of H
|
||||
<p float="left">
|
||||
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/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>
|
||||
<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/gohugoioTheme/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
|
||||
</p>
|
||||
|
||||
## Editions
|
||||
|
||||
Hugo is available in three editions: standard, extended, and extended/deploy. While the standard edition provides core functionality, the extended and extended/deploy editions offer advanced features.
|
||||
|
||||
Feature|extended edition|extended/deploy edition
|
||||
:--|:-:|:-:
|
||||
Encode to the WebP format when [processing images]. You can decode WebP images with any edition.|:heavy_check_mark:|:heavy_check_mark:
|
||||
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
|
||||
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See [details].|:x:|:heavy_check_mark:
|
||||
|
||||
[dart sass]: /functions/css/sass/#dart-sass
|
||||
[processing images]: /content-management/image-processing/
|
||||
[transpile sass to css]: /functions/css/sass/
|
||||
[details]: /hosting-and-deployment/hugo-deploy/
|
||||
|
||||
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
|
||||
|
||||
## Installation
|
||||
|
||||
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
|
||||
@@ -81,15 +98,11 @@ 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.20 or later
|
||||
- Extended edition: Go 1.20 or later, and GCC
|
||||
- Standard edition: Go 1.23.0 or later
|
||||
- Extended edition: Go 1.23.0 or later, and GCC
|
||||
- Extended/deploy edition: Go 1.23.0 or later, and GCC
|
||||
|
||||
Build the standard edition:
|
||||
|
||||
@@ -102,6 +115,13 @@ 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)
|
||||
@@ -148,153 +168,114 @@ 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/alecthomas/chroma/v2="v2.14.0"
|
||||
github.com/PuerkitoBio/goquery="v1.10.1"
|
||||
github.com/alecthomas/chroma/v2="v2.15.0"
|
||||
github.com/andybalholm/cascadia="v1.3.3"
|
||||
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.1.2"
|
||||
github.com/bep/gitmap="v1.6.0"
|
||||
github.com/bep/goat="v0.5.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/godartsass/v2="v2.3.2"
|
||||
github.com/bep/golibsass="v1.2.0"
|
||||
github.com/bep/gowebp="v0.3.0"
|
||||
github.com/bep/lazycache="v0.4.0"
|
||||
github.com/bep/imagemeta="v0.8.4"
|
||||
github.com/bep/lazycache="v0.7.0"
|
||||
github.com/bep/logg="v0.4.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/overlayfs="v0.9.2"
|
||||
github.com/bep/simplecobra="v0.4.0"
|
||||
github.com/bep/simplecobra="v0.5.0"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/cespare/xxhash/v2="v2.3.0"
|
||||
github.com/clbanning/mxj/v2="v2.7.0"
|
||||
github.com/cli/safeexec="v1.0.1"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.3"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.4"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
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/dlclark/regexp2="v1.11.5"
|
||||
github.com/dop251/goja="v0.0.0-20250125213203-5ef83b82af17"
|
||||
github.com/evanw/esbuild="v0.24.2"
|
||||
github.com/fatih/color="v1.18.0"
|
||||
github.com/frankban/quicktest="v1.14.6"
|
||||
github.com/fsnotify/fsnotify="v1.7.0"
|
||||
github.com/getkin/kin-openapi="v0.123.0"
|
||||
github.com/fsnotify/fsnotify="v1.8.0"
|
||||
github.com/getkin/kin-openapi="v0.129.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/go-openapi/jsonpointer="v0.21.0"
|
||||
github.com/go-openapi/swag="v0.23.0"
|
||||
github.com/go-sourcemap/sourcemap="v2.1.4+incompatible"
|
||||
github.com/gobuffalo/flect="v1.0.3"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
|
||||
github.com/gohugoio/hashstructure="v0.5.0"
|
||||
github.com/gohugoio/httpcache="v0.7.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.2.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v1.0.1"
|
||||
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/golang/freetype="v0.0.0-20170609003504-e2365dfdc4a0"
|
||||
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/google/pprof="v0.0.0-20250208200701-d0013a598941"
|
||||
github.com/gorilla/websocket="v1.5.3"
|
||||
github.com/hairyhenderson/go-codeowners="v0.7.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/kylelemons/godebug="v1.1.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.12"
|
||||
github.com/kyokomi/emoji/v2="v2.2.13"
|
||||
github.com/lucasb-eyer/go-colorful="v1.2.0"
|
||||
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.9"
|
||||
github.com/mitchellh/hashstructure="v1.1.0"
|
||||
github.com/mazznoer/csscolorparser="v0.1.5"
|
||||
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.7.0"
|
||||
github.com/oasdiff/yaml3="v0.0.0-20241210130736-a94c01f36349"
|
||||
github.com/oasdiff/yaml="v0.0.0-20241210131133-6b86fb107d80"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
|
||||
github.com/pelletier/go-toml/v2="v2.2.2"
|
||||
github.com/pelletier/go-toml/v2="v2.2.3"
|
||||
github.com/perimeterx/marshmallow="v1.1.5"
|
||||
github.com/pkg/browser="v0.0.0-20210911075715-681adbf594b8"
|
||||
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.12.0"
|
||||
github.com/rivo/uniseg="v0.4.7"
|
||||
github.com/rogpeppe/go-internal="v1.13.1"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
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/sass/libsass="3.6.6"
|
||||
github.com/spf13/afero="v1.11.0"
|
||||
github.com/spf13/cast="v1.6.0"
|
||||
github.com/spf13/cobra="v1.8.0"
|
||||
github.com/spf13/cast="v1.7.1"
|
||||
github.com/spf13/cobra="v1.8.1"
|
||||
github.com/spf13/fsync="v0.10.1"
|
||||
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/spf13/pflag="v1.0.6"
|
||||
github.com/tdewolff/minify/v2="v2.20.37"
|
||||
github.com/tdewolff/parse/v2="v2.7.15"
|
||||
github.com/tetratelabs/wazero="v1.8.2"
|
||||
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"
|
||||
github.com/yuin/goldmark-emoji="v1.0.4"
|
||||
github.com/yuin/goldmark="v1.7.8"
|
||||
go.uber.org/automaxprocs="v1.5.3"
|
||||
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"
|
||||
golang.org/x/crypto="v0.33.0"
|
||||
golang.org/x/exp="v0.0.0-20250210185358-939b2ce775ac"
|
||||
golang.org/x/image="v0.24.0"
|
||||
golang.org/x/mod="v0.23.0"
|
||||
golang.org/x/net="v0.35.0"
|
||||
golang.org/x/sync="v0.11.0"
|
||||
golang.org/x/sys="v0.30.0"
|
||||
golang.org/x/text="v0.22.0"
|
||||
golang.org/x/tools="v0.30.0"
|
||||
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da"
|
||||
gonum.org/v1/plot="v0.15.0"
|
||||
google.golang.org/protobuf="v1.36.5"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
oss.terrastruct.com/d2="v0.6.9"
|
||||
oss.terrastruct.com/util-go="v0.0.0-20241005222610-44c011a04896"
|
||||
rsc.io/qr="v0.2.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# allow user to override go executable by running as GOEXE=xxx make ...
|
||||
GOEXE="${GOEXE-go}"
|
||||
|
||||
# Send in a regexp matching the benchmarks you want to run, i.e. './benchSite.sh "YAML"'.
|
||||
# Note the quotes, which will be needed for more complex expressions.
|
||||
# The above will run all variations, but only for front matter YAML.
|
||||
|
||||
echo "Running with BenchmarkSiteBuilding/${1}"
|
||||
|
||||
"${GOEXE}" test -run="NONE" -bench="BenchmarkSiteBuilding/${1}" -test.benchmem=true ./hugolib -memprofile mem.prof -count 3 -cpuprofile cpu.prof
|
||||
@@ -1 +0,0 @@
|
||||
gobench -package=./hugolib -bench="BenchmarkSiteNew/Deep_content_tree"
|
||||
@@ -1 +0,0 @@
|
||||
docker run --rm --mount type=bind,source="$(pwd)",target=/hugo -w /hugo -i -t bepsays/ci-goreleaser:1.11-2 /bin/bash
|
||||
Vendored
+30
-11
@@ -176,11 +176,12 @@ func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManag
|
||||
}
|
||||
|
||||
// ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account.
|
||||
func (c *Cache) ClearOnRebuild(changeset ...identity.Identity) {
|
||||
// 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) {
|
||||
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
|
||||
NumWorkers: len(c.partitions),
|
||||
Handle: func(ctx context.Context, partition PartitionManager) error {
|
||||
partition.clearOnRebuild(changeset...)
|
||||
partition.clearOnRebuild(predicate, changeset...)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
@@ -430,12 +431,25 @@ func (p *Partition[K, V]) doGetOrCreateWitTimeout(key K, duration time.Duration,
|
||||
errch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
v, _, err := p.c.GetOrCreate(key, create)
|
||||
if err != nil {
|
||||
errch <- err
|
||||
return
|
||||
}
|
||||
resultch <- v
|
||||
var (
|
||||
v V
|
||||
err error
|
||||
)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if rerr, ok := r.(error); ok {
|
||||
err = rerr
|
||||
} else {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
errch <- err
|
||||
} else {
|
||||
resultch <- v
|
||||
}
|
||||
}()
|
||||
v, _, err = p.c.GetOrCreate(key, create)
|
||||
}()
|
||||
|
||||
select {
|
||||
@@ -466,7 +480,12 @@ func (p *Partition[K, V]) clearMatching(predicate func(k, v any) bool) {
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
|
||||
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
|
||||
}
|
||||
}
|
||||
opts := p.getOptions()
|
||||
if opts.ClearWhen == ClearNever {
|
||||
return
|
||||
@@ -512,7 +531,7 @@ func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
|
||||
// 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 shouldDelete(key, v) {
|
||||
if predicate(key, v) || shouldDelete(key, v) {
|
||||
p.trace.Log(
|
||||
logg.StringFunc(
|
||||
func() string {
|
||||
@@ -588,7 +607,7 @@ type PartitionManager interface {
|
||||
adjustMaxSize(addend int) int
|
||||
getMaxSize() int
|
||||
getOptions() OptionsPartition
|
||||
clearOnRebuild(changeset ...identity.Identity)
|
||||
clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity)
|
||||
clearMatching(predicate func(k, v any) bool)
|
||||
clearStale()
|
||||
}
|
||||
|
||||
Vendored
+57
-2
@@ -14,8 +14,11 @@
|
||||
package dynacache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
@@ -144,13 +147,13 @@ func TestClear(t *testing.T) {
|
||||
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 4)
|
||||
|
||||
cache.ClearOnRebuild()
|
||||
cache.ClearOnRebuild(nil)
|
||||
|
||||
// Stale items are always cleared.
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 2)
|
||||
|
||||
cache = newTestCache(t)
|
||||
cache.ClearOnRebuild(identity.StringIdentity("changed"))
|
||||
cache.ClearOnRebuild(nil, identity.StringIdentity("changed"))
|
||||
|
||||
c.Assert(cache.Keys(nil), qt.HasLen, 1)
|
||||
|
||||
@@ -165,6 +168,58 @@ func TestClear(t *testing.T) {
|
||||
cache.adjustCurrentMaxSize()
|
||||
}
|
||||
|
||||
func TestPanicInCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
cache := newTestCache(t)
|
||||
|
||||
p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", OptionsPartition{Weight: 30, ClearWhen: ClearOnRebuild})
|
||||
|
||||
willPanic := func(i int) func() {
|
||||
return func() {
|
||||
p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) {
|
||||
panic(errors.New(key))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrCreateWitTimeout needs to recover from panics in the create func.
|
||||
willErr := func(i int) error {
|
||||
_, err := p1.GetOrCreateWitTimeout(fmt.Sprintf("error-%d", i), 10*time.Second, func(key string) (testItem, error) {
|
||||
return testItem{}, errors.New(key)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 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 := 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,
|
||||
}, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v.name, qt.Equals, fmt.Sprintf("panic-%d", i))
|
||||
|
||||
v, err = p1.GetOrCreateWitTimeout(fmt.Sprintf("error-%d", i), 10*time.Second, func(key string) (testItem, error) {
|
||||
return testItem{
|
||||
name: key,
|
||||
}, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v.name, qt.Equals, fmt.Sprintf("error-%d", i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdjustCurrentMaxSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
+84
-53
@@ -39,9 +39,9 @@ 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"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/allconfig"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
@@ -66,6 +66,12 @@ func Execute(args []string) error {
|
||||
}
|
||||
args = mapLegacyArgs(args)
|
||||
cd, err := x.Execute(context.Background(), args)
|
||||
if cd != nil {
|
||||
if closer, ok := cd.Root.Command.(types.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == errHelp {
|
||||
cd.CobraCommand.Help()
|
||||
@@ -88,11 +94,17 @@ type commonConfig struct {
|
||||
fs *hugofs.Fs
|
||||
}
|
||||
|
||||
type configKey struct {
|
||||
counter int32
|
||||
ignoreModulesDoesNotExists bool
|
||||
}
|
||||
|
||||
// This is the root command.
|
||||
type rootCommand struct {
|
||||
Printf func(format string, v ...interface{})
|
||||
Println func(a ...interface{})
|
||||
Out io.Writer
|
||||
StdOut io.Writer
|
||||
StdErr io.Writer
|
||||
|
||||
logger loggers.Logger
|
||||
|
||||
@@ -101,8 +113,8 @@ type rootCommand struct {
|
||||
|
||||
// Some, but not all commands need access to these.
|
||||
// Some needs more than one, so keep them in a small cache.
|
||||
commonConfigs *lazycache.Cache[int32, *commonConfig]
|
||||
hugoSites *lazycache.Cache[int32, *hugolib.HugoSites]
|
||||
commonConfigs *lazycache.Cache[configKey, *commonConfig]
|
||||
hugoSites *lazycache.Cache[configKey, *hugolib.HugoSites]
|
||||
|
||||
// changesFromBuild received from Hugo in watch mode.
|
||||
changesFromBuild chan []identity.Identity
|
||||
@@ -129,8 +141,6 @@ type rootCommand struct {
|
||||
|
||||
logLevel string
|
||||
|
||||
verbose bool
|
||||
debug bool
|
||||
quiet bool
|
||||
devMode bool // Hidden flag.
|
||||
|
||||
@@ -144,6 +154,18 @@ func (r *rootCommand) isVerbose() bool {
|
||||
return r.logger.Level() <= logg.LevelInfo
|
||||
}
|
||||
|
||||
func (r *rootCommand) Close() error {
|
||||
if r.hugoSites != nil {
|
||||
r.hugoSites.DeleteFunc(func(key configKey, value *hugolib.HugoSites) bool {
|
||||
if value != nil {
|
||||
value.Close()
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rootCommand) Build(cd *simplecobra.Commandeer, bcfg hugolib.BuildCfg, cfg config.Provider) (*hugolib.HugoSites, error) {
|
||||
h, err := r.Hugo(cfg)
|
||||
if err != nil {
|
||||
@@ -160,17 +182,18 @@ func (r *rootCommand) Commands() []simplecobra.Commander {
|
||||
return r.commands
|
||||
}
|
||||
|
||||
func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commonConfig, error) {
|
||||
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key int32) (*commonConfig, error) {
|
||||
func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*commonConfig, error) {
|
||||
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) {
|
||||
fs := oldConf.fs
|
||||
configs, err := allconfig.LoadConfig(
|
||||
allconfig.ConfigSourceDescriptor{
|
||||
Flags: oldConf.cfg,
|
||||
Fs: fs.Source,
|
||||
Filename: r.cfgFile,
|
||||
ConfigDir: r.cfgDir,
|
||||
Logger: r.logger,
|
||||
Environment: r.environment,
|
||||
Flags: oldConf.cfg,
|
||||
Fs: fs.Source,
|
||||
Filename: r.cfgFile,
|
||||
ConfigDir: r.cfgDir,
|
||||
Logger: r.logger,
|
||||
Environment: r.environment,
|
||||
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -193,11 +216,11 @@ func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commo
|
||||
return cc, err
|
||||
}
|
||||
|
||||
func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commonConfig, error) {
|
||||
func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*commonConfig, error) {
|
||||
if cfg == nil {
|
||||
panic("cfg must be set")
|
||||
}
|
||||
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key int32) (*commonConfig, error) {
|
||||
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) {
|
||||
var dir string
|
||||
if r.source != "" {
|
||||
dir, _ = filepath.Abs(r.source)
|
||||
@@ -220,12 +243,13 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
// Load the config first to allow publishDir to be configured in config file.
|
||||
configs, err := allconfig.LoadConfig(
|
||||
allconfig.ConfigSourceDescriptor{
|
||||
Flags: cfg,
|
||||
Fs: hugofs.Os,
|
||||
Filename: r.cfgFile,
|
||||
ConfigDir: r.cfgDir,
|
||||
Environment: r.environment,
|
||||
Logger: r.logger,
|
||||
Flags: cfg,
|
||||
Fs: hugofs.Os,
|
||||
Filename: r.cfgFile,
|
||||
ConfigDir: r.cfgDir,
|
||||
Environment: r.environment,
|
||||
Logger: r.logger,
|
||||
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -307,7 +331,8 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
}
|
||||
|
||||
func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
|
||||
h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) {
|
||||
k := configKey{counter: r.configVersionID.Load()}
|
||||
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
|
||||
depsCfg := r.newDepsConfig(conf)
|
||||
return hugolib.NewHugoSites(depsCfg)
|
||||
})
|
||||
@@ -315,7 +340,12 @@ func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, err
|
||||
}
|
||||
|
||||
func (r *rootCommand) Hugo(cfg config.Provider) (*hugolib.HugoSites, error) {
|
||||
h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) {
|
||||
return r.getOrCreateHugo(cfg, false)
|
||||
}
|
||||
|
||||
func (r *rootCommand) getOrCreateHugo(cfg config.Provider, ignoreModuleDoesNotExist bool) (*hugolib.HugoSites, error) {
|
||||
k := configKey{counter: r.configVersionID.Load(), ignoreModulesDoesNotExists: ignoreModuleDoesNotExist}
|
||||
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
|
||||
conf, err := r.ConfigFromProvider(key, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -327,7 +357,7 @@ func (r *rootCommand) Hugo(cfg config.Provider) (*hugolib.HugoSites, error) {
|
||||
}
|
||||
|
||||
func (r *rootCommand) newDepsConfig(conf *commonConfig) deps.DepsCfg {
|
||||
return deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, LogOut: r.logger.Out(), LogLevel: r.logger.Level(), ChangesFromBuild: r.changesFromBuild}
|
||||
return deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, StdOut: r.logger.StdOut(), StdErr: r.logger.StdErr(), LogLevel: r.logger.Level(), ChangesFromBuild: r.changesFromBuild}
|
||||
}
|
||||
|
||||
func (r *rootCommand) Name() string {
|
||||
@@ -392,21 +422,23 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
}
|
||||
|
||||
func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
r.Out = os.Stdout
|
||||
r.StdOut = os.Stdout
|
||||
r.StdErr = os.Stderr
|
||||
if r.quiet {
|
||||
r.Out = io.Discard
|
||||
r.StdOut = io.Discard
|
||||
r.StdErr = io.Discard
|
||||
}
|
||||
// Used by mkcert (server).
|
||||
log.SetOutput(r.Out)
|
||||
log.SetOutput(r.StdOut)
|
||||
|
||||
r.Printf = func(format string, v ...interface{}) {
|
||||
if !r.quiet {
|
||||
fmt.Fprintf(r.Out, format, v...)
|
||||
fmt.Fprintf(r.StdOut, format, v...)
|
||||
}
|
||||
}
|
||||
r.Println = func(a ...interface{}) {
|
||||
if !r.quiet {
|
||||
fmt.Fprintln(r.Out, a...)
|
||||
fmt.Fprintln(r.StdOut, a...)
|
||||
}
|
||||
}
|
||||
_, running := runner.Command.(*serverCommand)
|
||||
@@ -415,14 +447,16 @@ 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)
|
||||
|
||||
r.commonConfigs = lazycache.New(lazycache.Options[int32, *commonConfig]{MaxEntries: 5})
|
||||
r.commonConfigs = lazycache.New(lazycache.Options[configKey, *commonConfig]{MaxEntries: 5})
|
||||
// We don't want to keep stale HugoSites in memory longer than needed.
|
||||
r.hugoSites = lazycache.New(lazycache.Options[int32, *hugolib.HugoSites]{
|
||||
r.hugoSites = lazycache.New(lazycache.Options[configKey, *hugolib.HugoSites]{
|
||||
MaxEntries: 1,
|
||||
OnEvict: func(key int32, value *hugolib.HugoSites) {
|
||||
OnEvict: func(key configKey, value *hugolib.HugoSites) {
|
||||
value.Close()
|
||||
runtime.GC()
|
||||
},
|
||||
@@ -450,32 +484,21 @@ 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
|
||||
}
|
||||
|
||||
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.Out,
|
||||
Stderr: r.Out,
|
||||
StdOut: r.StdOut,
|
||||
StdErr: r.StdErr,
|
||||
StoreErrors: running,
|
||||
}
|
||||
|
||||
return loggers.New(optsLogger), nil
|
||||
}
|
||||
|
||||
func (r *rootCommand) Reset() {
|
||||
func (r *rootCommand) resetLogs() {
|
||||
r.logger.Reset()
|
||||
loggers.Log().Reset()
|
||||
}
|
||||
@@ -486,16 +509,26 @@ func (r *rootCommand) IsTestRun() bool {
|
||||
}
|
||||
|
||||
func (r *rootCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
return r.initRootCommand("", cd)
|
||||
}
|
||||
|
||||
func (r *rootCommand) initRootCommand(subCommandName string, cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Use = "hugo [flags]"
|
||||
cmd.Short = "hugo builds your site"
|
||||
cmd.Long = `hugo is the main command, used to build your Hugo site.
|
||||
commandName := "hugo"
|
||||
if subCommandName != "" {
|
||||
commandName = subCommandName
|
||||
}
|
||||
cmd.Use = fmt.Sprintf("%s [flags]", commandName)
|
||||
cmd.Short = "Build your site"
|
||||
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo site.
|
||||
|
||||
Hugo is a Fast and Flexible Static Site Generator
|
||||
built with love by spf13 and friends in Go.
|
||||
|
||||
Complete documentation is available at https://gohugo.io/.`
|
||||
|
||||
cmd.Long = strings.ReplaceAll(cmd.Long, "COMMAND_NAME", commandName)
|
||||
|
||||
// Configure persistent flags
|
||||
cmd.PersistentFlags().StringVarP(&r.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
_ = cmd.MarkFlagDirname("source")
|
||||
@@ -507,6 +540,7 @@ 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)
|
||||
@@ -518,8 +552,6 @@ 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))
|
||||
@@ -564,7 +596,6 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
|
||||
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.")
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
)
|
||||
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
func newExec() (*simplecobra.Exec, error) {
|
||||
rootCmd := &rootCommand{
|
||||
commands: []simplecobra.Commander{
|
||||
newHugoBuildCmd(),
|
||||
newVersionCmd(),
|
||||
newEnvCommand(),
|
||||
newServerCommand(),
|
||||
@@ -38,3 +41,33 @@ func newExec() (*simplecobra.Exec, error) {
|
||||
|
||||
return simplecobra.New(rootCmd)
|
||||
}
|
||||
|
||||
func newHugoBuildCmd() simplecobra.Commander {
|
||||
return &hugoBuildCommand{}
|
||||
}
|
||||
|
||||
// hugoBuildCommand just delegates to the rootCommand.
|
||||
type hugoBuildCommand struct {
|
||||
rootCmd *rootCommand
|
||||
}
|
||||
|
||||
func (c *hugoBuildCommand) Commands() []simplecobra.Commander {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *hugoBuildCommand) Name() string {
|
||||
return "build"
|
||||
}
|
||||
|
||||
func (c *hugoBuildCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
c.rootCmd = cd.Root.Command.(*rootCommand)
|
||||
return c.rootCmd.initRootCommand("build", cd)
|
||||
}
|
||||
|
||||
func (c *hugoBuildCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
return c.rootCmd.PreRun(cd, runner)
|
||||
}
|
||||
|
||||
func (c *hugoBuildCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
|
||||
return c.rootCmd.Run(ctx, cd, args)
|
||||
}
|
||||
|
||||
+9
-7
@@ -43,8 +43,9 @@ func newConfigCommand() *configCommand {
|
||||
type configCommand struct {
|
||||
r *rootCommand
|
||||
|
||||
format string
|
||||
lang string
|
||||
format string
|
||||
lang string
|
||||
printZero bool
|
||||
|
||||
commands []simplecobra.Commander
|
||||
}
|
||||
@@ -58,7 +59,7 @@ func (c *configCommand) Name() string {
|
||||
}
|
||||
|
||||
func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
|
||||
conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil))
|
||||
conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,7 +79,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: true}); err != nil {
|
||||
if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: !c.printZero}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -110,11 +111,12 @@ 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 = "Print the site configuration"
|
||||
cmd.Long = `Print the site 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)
|
||||
|
||||
@@ -209,7 +211,7 @@ func (c *configMountsCommand) Name() string {
|
||||
|
||||
func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
|
||||
r := c.configCmd.r
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
|
||||
conf, err := r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+2
-2
@@ -105,8 +105,8 @@ func (c *convertCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, ar
|
||||
|
||||
func (c *convertCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Convert your content to different formats"
|
||||
cmd.Long = `Convert your content (e.g. front matter) to different formats.
|
||||
cmd.Short = "Convert front matter to another format"
|
||||
cmd.Long = `Convert front matter to another format.
|
||||
|
||||
See convert's subcommands toJSON, toTOML and toYAML for more information.`
|
||||
|
||||
|
||||
+4
-29
@@ -11,21 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//go:build withdeploy
|
||||
|
||||
package commands
|
||||
|
||||
@@ -33,7 +19,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/gohugoio/hugo/deploy"
|
||||
"github.com/gohugoio/hugo/deploy/deployconfig"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -42,8 +27,8 @@ import (
|
||||
func newDeployCommand() simplecobra.Commander {
|
||||
return &simpleCommand{
|
||||
name: "deploy",
|
||||
short: "Deploy your site to a Cloud provider.",
|
||||
long: `Deploy your site 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.
|
||||
@@ -60,17 +45,7 @@ documentation.
|
||||
return deployer.Deploy(ctx)
|
||||
},
|
||||
withc: func(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)
|
||||
applyDeployFlags(cmd, r)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -11,8 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build nodeploy
|
||||
// +build nodeploy
|
||||
//go:build !withdeploy
|
||||
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
@@ -31,6 +30,7 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -40,9 +40,10 @@ func newDeployCommand() simplecobra.Commander {
|
||||
return &simpleCommand{
|
||||
name: "deploy",
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
return nil
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
+4
-4
@@ -25,8 +25,8 @@ import (
|
||||
func newEnvCommand() simplecobra.Commander {
|
||||
return &simpleCommand{
|
||||
name: "env",
|
||||
short: "Print Hugo version and environment info",
|
||||
long: "Print Hugo version and environment info. This is useful in Hugo bug reports",
|
||||
short: "Display version and environment info",
|
||||
long: "Display version and environment info. This is useful in Hugo bug reports",
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
r.Printf("%s\n", hugo.BuildVersionString())
|
||||
r.Printf("GOOS=%q\n", runtime.GOOS)
|
||||
@@ -61,8 +61,8 @@ func newVersionCmd() simplecobra.Commander {
|
||||
r.Println(hugo.BuildVersionString())
|
||||
return nil
|
||||
},
|
||||
short: "Print Hugo version and environment info",
|
||||
long: "Print Hugo version and environment info. This is useful in Hugo bug reports.",
|
||||
short: "Display version",
|
||||
long: "Display version and environment info. This is useful in Hugo bug reports.",
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
|
||||
+12
-4
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
@@ -60,6 +61,10 @@ func newGenCommand() *genCommand {
|
||||
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)
|
||||
@@ -75,7 +80,9 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
return err
|
||||
}
|
||||
formatter := html.New(html.WithAllClasses(true))
|
||||
formatter.WriteCSS(os.Stdout, style)
|
||||
w := os.Stdout
|
||||
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
|
||||
formatter.WriteCSS(w, style)
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
@@ -142,7 +149,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/.
|
||||
@@ -194,7 +201,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)
|
||||
@@ -273,7 +280,8 @@ func (c *genCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args [
|
||||
|
||||
func (c *genCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "A collection of several useful generators."
|
||||
cmd.Short = "Generate documentation and syntax highlighting styles"
|
||||
cmd.Long = "Generate documentation for your project using Hugo's documentation engine, including syntax highlighting for various programming languages."
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
|
||||
+59
-46
@@ -27,7 +27,6 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
@@ -63,7 +62,7 @@ type hugoBuilder struct {
|
||||
|
||||
// Currently only set when in "fast render mode".
|
||||
changeDetector *fileChangeDetector
|
||||
visitedURLs *types.EvictingStringQueue
|
||||
visitedURLs *types.EvictingQueue[string]
|
||||
|
||||
fullRebuildSem *semaphore.Weighted
|
||||
debounce func(f func())
|
||||
@@ -136,10 +135,6 @@ func (e *hugoBuilderErrState) wasErr() bool {
|
||||
return e.waserr
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) errCount() int {
|
||||
return c.r.logger.LoggCount(logg.LevelError) + loggers.Log().LoggCount(logg.LevelError)
|
||||
}
|
||||
|
||||
// getDirList provides NewWatcher() with a list of directories to watch for changes.
|
||||
func (c *hugoBuilder) getDirList() ([]string, error) {
|
||||
h, err := c.hugo()
|
||||
@@ -345,7 +340,6 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa
|
||||
for {
|
||||
select {
|
||||
case changes := <-c.r.changesFromBuild:
|
||||
c.errState.setBuildErr(nil)
|
||||
unlock, err := h.LockBuild()
|
||||
if err != nil {
|
||||
c.r.logger.Errorln("Failed to acquire a build lock: %s", err)
|
||||
@@ -358,7 +352,7 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa
|
||||
}
|
||||
if c.s != nil && c.s.doLiveReload {
|
||||
doReload := c.changeDetector == nil || len(c.changeDetector.changed()) > 0
|
||||
doReload = doReload || c.showErrorInBrowser && c.errCount() > 0
|
||||
doReload = doReload || c.showErrorInBrowser && c.errState.buildErr() != nil
|
||||
if doReload {
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
@@ -372,7 +366,7 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa
|
||||
return
|
||||
}
|
||||
c.handleEvents(watcher, staticSyncer, evs, configSet)
|
||||
if c.showErrorInBrowser && c.errCount() > 0 {
|
||||
if c.showErrorInBrowser && c.errState.buildErr() != nil {
|
||||
// Need to reload browser to show the error
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
@@ -419,11 +413,17 @@ func (c *hugoBuilder) build() error {
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) buildSites(noBuildLock bool) (err error) {
|
||||
h, err := c.hugo()
|
||||
defer func() {
|
||||
c.errState.setBuildErr(err)
|
||||
}()
|
||||
|
||||
var h *hugolib.HugoSites
|
||||
h, err = c.hugo()
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
return h.Build(hugolib.BuildCfg{NoBuildLock: noBuildLock})
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: noBuildLock})
|
||||
return
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) copyStatic() (map[string]uint64, error) {
|
||||
@@ -619,6 +619,9 @@ func (c *hugoBuilder) fullRebuild(changeType string) {
|
||||
// Set the processing on pause until the state is recovered.
|
||||
c.errState.setPaused(true)
|
||||
c.handleBuildErr(err, "Failed to reload config")
|
||||
if c.s.doLiveReload {
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
} else {
|
||||
c.errState.setPaused(false)
|
||||
}
|
||||
@@ -660,7 +663,20 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
var n int
|
||||
for _, ev := range evs {
|
||||
keep := true
|
||||
if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
|
||||
// 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 _, err := os.Stat(ev.Name); err != nil {
|
||||
keep = false
|
||||
}
|
||||
@@ -779,6 +795,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
istemp := strings.HasSuffix(ext, "~") ||
|
||||
(ext == ".swp") || // vim
|
||||
(ext == ".swx") || // vim
|
||||
(ext == ".bck") || // helix
|
||||
(ext == ".tmp") || // generic temp file
|
||||
(ext == ".DS_Store") || // OSX Thumbnail
|
||||
baseName == "4913" || // vim
|
||||
@@ -801,21 +818,6 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
continue
|
||||
}
|
||||
|
||||
// Write and rename operations are often followed by CHMOD.
|
||||
// There may be valid use cases for rebuilding the site on CHMOD,
|
||||
// but that will require more complex logic than this simple conditional.
|
||||
// On OS X this seems to be related to Spotlight, see:
|
||||
// https://github.com/go-fsnotify/fsnotify/issues/15
|
||||
// A workaround is to put your site(s) on the Spotlight exception list,
|
||||
// but that may be a little mysterious for most end users.
|
||||
// So, for now, we skip reload on CHMOD.
|
||||
// We do have to check for WRITE though. On slower laptops a Chmod
|
||||
// could be aggregated with other important events, and we still want
|
||||
// to rebuild on those
|
||||
if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
|
||||
continue
|
||||
}
|
||||
|
||||
walkAdder := func(path string, f hugofs.FileMetaInfo) error {
|
||||
if f.IsDir() {
|
||||
c.r.logger.Println("adding created directory to watchlist", path)
|
||||
@@ -916,7 +918,11 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
|
||||
changed := c.changeDetector.changed()
|
||||
if c.changeDetector != nil {
|
||||
lrl.Logf("build changed %d files", len(changed))
|
||||
if len(changed) >= 10 {
|
||||
lrl.Logf("build changed %d files", len(changed))
|
||||
} else {
|
||||
lrl.Logf("build changed %d files: %q", len(changed), changed)
|
||||
}
|
||||
if len(changed) == 0 {
|
||||
// Nothing has changed.
|
||||
return
|
||||
@@ -962,7 +968,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(otherChanges[0]), false)
|
||||
lrl.Logf("refreshing %q", pathToRefresh)
|
||||
livereload.RefreshPath(pathToRefresh)
|
||||
} else if len(cssChanges) == 0 {
|
||||
} else if len(cssChanges) == 0 || len(otherChanges) > 1 {
|
||||
lrl.Logf("force refresh")
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
@@ -1046,7 +1052,7 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
"fastRenderMode": c.fastRenderMode,
|
||||
})
|
||||
|
||||
conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, cfg))
|
||||
conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1080,42 +1086,49 @@ func (c *hugoBuilder) printChangeDetected(typ string) {
|
||||
c.r.logger.Println(htime.Now().Format(layout))
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) error {
|
||||
func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) (err error) {
|
||||
defer func() {
|
||||
c.errState.setBuildErr(err)
|
||||
}()
|
||||
if err := c.errState.buildErr(); err != nil {
|
||||
ferrs := herrors.UnwrapFileErrorsWithErrorContext(err)
|
||||
for _, err := range ferrs {
|
||||
events = append(events, fsnotify.Event{Name: err.Position().Filename, Op: fsnotify.Write})
|
||||
}
|
||||
}
|
||||
c.errState.setBuildErr(nil)
|
||||
h, err := c.hugo()
|
||||
var h *hugolib.HugoSites
|
||||
h, err = c.hugo()
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...)
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) rebuildSitesForChanges(ids []identity.Identity) error {
|
||||
c.errState.setBuildErr(nil)
|
||||
h, err := c.hugo()
|
||||
func (c *hugoBuilder) rebuildSitesForChanges(ids []identity.Identity) (err error) {
|
||||
defer func() {
|
||||
c.errState.setBuildErr(err)
|
||||
}()
|
||||
|
||||
var h *hugolib.HugoSites
|
||||
h, err = c.hugo()
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
whatChanged := &hugolib.WhatChanged{}
|
||||
whatChanged.Add(ids...)
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, WhatChanged: whatChanged, RecentlyVisited: c.visitedURLs, ErrRecovery: c.errState.wasErr()})
|
||||
c.errState.setBuildErr(err)
|
||||
return err
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, WhatChanged: whatChanged, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) reloadConfig() error {
|
||||
c.r.Reset()
|
||||
c.r.resetLogs()
|
||||
c.r.configVersionID.Add(1)
|
||||
|
||||
if err := c.withConfE(func(conf *commonConfig) error {
|
||||
oldConf := conf
|
||||
newConf, err := c.r.ConfigFromConfig(c.r.configVersionID.Load(), conf)
|
||||
newConf, err := c.r.ConfigFromConfig(configKey{counter: c.r.configVersionID.Load()}, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+2
-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 your site from others."
|
||||
cmd.Long = `Import your site from other web site generators like Jekyll.
|
||||
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`."
|
||||
|
||||
|
||||
+3
-3
@@ -57,7 +57,7 @@ func newListCommand() *listCommand {
|
||||
return err
|
||||
}
|
||||
|
||||
writer := csv.NewWriter(r.Out)
|
||||
writer := csv.NewWriter(r.StdOut)
|
||||
defer writer.Flush()
|
||||
|
||||
writer.Write([]string{
|
||||
@@ -199,8 +199,8 @@ func (c *listCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
|
||||
func (c *listCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Listing out various types of content"
|
||||
cmd.Long = `Listing out various types of content.
|
||||
cmd.Short = "List content"
|
||||
cmd.Long = `List content.
|
||||
|
||||
List requires a subcommand, e.g. hugo list drafts`
|
||||
|
||||
|
||||
+26
-22
@@ -44,16 +44,16 @@ 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: "Experimental: Prepares and writes a composite package.json file for your project.",
|
||||
short: "Experimental: Prepares and writes a composite package.json file for your project",
|
||||
long: `Prepares and writes a composite package.json file for your project.
|
||||
|
||||
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.
|
||||
with the base dependency set.
|
||||
|
||||
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
|
||||
|
||||
@@ -80,12 +80,12 @@ so this may/will change in future versions of Hugo.
|
||||
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.
|
||||
`,
|
||||
@@ -94,7 +94,7 @@ so this may/will change in future versions of Hugo.
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
h, err := r.getOrCreateHugo(flagsToCfg(cd, nil), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -102,12 +102,16 @@ so this may/will change in future versions of Hugo.
|
||||
if len(args) >= 1 {
|
||||
initPath = args[0]
|
||||
}
|
||||
return h.Configs.ModulesClient.Init(initPath)
|
||||
c := h.Configs.ModulesClient
|
||||
if err := c.Init(initPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&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
|
||||
@@ -115,7 +119,7 @@ so this may/will change in future versions of Hugo.
|
||||
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
|
||||
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -125,7 +129,7 @@ so this may/will change in future versions of Hugo.
|
||||
},
|
||||
&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.
|
||||
`,
|
||||
@@ -135,7 +139,7 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
|
||||
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -145,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
|
||||
@@ -171,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)
|
||||
@@ -186,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.
|
||||
`,
|
||||
@@ -205,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
|
||||
@@ -271,7 +275,7 @@ Run "go help get" for more information. All flags available for "go get" is also
|
||||
|
||||
cfg := config.New()
|
||||
cfg.Set("workingDir", dir)
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Add(1), flagsToCfg(cd, cfg))
|
||||
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Add(1)}, flagsToCfg(cd, cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -284,7 +288,7 @@ Run "go help get" for more information. All flags available for "go get" is also
|
||||
})
|
||||
return nil
|
||||
} else {
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
|
||||
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -313,7 +317,7 @@ func (c *modCommands) Name() string {
|
||||
}
|
||||
|
||||
func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
|
||||
_, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), nil)
|
||||
_, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -324,7 +328,7 @@ func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
|
||||
func (c *modCommands) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Various Hugo Modules helpers."
|
||||
cmd.Short = "Manage modules"
|
||||
cmd.Long = `Various helpers to help manage the modules in your project's dependency graph.
|
||||
Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git).
|
||||
This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor".
|
||||
|
||||
+4
-4
@@ -40,7 +40,7 @@ func newNewCommand() *newCommand {
|
||||
&simpleCommand{
|
||||
name: "content",
|
||||
use: "content [path]",
|
||||
short: "Create new content for your site",
|
||||
short: "Create new content",
|
||||
long: `Create a new content file and automatically set the date and title.
|
||||
It will guess which kind of file to create based on the path provided.
|
||||
|
||||
@@ -93,7 +93,7 @@ Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
cfg.Set("workingDir", createpath)
|
||||
cfg.Set("publishDir", "public")
|
||||
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
|
||||
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -136,7 +136,7 @@ according to your needs.`,
|
||||
cfg := config.New()
|
||||
cfg.Set("publishDir", "public")
|
||||
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
|
||||
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func (c *newCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args [
|
||||
|
||||
func (c *newCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Create new content for your site"
|
||||
cmd.Short = "Create new content"
|
||||
cmd.Long = `Create a new content file and automatically set the date and title.
|
||||
It will guess which kind of file to create based on the path provided.
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ func newReleaseCommand() simplecobra.Commander {
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
|
||||
+104
-68
@@ -32,6 +32,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -40,6 +41,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bep/mclib"
|
||||
"github.com/pkg/browser"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/bep/simplecobra"
|
||||
@@ -82,10 +84,14 @@ 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.EvictingStringQueue
|
||||
var visitedURLs *types.EvictingQueue[string]
|
||||
if s != nil && !s.disableFastRender {
|
||||
visitedURLs = types.NewEvictingStringQueue(20)
|
||||
visitedURLs = types.NewEvictingQueue[string](20)
|
||||
}
|
||||
return &hugoBuilder{
|
||||
r: r,
|
||||
@@ -113,7 +119,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 {
|
||||
@@ -209,16 +215,17 @@ func (f *fileChangeDetector) changed() []string {
|
||||
}
|
||||
}
|
||||
|
||||
return f.filterIrrelevant(c)
|
||||
return f.filterIrrelevantAndSort(c)
|
||||
}
|
||||
|
||||
func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
|
||||
func (f *fileChangeDetector) filterIrrelevantAndSort(in []string) []string {
|
||||
var filtered []string
|
||||
for _, v := range in {
|
||||
if !f.irrelevantRe.MatchString(v) {
|
||||
filtered = append(filtered, v)
|
||||
}
|
||||
}
|
||||
sort.Strings(filtered)
|
||||
return filtered
|
||||
}
|
||||
|
||||
@@ -304,64 +311,65 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
w.Header().Set(header.Key, header.Value)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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 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 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>")
|
||||
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
|
||||
}
|
||||
}
|
||||
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 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
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if f.c.fastRenderMode && f.c.errState.buildErr() == nil {
|
||||
if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") {
|
||||
if isNavigation(requestURI, r) {
|
||||
if !f.c.visitedURLs.Contains(requestURI) {
|
||||
// If not already on stack, re-render that single page.
|
||||
if err := f.c.partialReRender(requestURI); err != nil {
|
||||
@@ -448,6 +456,7 @@ type serverCommand struct {
|
||||
// Flags.
|
||||
renderStaticToDisk bool
|
||||
navigateToChanged bool
|
||||
openBrowser bool
|
||||
serverAppend bool
|
||||
serverInterface string
|
||||
tlsCertFile string
|
||||
@@ -508,7 +517,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 = "A high performance webserver"
|
||||
cmd.Short = "Start the embedded web server"
|
||||
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.
|
||||
|
||||
@@ -539,6 +548,7 @@ 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 site in a browser after server startup")
|
||||
cmd.Flags().BoolVar(&c.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
|
||||
cmd.Flags().BoolVar(&c.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
|
||||
cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
|
||||
@@ -648,9 +658,8 @@ func (c *serverCommand) setServerInfoInConfig() error {
|
||||
}
|
||||
|
||||
func (c *serverCommand) getErrorWithContext() any {
|
||||
errCount := c.errCount()
|
||||
|
||||
if errCount == 0 {
|
||||
buildErr := c.errState.buildErr()
|
||||
if buildErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -659,7 +668,7 @@ func (c *serverCommand) getErrorWithContext() any {
|
||||
m["Error"] = cleanErrorLog(c.r.logger.Errors())
|
||||
|
||||
m["Version"] = hugo.BuildVersionString()
|
||||
ferrors := herrors.UnwrapFileErrorsWithErrorContext(c.errState.buildErr())
|
||||
ferrors := herrors.UnwrapFileErrorsWithErrorContext(buildErr)
|
||||
m["Files"] = ferrors
|
||||
|
||||
return m
|
||||
@@ -830,22 +839,25 @@ func (c *serverCommand) fixURL(baseURLFromConfig, baseURLFromFlag string, port i
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func (c *serverCommand) partialReRender(urls ...string) error {
|
||||
func (c *serverCommand) partialReRender(urls ...string) (err error) {
|
||||
defer func() {
|
||||
c.errState.setWasErr(false)
|
||||
}()
|
||||
c.errState.setBuildErr(nil)
|
||||
visited := types.NewEvictingStringQueue(len(urls))
|
||||
visited := types.NewEvictingQueue[string](len(urls))
|
||||
for _, url := range urls {
|
||||
visited.Add(url)
|
||||
}
|
||||
|
||||
h, err := c.hugo()
|
||||
var h *hugolib.HugoSites
|
||||
h, err = c.hugo()
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
// Note: We do not set NoBuildLock as the file lock is not acquired at this stage.
|
||||
return h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()})
|
||||
err = h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyTouched: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *serverCommand) serve() error {
|
||||
@@ -996,6 +1008,13 @@ 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 {
|
||||
@@ -1012,10 +1031,6 @@ func (c *serverCommand) serve() error {
|
||||
c.r.Println("Error:", err)
|
||||
}
|
||||
|
||||
if h := c.hugoTry(); h != nil {
|
||||
h.Close()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
wg2, ctx := errgroup.WithContext(ctx)
|
||||
@@ -1220,3 +1235,24 @@ 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, ".")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ const (
|
||||
ErrRemoteGetCSV = "error-remote-getcsv"
|
||||
|
||||
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
|
||||
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
|
||||
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
|
||||
)
|
||||
|
||||
// Field/method names with special meaning.
|
||||
@@ -39,7 +41,7 @@ const (
|
||||
ResourceTransformationFingerprint = "fingerprint"
|
||||
)
|
||||
|
||||
// IsResourceTransformationLinkChange returns whether the given name is a resource transformation that changes the permalink based on the content.
|
||||
// IsResourceTransformationPermalinkHash 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
|
||||
}
|
||||
|
||||
@@ -38,6 +38,19 @@ func XXHashFromReader(r io.Reader) (uint64, int64, error) {
|
||||
return h.Sum64(), size, nil
|
||||
}
|
||||
|
||||
// XxHashFromReaderHexEncoded calculates the xxHash for the given reader
|
||||
// and returns the hash as a hex encoded string.
|
||||
func XxHashFromReaderHexEncoded(r io.Reader) (string, error) {
|
||||
h := getXxHashReadFrom()
|
||||
defer putXxHashReadFrom(h)
|
||||
_, err := io.Copy(h, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash := h.Sum(nil)
|
||||
return hex.EncodeToString(hash), nil
|
||||
}
|
||||
|
||||
// XXHashFromString calculates the xxHash for the given string.
|
||||
func XXHashFromString(s string) (uint64, error) {
|
||||
h := xxhash.New()
|
||||
@@ -70,6 +83,13 @@ 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{
|
||||
@@ -103,16 +123,24 @@ func HashUint64(vs ...any) uint64 {
|
||||
o = elements
|
||||
}
|
||||
|
||||
hashOpts := getHashOpts()
|
||||
defer putHashOpts(hashOpts)
|
||||
|
||||
hash, err := hashstructure.Hash(o, hashOpts)
|
||||
hash, err := Hash(o)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// Hash returns a hash from vs.
|
||||
func Hash(vs ...any) (uint64, error) {
|
||||
hashOpts := getHashOpts()
|
||||
defer putHashOpts(hashOpts)
|
||||
var v any = vs
|
||||
if len(vs) == 1 {
|
||||
v = vs[0]
|
||||
}
|
||||
return hashstructure.Hash(v, hashOpts)
|
||||
}
|
||||
|
||||
type keyer interface {
|
||||
Key() string
|
||||
}
|
||||
|
||||
@@ -142,3 +142,16 @@ func BenchmarkHashString(b *testing.B) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHashMap(b *testing.B) {
|
||||
m := map[string]interface{}{}
|
||||
for i := 0; i < 1000; i++ {
|
||||
m[fmt.Sprintf("key%d", i)] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
HashString(m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,11 +133,26 @@ 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/"
|
||||
|
||||
var deferredStringToRemove = regexp.MustCompile(`executing "__hdeferred/.*" `)
|
||||
var deferredStringToRemove = regexp.MustCompile(`executing "__hdeferred/.*?" `)
|
||||
|
||||
// ImproveRenderErr improves the error message for rendering errors.
|
||||
func ImproveRenderErr(inErr error) (outErr error) {
|
||||
|
||||
@@ -20,8 +20,6 @@ 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"
|
||||
@@ -153,8 +151,6 @@ 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:
|
||||
@@ -262,8 +258,27 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
|
||||
return f, realFilename, nil
|
||||
}
|
||||
|
||||
// Cause returns the underlying error or itself if it does not implement Unwrap.
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
@@ -271,7 +286,7 @@ func Cause(err error) error {
|
||||
}
|
||||
|
||||
func extractFileTypePos(err error) (string, text.Position) {
|
||||
err = Cause(err)
|
||||
err = Unwrap(err)
|
||||
|
||||
var fileType string
|
||||
|
||||
@@ -388,14 +403,7 @@ func extractPosition(e error) (pos text.Position) {
|
||||
case godartsass.SassError:
|
||||
span := v.Span
|
||||
start := span.Start
|
||||
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)
|
||||
filename, _ := paths.UrlStringToFilename(span.Url)
|
||||
pos.Filename = filename
|
||||
pos.Offset = start.Offset
|
||||
pos.ColumnNumber = start.Column
|
||||
|
||||
+85
-17
@@ -26,7 +26,10 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/security"
|
||||
)
|
||||
@@ -86,7 +89,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) *Exec {
|
||||
func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
|
||||
var baseEnviron []string
|
||||
for _, v := range os.Environ() {
|
||||
k, _ := config.SplitEnvVar(v)
|
||||
@@ -96,9 +99,11 @@ func New(cfg security.Config, workingDir string) *Exec {
|
||||
}
|
||||
|
||||
return &Exec{
|
||||
sc: cfg,
|
||||
workingDir: workingDir,
|
||||
baseEnviron: baseEnviron,
|
||||
sc: cfg,
|
||||
workingDir: workingDir,
|
||||
infol: log.InfoCommand("exec"),
|
||||
baseEnviron: baseEnviron,
|
||||
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,12 +129,14 @@ func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
|
||||
type Exec struct {
|
||||
sc security.Config
|
||||
workingDir string
|
||||
infol logg.LevelLogger
|
||||
|
||||
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
|
||||
baseEnviron []string
|
||||
|
||||
npxInit sync.Once
|
||||
npxAvailable bool
|
||||
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
|
||||
npxInit sync.Once
|
||||
npxAvailable bool
|
||||
}
|
||||
|
||||
func (e *Exec) New(name string, arg ...any) (Runner, error) {
|
||||
@@ -155,25 +162,86 @@ 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) {
|
||||
// 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...)
|
||||
if err := e.sc.CheckAllowedExec(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.checkNpx()
|
||||
if e.npxAvailable {
|
||||
r, err := e.npx(name, arg...)
|
||||
if err == nil {
|
||||
return r, nil
|
||||
|
||||
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 := safeexec.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 := safeexec.LookPath(name); err != nil {
|
||||
return nil
|
||||
}
|
||||
return func(arg2 ...any) (Runner, error) {
|
||||
return e.New(name, arg2...)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
locations := []binaryLocation{binaryLocationNodeModules, binaryLocationNpx, binaryLocationPath}
|
||||
if name == "tailwindcss" {
|
||||
// See https://github.com/gohugoio/hugo/issues/13221#issuecomment-2574801253
|
||||
locations = []binaryLocation{binaryLocationNodeModules, binaryLocationPath, binaryLocationNpx}
|
||||
}
|
||||
for _, loc := range locations {
|
||||
if f := tryFuncs[loc](); f != nil {
|
||||
e.infol.Logf("resolve %q using %s", name, loc)
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return nil, &NotFoundError{name: name, method: fmt.Sprintf("in %s", locations[len(locations)-1])}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.New(name, arg...)
|
||||
|
||||
return newRunner(arg...)
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -74,6 +74,16 @@ func IsTruthful(in any) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsMap reports whether v is a map.
|
||||
func IsMap(v any) bool {
|
||||
return reflect.ValueOf(v).Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// IsSlice reports whether v is a slice.
|
||||
func IsSlice(v any) bool {
|
||||
return reflect.ValueOf(v).Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
|
||||
|
||||
// IsTruthfulValue returns whether the given value has a meaningful truth value.
|
||||
@@ -223,6 +233,27 @@ 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 := 0; i < vvv.Len(); i++ {
|
||||
out[i] = vvv.Index(i).Interface()
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
|
||||
fn := v.MethodByName(name)
|
||||
var args []reflect.Value
|
||||
|
||||
@@ -50,6 +50,19 @@ 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})
|
||||
}
|
||||
|
||||
func BenchmarkIsContextType(b *testing.B) {
|
||||
type k string
|
||||
b.Run("value", func(b *testing.B) {
|
||||
|
||||
@@ -48,16 +48,16 @@ func TestHasBytesWriter(t *testing.T) {
|
||||
|
||||
for i := 0; i < 22; i++ {
|
||||
h, w := neww()
|
||||
fmt.Fprintf(w, rndStr()+"abc __foobar"+rndStr())
|
||||
fmt.Fprint(w, rndStr()+"abc __foobar"+rndStr())
|
||||
c.Assert(h.Patterns[0].Match, qt.Equals, true)
|
||||
|
||||
h, w = neww()
|
||||
fmt.Fprintf(w, rndStr()+"abc __f")
|
||||
fmt.Fprintf(w, "oo bar"+rndStr())
|
||||
fmt.Fprint(w, rndStr()+"abc __f")
|
||||
fmt.Fprint(w, "oo bar"+rndStr())
|
||||
c.Assert(h.Patterns[0].Match, qt.Equals, true)
|
||||
|
||||
h, w = neww()
|
||||
fmt.Fprintf(w, rndStr()+"abc __moo bar")
|
||||
fmt.Fprint(w, rndStr()+"abc __moo bar")
|
||||
c.Assert(h.Patterns[0].Match, qt.Equals, false)
|
||||
}
|
||||
|
||||
|
||||
@@ -74,13 +74,13 @@ type StringReader interface {
|
||||
ReadString() string
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloserFromString uses strings.NewReader to create a new ReadSeekerNoOpCloser
|
||||
// NewReadSeekerNoOpCloserFromBytes uses bytes.NewReader to create a new ReadSeekerNoOpCloser
|
||||
// from the given bytes slice.
|
||||
func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser {
|
||||
return readSeekerNopCloser{bytes.NewReader(content)}
|
||||
}
|
||||
|
||||
// NewReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker.
|
||||
// NewOpenReadSeekCloser 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) {
|
||||
|
||||
+35
-22
@@ -25,14 +25,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
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/maps"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -55,6 +54,8 @@ var (
|
||||
vendorInfo string
|
||||
)
|
||||
|
||||
var _ maps.StoreProvider = (*HugoInfo)(nil)
|
||||
|
||||
// HugoInfo contains information about the current Hugo environment
|
||||
type HugoInfo struct {
|
||||
CommitHash string
|
||||
@@ -72,6 +73,8 @@ type HugoInfo struct {
|
||||
conf ConfigProvider
|
||||
deps []*Dependency
|
||||
|
||||
store *maps.Scratch
|
||||
|
||||
// Context gives access to some of the context scoped variables.
|
||||
Context Context
|
||||
}
|
||||
@@ -116,6 +119,10 @@ func (i HugoInfo) Deps() []*Dependency {
|
||||
return i.deps
|
||||
}
|
||||
|
||||
func (i HugoInfo) Store() *maps.Scratch {
|
||||
return i.store
|
||||
}
|
||||
|
||||
// Deprecated: Use hugo.IsMultihost instead.
|
||||
func (i HugoInfo) IsMultiHost() bool {
|
||||
Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0")
|
||||
@@ -185,6 +192,7 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
|
||||
Environment: conf.Environment(),
|
||||
conf: conf,
|
||||
deps: deps,
|
||||
store: maps.NewScratch(),
|
||||
GoVersion: goVersion,
|
||||
}
|
||||
}
|
||||
@@ -308,7 +316,7 @@ func GetDependencyListNonGo() []string {
|
||||
|
||||
if dartSass := dartSassVersion(); dartSass.ProtocolVersion != "" {
|
||||
dartSassPath := "github.com/sass/dart-sass-embedded"
|
||||
if IsDartSassV2() {
|
||||
if IsDartSassGeV2() {
|
||||
dartSassPath = "github.com/sass/dart-sass"
|
||||
}
|
||||
deps = append(deps,
|
||||
@@ -355,22 +363,15 @@ type Dependency struct {
|
||||
}
|
||||
|
||||
func dartSassVersion() godartsass.DartSassVersion {
|
||||
if DartSassBinaryName == "" {
|
||||
if DartSassBinaryName == "" || !IsDartSassGeV2() {
|
||||
return godartsass.DartSassVersion{}
|
||||
}
|
||||
if IsDartSassV2() {
|
||||
v, _ := godartsass.Version(DartSassBinaryName)
|
||||
return v
|
||||
}
|
||||
|
||||
v, _ := godartsassv1.Version(DartSassBinaryName)
|
||||
var vv godartsass.DartSassVersion
|
||||
mapstructure.WeakDecode(v, &vv)
|
||||
return vv
|
||||
v, _ := godartsass.Version(DartSassBinaryName)
|
||||
return v
|
||||
}
|
||||
|
||||
// DartSassBinaryName is the name of the Dart Sass binary to use.
|
||||
// TODO(beop) find a better place for this.
|
||||
// TODO(bep) find a better place for this.
|
||||
var DartSassBinaryName string
|
||||
|
||||
func init() {
|
||||
@@ -395,7 +396,10 @@ var (
|
||||
dartSassBinaryNamesV2 = []string{"dart-sass", "sass"}
|
||||
)
|
||||
|
||||
func IsDartSassV2() bool {
|
||||
// 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.
|
||||
return !strings.Contains(DartSassBinaryName, "embedded")
|
||||
}
|
||||
|
||||
@@ -410,19 +414,28 @@ func Deprecate(item, alternative string, version string) {
|
||||
DeprecateLevel(item, alternative, version, level)
|
||||
}
|
||||
|
||||
// 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 := deprecationLogLevelFromVersion(version)
|
||||
if level < minLevel {
|
||||
level = 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) {
|
||||
var msg string
|
||||
if level == logg.LevelError {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in Hugo %s. %s", item, version, CurrentVersion.Next().ReleaseVersion(), alternative)
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative)
|
||||
} else {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
|
||||
}
|
||||
|
||||
loggers.Log().Logger().WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf(msg)
|
||||
loggers.Log().Logger().WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf("%s", msg)
|
||||
}
|
||||
|
||||
// We ususally do about one minor version a month.
|
||||
// We usually 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 {
|
||||
@@ -430,11 +443,11 @@ func deprecationLogLevelFromVersion(ver string) logg.Level {
|
||||
to := CurrentVersion
|
||||
minorDiff := to.Minor - from.Minor
|
||||
switch {
|
||||
case minorDiff >= 12:
|
||||
// Start failing the build after about a year.
|
||||
case minorDiff >= 15:
|
||||
// Start failing the build after about 15 months.
|
||||
return logg.LevelError
|
||||
case minorDiff >= 6:
|
||||
// Start printing warnings after about six months.
|
||||
case minorDiff >= 3:
|
||||
// Start printing warnings after about 3 months.
|
||||
return logg.LevelWarn
|
||||
default:
|
||||
return logg.LevelInfo
|
||||
|
||||
@@ -57,12 +57,16 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
|
||||
c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError)
|
||||
ver := CurrentVersion
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
|
||||
ver.Minor -= 1
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
|
||||
ver.Minor -= 6
|
||||
ver.Minor -= 3
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
|
||||
ver.Minor -= 6
|
||||
ver.Minor -= 4
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
|
||||
ver.Minor -= 13
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
|
||||
|
||||
// Added just to find the threshold for where we can remove deprecated items.
|
||||
// Subtract 5 from the minor version of the first ERRORed version => 0.122.0.
|
||||
c.Assert(deprecationLogLevelFromVersion("0.127.0"), qt.Equals, logg.LevelError)
|
||||
}
|
||||
|
||||
func TestMarkupScope(t *testing.T) {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build extended
|
||||
// +build extended
|
||||
|
||||
package hugo
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !extended
|
||||
// +build !extended
|
||||
|
||||
package hugo
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 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,4 +11,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package js
|
||||
//go:build withdeploy
|
||||
|
||||
package hugo
|
||||
|
||||
var IsWithdeploy = true
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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
|
||||
@@ -152,6 +152,9 @@ func BuildVersionString() string {
|
||||
if IsExtended {
|
||||
version += "+extended"
|
||||
}
|
||||
if IsWithdeploy {
|
||||
version += "+withdeploy"
|
||||
}
|
||||
|
||||
osArch := bi.GoOS + "/" + bi.GoArch
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ package hugo
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Major: 0,
|
||||
Minor: 134,
|
||||
Minor: 144,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
Suffix: "",
|
||||
}
|
||||
|
||||
@@ -18,18 +18,19 @@ package loggers
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
)
|
||||
|
||||
// newNoColoursHandler creates a new NoColoursHandler
|
||||
func newNoColoursHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noColoursHandler {
|
||||
// newNoAnsiEscapeHandler creates a new noAnsiEscapeHandler
|
||||
func newNoAnsiEscapeHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noAnsiEscapeHandler {
|
||||
if predicate == nil {
|
||||
predicate = func(e *logg.Entry) bool { return true }
|
||||
}
|
||||
return &noColoursHandler{
|
||||
return &noAnsiEscapeHandler{
|
||||
noLevelPrefix: noLevelPrefix,
|
||||
outWriter: outWriter,
|
||||
errWriter: errWriter,
|
||||
@@ -37,15 +38,15 @@ func newNoColoursHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, pre
|
||||
}
|
||||
}
|
||||
|
||||
type noColoursHandler struct {
|
||||
type noAnsiEscapeHandler struct {
|
||||
mu sync.Mutex
|
||||
outWriter io.Writer // Defaults to os.Stdout.
|
||||
errWriter io.Writer // Defaults to os.Stderr.
|
||||
outWriter io.Writer
|
||||
errWriter io.Writer
|
||||
predicate func(*logg.Entry) bool
|
||||
noLevelPrefix bool
|
||||
}
|
||||
|
||||
func (h *noColoursHandler) HandleLog(e *logg.Entry) error {
|
||||
func (h *noAnsiEscapeHandler) HandleLog(e *logg.Entry) error {
|
||||
if !h.predicate(e) {
|
||||
return nil
|
||||
}
|
||||
@@ -71,10 +72,12 @@ func (h *noColoursHandler) HandleLog(e *logg.Entry) error {
|
||||
prefix = prefix + ": "
|
||||
}
|
||||
|
||||
msg := stripANSI(e.Message)
|
||||
|
||||
if h.noLevelPrefix {
|
||||
fmt.Fprintf(w, "%s%s", prefix, e.Message)
|
||||
fmt.Fprintf(w, "%s%s", prefix, msg)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s %s%s", levelString[e.Level], prefix, e.Message)
|
||||
fmt.Fprintf(w, "%s %s%s", levelString[e.Level], prefix, msg)
|
||||
}
|
||||
|
||||
for _, field := range e.Fields {
|
||||
@@ -88,3 +91,10 @@ func (h *noColoursHandler) 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, "")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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")
|
||||
}
|
||||
+26
-22
@@ -38,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
|
||||
@@ -48,21 +48,22 @@ 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.Stdout
|
||||
if opts.StdErr == nil {
|
||||
opts.StdErr = os.Stderr
|
||||
}
|
||||
|
||||
if opts.Level == 0 {
|
||||
opts.Level = logg.LevelWarn
|
||||
}
|
||||
|
||||
var logHandler logg.Handler
|
||||
if terminal.PrintANSIColors(os.Stdout) {
|
||||
logHandler = newDefaultHandler(opts.Stdout, opts.Stderr)
|
||||
if terminal.PrintANSIColors(os.Stderr) {
|
||||
logHandler = newDefaultHandler(opts.StdErr, opts.StdErr)
|
||||
} else {
|
||||
logHandler = newNoColoursHandler(opts.Stdout, opts.Stderr, false, nil)
|
||||
logHandler = newNoAnsiEscapeHandler(opts.StdErr, opts.StdErr, false, nil)
|
||||
}
|
||||
|
||||
errorsw := &strings.Builder{}
|
||||
@@ -95,7 +96,7 @@ func New(opts Options) Logger {
|
||||
}
|
||||
|
||||
if opts.StoreErrors {
|
||||
h := newNoColoursHandler(io.Discard, errorsw, true, func(e *logg.Entry) bool {
|
||||
h := newNoAnsiEscapeHandler(io.Discard, errorsw, true, func(e *logg.Entry) bool {
|
||||
return e.Level >= logg.LevelError
|
||||
})
|
||||
|
||||
@@ -137,7 +138,8 @@ func New(opts Options) Logger {
|
||||
logCounters: logCounters,
|
||||
errors: errorsw,
|
||||
reset: reset,
|
||||
out: opts.Stdout,
|
||||
stdOut: opts.StdOut,
|
||||
stdErr: opts.StdErr,
|
||||
level: opts.Level,
|
||||
logger: logger,
|
||||
tracel: l.WithLevel(logg.LevelTrace),
|
||||
@@ -153,8 +155,6 @@ func NewDefault() Logger {
|
||||
opts := Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: logg.LevelWarn,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
}
|
||||
return New(opts)
|
||||
}
|
||||
@@ -163,8 +163,6 @@ func NewTrace() Logger {
|
||||
opts := Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: logg.LevelTrace,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
}
|
||||
return New(opts)
|
||||
}
|
||||
@@ -189,7 +187,8 @@ type Logger interface {
|
||||
Level() logg.Level
|
||||
LoggCount(logg.Level) int
|
||||
Logger() logg.Logger
|
||||
Out() io.Writer
|
||||
StdOut() io.Writer
|
||||
StdErr() io.Writer
|
||||
Printf(format string, v ...any)
|
||||
Println(v ...any)
|
||||
PrintTimerIfDelayed(start time.Time, name string)
|
||||
@@ -207,7 +206,8 @@ type logAdapter struct {
|
||||
logCounters *logLevelCounter
|
||||
errors *strings.Builder
|
||||
reset func()
|
||||
out io.Writer
|
||||
stdOut io.Writer
|
||||
stdErr io.Writer
|
||||
level logg.Level
|
||||
logger logg.Logger
|
||||
tracel logg.LevelLogger
|
||||
@@ -259,8 +259,12 @@ func (l *logAdapter) Logger() logg.Logger {
|
||||
return l.logger
|
||||
}
|
||||
|
||||
func (l *logAdapter) Out() io.Writer {
|
||||
return l.out
|
||||
func (l *logAdapter) StdOut() io.Writer {
|
||||
return l.stdOut
|
||||
}
|
||||
|
||||
func (l *logAdapter) StdErr() io.Writer {
|
||||
return l.stdErr
|
||||
}
|
||||
|
||||
// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
|
||||
@@ -271,7 +275,7 @@ func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
if milli < 500 {
|
||||
return
|
||||
}
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
fmt.Fprintf(l.stdErr, "%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Printf(format string, v ...any) {
|
||||
@@ -279,11 +283,11 @@ func (l *logAdapter) Printf(format string, v ...any) {
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format += "\n"
|
||||
}
|
||||
fmt.Fprintf(l.out, format, v...)
|
||||
fmt.Fprintf(l.stdOut, format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Println(v ...any) {
|
||||
fmt.Fprintln(l.out, v...)
|
||||
fmt.Fprintln(l.stdOut, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Reset() {
|
||||
|
||||
@@ -31,8 +31,8 @@ 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)
|
||||
@@ -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,8 +131,8 @@ 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)
|
||||
|
||||
@@ -21,7 +21,15 @@ import (
|
||||
"github.com/bep/logg"
|
||||
)
|
||||
|
||||
func InitGlobalLogger(level logg.Level, panicOnWarnings bool) {
|
||||
// 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) {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
var logHookLast func(e *logg.Entry) error
|
||||
@@ -50,5 +58,5 @@ func Log() Logger {
|
||||
var log Logger
|
||||
|
||||
func init() {
|
||||
InitGlobalLogger(logg.LevelWarn, false)
|
||||
initGlobalLogger(logg.LevelWarn, false)
|
||||
}
|
||||
|
||||
+55
-6
@@ -13,11 +13,14 @@
|
||||
|
||||
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
|
||||
m map[K]T
|
||||
hasBeenInitialized bool
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -34,11 +37,16 @@ func (c *Cache[K, T]) Get(key K) (T, bool) {
|
||||
return zero, false
|
||||
}
|
||||
c.RLock()
|
||||
v, found := c.m[key]
|
||||
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]
|
||||
return v, found
|
||||
}
|
||||
|
||||
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
||||
func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||
c.RLock()
|
||||
@@ -61,19 +69,58 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// 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.m[key] = value
|
||||
c.set(key, value)
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
func (c *Cache[K, T]) set(key K, value T) {
|
||||
c.m[key] = value
|
||||
}
|
||||
|
||||
// ForEeach calls the given function for each key/value pair in the cache.
|
||||
func (c *Cache[K, T]) ForEeach(f func(K, T)) {
|
||||
// If the function returns false, the iteration stops.
|
||||
func (c *Cache[K, T]) ForEeach(f func(K, T) bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
for k, v := range c.m {
|
||||
f(k, v)
|
||||
if !f(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +128,7 @@ 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
|
||||
}
|
||||
@@ -94,6 +142,7 @@ func (c *Cache[K, T]) Len() int {
|
||||
func (c *Cache[K, T]) Reset() {
|
||||
c.Lock()
|
||||
c.m = make(map[K]T)
|
||||
c.hasBeenInitialized = false
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -73,10 +73,14 @@ func TestPrepareParams(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
// PrepareParams modifies input.
|
||||
prepareClone := PrepareParamsClone(test.input)
|
||||
PrepareParams(test.input)
|
||||
if !reflect.DeepEqual(test.expected, test.input) {
|
||||
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expected, prepareClone) {
|
||||
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, prepareClone)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"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
|
||||
}
|
||||
|
||||
// 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 = append(m.keys[:i], m.keys[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)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestOrdered(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := NewOrdered[string, int]()
|
||||
m.Set("a", 1)
|
||||
m.Set("b", 2)
|
||||
m.Set("c", 3)
|
||||
|
||||
c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(m.Values(), qt.DeepEquals, []int{1, 2, 3})
|
||||
|
||||
v, found := m.Get("b")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 2)
|
||||
|
||||
m.Set("b", 22)
|
||||
c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(m.Values(), qt.DeepEquals, []int{1, 22, 3})
|
||||
|
||||
m.Delete("b")
|
||||
|
||||
c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "c"})
|
||||
c.Assert(m.Values(), qt.DeepEquals, []int{1, 3})
|
||||
}
|
||||
|
||||
func TestOrderedHash(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := NewOrdered[string, int]()
|
||||
m.Set("a", 1)
|
||||
m.Set("b", 2)
|
||||
m.Set("c", 3)
|
||||
|
||||
h1, err := m.Hash()
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
m.Set("d", 4)
|
||||
|
||||
h2, err := m.Hash()
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(h1, qt.Not(qt.Equals), h2)
|
||||
|
||||
m = NewOrdered[string, int]()
|
||||
m.Set("b", 2)
|
||||
m.Set("a", 1)
|
||||
m.Set("c", 3)
|
||||
|
||||
h3, err := m.Hash()
|
||||
c.Assert(err, qt.IsNil)
|
||||
// Order does not matter.
|
||||
c.Assert(h1, qt.Equals, h3)
|
||||
}
|
||||
|
||||
func TestOrderedNil(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var m *Ordered[string, int]
|
||||
|
||||
m.Set("a", 1)
|
||||
c.Assert(m.Keys(), qt.IsNil)
|
||||
c.Assert(m.Values(), qt.IsNil)
|
||||
v, found := m.Get("a")
|
||||
c.Assert(found, qt.Equals, false)
|
||||
c.Assert(v, qt.Equals, 0)
|
||||
m.Delete("a")
|
||||
var b bool
|
||||
m.Range(func(k string, v int) bool {
|
||||
b = true
|
||||
return true
|
||||
})
|
||||
c.Assert(b, qt.Equals, false)
|
||||
c.Assert(m.Len(), qt.Equals, 0)
|
||||
c.Assert(m.Clone(), qt.IsNil)
|
||||
h, err := m.Hash()
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(h, qt.Equals, uint64(0))
|
||||
}
|
||||
+40
-1
@@ -303,7 +303,7 @@ func toMergeStrategy(v any) ParamsMergeStrategy {
|
||||
}
|
||||
|
||||
// PrepareParams
|
||||
// * makes all the keys in the given map lower cased and will do so
|
||||
// * makes all the keys in the given map lower cased and will do so recursively.
|
||||
// * This will modify the map given.
|
||||
// * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params.
|
||||
// * Any _merge value will be converted to proper type and value.
|
||||
@@ -343,3 +343,42 @@ func PrepareParams(m Params) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareParamsClone is like PrepareParams, but it does not modify the input.
|
||||
func PrepareParamsClone(m Params) Params {
|
||||
m2 := make(Params)
|
||||
for k, v := range m {
|
||||
var retyped bool
|
||||
lKey := strings.ToLower(k)
|
||||
if lKey == MergeStrategyKey {
|
||||
v = toMergeStrategy(v)
|
||||
retyped = true
|
||||
} else {
|
||||
switch vv := v.(type) {
|
||||
case map[any]any:
|
||||
var p Params = cast.ToStringMap(v)
|
||||
v = PrepareParamsClone(p)
|
||||
retyped = true
|
||||
case map[string]any:
|
||||
var p Params = v.(map[string]any)
|
||||
v = PrepareParamsClone(p)
|
||||
retyped = true
|
||||
case map[string]string:
|
||||
p := make(Params)
|
||||
for k, v := range vv {
|
||||
p[k] = v
|
||||
}
|
||||
v = p
|
||||
PrepareParams(p)
|
||||
retyped = true
|
||||
}
|
||||
}
|
||||
|
||||
if retyped || k != lKey {
|
||||
m2[lKey] = v
|
||||
} else {
|
||||
m2[k] = v
|
||||
}
|
||||
}
|
||||
return m2
|
||||
}
|
||||
|
||||
+7
-20
@@ -22,31 +22,18 @@ import (
|
||||
"github.com/gohugoio/hugo/common/math"
|
||||
)
|
||||
|
||||
// Scratch is a writable context used for stateful operations in Page/Node rendering.
|
||||
type StoreProvider interface {
|
||||
// Store returns a Scratch that can be used to store temporary state.
|
||||
// Store is not reset on server rebuilds.
|
||||
Store() *Scratch
|
||||
}
|
||||
|
||||
// Scratch is a writable context used for stateful build operations
|
||||
type Scratch struct {
|
||||
values map[string]any
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Scratcher provides a scratching service.
|
||||
type Scratcher interface {
|
||||
// Scratch returns a "scratch pad" that can be used to store state.
|
||||
Scratch() *Scratch
|
||||
}
|
||||
|
||||
type scratcher struct {
|
||||
s *Scratch
|
||||
}
|
||||
|
||||
func (s scratcher) Scratch() *Scratch {
|
||||
return s.s
|
||||
}
|
||||
|
||||
// NewScratcher creates a new Scratcher.
|
||||
func NewScratcher() Scratcher {
|
||||
return scratcher{s: NewScratch()}
|
||||
}
|
||||
|
||||
// Add will, for single values, add (using the + operator) the addend to the existing addend (if found).
|
||||
// Supports numeric values and strings.
|
||||
//
|
||||
|
||||
@@ -185,7 +185,7 @@ func TestScratchSetInMap(t *testing.T) {
|
||||
scratch.SetInMap("key", "zyx", "Zyx")
|
||||
scratch.SetInMap("key", "abc", "Abc (updated)")
|
||||
scratch.SetInMap("key", "def", "Def")
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []any{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"})
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, any([]any{"Abc (updated)", "Def", "Lux", "Zyx"}))
|
||||
}
|
||||
|
||||
func TestScratchDeleteInMap(t *testing.T) {
|
||||
@@ -199,7 +199,7 @@ func TestScratchDeleteInMap(t *testing.T) {
|
||||
scratch.DeleteInMap("key", "abc")
|
||||
scratch.SetInMap("key", "def", "Def")
|
||||
scratch.DeleteInMap("key", "lmn") // Do nothing
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []any{0: "Def", 1: "Lux", 2: "Zyx"})
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, any([]any{"Def", "Lux", "Zyx"}))
|
||||
}
|
||||
|
||||
func TestScratchGetSortedMapValues(t *testing.T) {
|
||||
|
||||
+22
-24
@@ -26,29 +26,32 @@ func DoArithmetic(a, b any, op rune) (any, error) {
|
||||
var ai, bi int64
|
||||
var af, bf float64
|
||||
var au, bu uint64
|
||||
var isInt, isFloat, isUint bool
|
||||
switch av.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
ai = av.Int()
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
isInt = true
|
||||
bi = bv.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
isFloat = true
|
||||
af = float64(ai) // may overflow
|
||||
ai = 0
|
||||
bf = bv.Float()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
bu = bv.Uint()
|
||||
if ai >= 0 {
|
||||
isUint = true
|
||||
au = uint64(ai)
|
||||
ai = 0
|
||||
} else {
|
||||
isInt = true
|
||||
bi = int64(bu) // may overflow
|
||||
bu = 0
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("can't apply the operator to the values")
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
isFloat = true
|
||||
af = av.Float()
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
@@ -66,17 +69,18 @@ func DoArithmetic(a, b any, op rune) (any, error) {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
bi = bv.Int()
|
||||
if bi >= 0 {
|
||||
isUint = true
|
||||
bu = uint64(bi)
|
||||
bi = 0
|
||||
} else {
|
||||
isInt = true
|
||||
ai = int64(au) // may overflow
|
||||
au = 0
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
isFloat = true
|
||||
af = float64(au) // may overflow
|
||||
au = 0
|
||||
bf = bv.Float()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
isUint = true
|
||||
bu = bv.Uint()
|
||||
default:
|
||||
return nil, errors.New("can't apply the operator to the values")
|
||||
@@ -94,38 +98,32 @@ func DoArithmetic(a, b any, op rune) (any, error) {
|
||||
|
||||
switch op {
|
||||
case '+':
|
||||
if ai != 0 || bi != 0 {
|
||||
if isInt {
|
||||
return ai + bi, nil
|
||||
} else if af != 0 || bf != 0 {
|
||||
} else if isFloat {
|
||||
return af + bf, nil
|
||||
} else if au != 0 || bu != 0 {
|
||||
return au + bu, nil
|
||||
}
|
||||
return 0, nil
|
||||
return au + bu, nil
|
||||
case '-':
|
||||
if ai != 0 || bi != 0 {
|
||||
if isInt {
|
||||
return ai - bi, nil
|
||||
} else if af != 0 || bf != 0 {
|
||||
} else if isFloat {
|
||||
return af - bf, nil
|
||||
} else if au != 0 || bu != 0 {
|
||||
return au - bu, nil
|
||||
}
|
||||
return 0, nil
|
||||
return au - bu, nil
|
||||
case '*':
|
||||
if ai != 0 || bi != 0 {
|
||||
if isInt {
|
||||
return ai * bi, nil
|
||||
} else if af != 0 || bf != 0 {
|
||||
} else if isFloat {
|
||||
return af * bf, nil
|
||||
} else if au != 0 || bu != 0 {
|
||||
return au * bu, nil
|
||||
}
|
||||
return 0, nil
|
||||
return au * bu, nil
|
||||
case '/':
|
||||
if bi != 0 {
|
||||
if isInt && bi != 0 {
|
||||
return ai / bi, nil
|
||||
} else if bf != 0 {
|
||||
} else if isFloat && bf != 0 {
|
||||
return af / bf, nil
|
||||
} else if bu != 0 {
|
||||
} else if isUint && bu != 0 {
|
||||
return au / bu, nil
|
||||
}
|
||||
return nil, errors.New("can't divide the value by 0")
|
||||
|
||||
@@ -30,10 +30,12 @@ func TestDoArithmetic(t *testing.T) {
|
||||
expect any
|
||||
}{
|
||||
{3, 2, '+', int64(5)},
|
||||
{0, 0, '+', int64(0)},
|
||||
{3, 2, '-', int64(1)},
|
||||
{3, 2, '*', int64(6)},
|
||||
{3, 2, '/', int64(1)},
|
||||
{3.0, 2, '+', float64(5)},
|
||||
{0.0, 0, '+', float64(0.0)},
|
||||
{3.0, 2, '-', float64(1)},
|
||||
{3.0, 2, '*', float64(6)},
|
||||
{3.0, 2, '/', float64(1.5)},
|
||||
@@ -42,18 +44,22 @@ func TestDoArithmetic(t *testing.T) {
|
||||
{3, 2.0, '*', float64(6)},
|
||||
{3, 2.0, '/', float64(1.5)},
|
||||
{3.0, 2.0, '+', float64(5)},
|
||||
{0.0, 0.0, '+', float64(0.0)},
|
||||
{3.0, 2.0, '-', float64(1)},
|
||||
{3.0, 2.0, '*', float64(6)},
|
||||
{3.0, 2.0, '/', float64(1.5)},
|
||||
{uint(3), uint(2), '+', uint64(5)},
|
||||
{uint(0), uint(0), '+', uint64(0)},
|
||||
{uint(3), uint(2), '-', uint64(1)},
|
||||
{uint(3), uint(2), '*', uint64(6)},
|
||||
{uint(3), uint(2), '/', uint64(1)},
|
||||
{uint(3), 2, '+', uint64(5)},
|
||||
{uint(0), 0, '+', uint64(0)},
|
||||
{uint(3), 2, '-', uint64(1)},
|
||||
{uint(3), 2, '*', uint64(6)},
|
||||
{uint(3), 2, '/', uint64(1)},
|
||||
{3, uint(2), '+', uint64(5)},
|
||||
{0, uint(0), '+', uint64(0)},
|
||||
{3, uint(2), '-', uint64(1)},
|
||||
{3, uint(2), '*', uint64(6)},
|
||||
{3, uint(2), '/', uint64(1)},
|
||||
@@ -66,16 +72,15 @@ func TestDoArithmetic(t *testing.T) {
|
||||
{-3, uint(2), '*', int64(-6)},
|
||||
{-3, uint(2), '/', int64(-1)},
|
||||
{uint(3), 2.0, '+', float64(5)},
|
||||
{uint(0), 0.0, '+', float64(0)},
|
||||
{uint(3), 2.0, '-', float64(1)},
|
||||
{uint(3), 2.0, '*', float64(6)},
|
||||
{uint(3), 2.0, '/', float64(1.5)},
|
||||
{3.0, uint(2), '+', float64(5)},
|
||||
{0.0, uint(0), '+', float64(0)},
|
||||
{3.0, uint(2), '-', float64(1)},
|
||||
{3.0, uint(2), '*', float64(6)},
|
||||
{3.0, uint(2), '/', float64(1.5)},
|
||||
{0, 0, '+', 0},
|
||||
{0, 0, '-', 0},
|
||||
{0, 0, '*', 0},
|
||||
{"foo", "bar", '+', "foobar"},
|
||||
{3, 0, '/', false},
|
||||
{3.0, 0, '/', false},
|
||||
|
||||
+104
-26
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 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.
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -159,31 +160,6 @@ func Uglify(in string) string {
|
||||
return path.Clean(in)
|
||||
}
|
||||
|
||||
// UrlToFilename converts the URL s to a filename.
|
||||
// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned.
|
||||
func UrlToFilename(s string) (string, bool) {
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
return filepath.FromSlash(s), false
|
||||
}
|
||||
|
||||
p := u.Path
|
||||
|
||||
if p == "" {
|
||||
p, _ = url.QueryUnescape(u.Opaque)
|
||||
return filepath.FromSlash(p), true
|
||||
}
|
||||
|
||||
p = filepath.FromSlash(p)
|
||||
|
||||
if u.Host != "" {
|
||||
// C:\data\file.txt
|
||||
p = strings.ToUpper(u.Host) + ":" + p
|
||||
}
|
||||
|
||||
return p, true
|
||||
}
|
||||
|
||||
// URLEscape escapes unicode letters.
|
||||
func URLEscape(uri string) string {
|
||||
// escape unicode letters
|
||||
@@ -193,3 +169,105 @@ func URLEscape(uri string) string {
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// TrimExt trims the extension from a path..
|
||||
func TrimExt(in string) string {
|
||||
return strings.TrimSuffix(in, path.Ext(in))
|
||||
}
|
||||
|
||||
// From https://github.com/golang/go/blob/e0c76d95abfc1621259864adb3d101cf6f1f90fc/src/cmd/go/internal/web/url.go#L45
|
||||
func UrlFromFilename(filename string) (*url.URL, error) {
|
||||
if !filepath.IsAbs(filename) {
|
||||
return nil, fmt.Errorf("filepath must be absolute")
|
||||
}
|
||||
|
||||
// If filename has a Windows volume name, convert the volume to a host and prefix
|
||||
// per https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/.
|
||||
if vol := filepath.VolumeName(filename); vol != "" {
|
||||
if strings.HasPrefix(vol, `\\`) {
|
||||
filename = filepath.ToSlash(filename[2:])
|
||||
i := strings.IndexByte(filename, '/')
|
||||
|
||||
if i < 0 {
|
||||
// A degenerate case.
|
||||
// \\host.example.com (without a share name)
|
||||
// becomes
|
||||
// file://host.example.com/
|
||||
return &url.URL{
|
||||
Scheme: "file",
|
||||
Host: filename,
|
||||
Path: "/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// \\host.example.com\Share\path\to\file
|
||||
// becomes
|
||||
// file://host.example.com/Share/path/to/file
|
||||
return &url.URL{
|
||||
Scheme: "file",
|
||||
Host: filename[:i],
|
||||
Path: filepath.ToSlash(filename[i:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// C:\path\to\file
|
||||
// becomes
|
||||
// file:///C:/path/to/file
|
||||
return &url.URL{
|
||||
Scheme: "file",
|
||||
Path: "/" + filepath.ToSlash(filename),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// /path/to/file
|
||||
// becomes
|
||||
// file:///path/to/file
|
||||
return &url.URL{
|
||||
Scheme: "file",
|
||||
Path: filepath.ToSlash(filename),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UrlStringToFilename converts the URL s to a filename.
|
||||
// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned.
|
||||
func UrlStringToFilename(s string) (string, bool) {
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
return filepath.FromSlash(s), false
|
||||
}
|
||||
|
||||
p := u.Path
|
||||
|
||||
if p == "" {
|
||||
p, _ = url.QueryUnescape(u.Opaque)
|
||||
return filepath.FromSlash(p), false
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
return p, true
|
||||
}
|
||||
|
||||
if len(p) == 0 || p[0] != '/' {
|
||||
return filepath.FromSlash(p), false
|
||||
}
|
||||
|
||||
p = filepath.FromSlash(p)
|
||||
|
||||
if len(u.Host) == 1 {
|
||||
// file://c/Users/...
|
||||
return strings.ToUpper(u.Host) + ":" + p, true
|
||||
}
|
||||
|
||||
if u.Host != "" && u.Host != "localhost" {
|
||||
if filepath.VolumeName(u.Host) != "" {
|
||||
return "", false
|
||||
}
|
||||
return `\\` + u.Host + p, true
|
||||
}
|
||||
|
||||
if vol := filepath.VolumeName(p[1:]); vol == "" || strings.HasPrefix(vol, `\\`) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return p[1:], true
|
||||
}
|
||||
|
||||
@@ -19,6 +19,13 @@ type Closer interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// CloserFunc is a convenience type to create a Closer from a function.
|
||||
type CloserFunc func() error
|
||||
|
||||
func (f CloserFunc) Close() error {
|
||||
return f()
|
||||
}
|
||||
|
||||
type CloseAdder interface {
|
||||
Add(Closer)
|
||||
}
|
||||
|
||||
@@ -18,24 +18,24 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// EvictingStringQueue is a queue which automatically evicts elements from the head of
|
||||
// EvictingQueue is a queue which automatically evicts elements from the head of
|
||||
// the queue when attempting to add new elements onto the queue and it is full.
|
||||
// This queue orders elements LIFO (last-in-first-out). It throws away duplicates.
|
||||
// Note: This queue currently does not contain any remove (poll etc.) methods.
|
||||
type EvictingStringQueue struct {
|
||||
type EvictingQueue[T comparable] struct {
|
||||
size int
|
||||
vals []string
|
||||
set map[string]bool
|
||||
vals []T
|
||||
set map[T]bool
|
||||
mu sync.Mutex
|
||||
zero T
|
||||
}
|
||||
|
||||
// NewEvictingStringQueue creates a new queue with the given size.
|
||||
func NewEvictingStringQueue(size int) *EvictingStringQueue {
|
||||
return &EvictingStringQueue{size: size, set: make(map[string]bool)}
|
||||
// NewEvictingQueue creates a new queue with the given size.
|
||||
func NewEvictingQueue[T comparable](size int) *EvictingQueue[T] {
|
||||
return &EvictingQueue[T]{size: size, set: make(map[T]bool)}
|
||||
}
|
||||
|
||||
// Add adds a new string to the tail of the queue if it's not already there.
|
||||
func (q *EvictingStringQueue) Add(v string) *EvictingStringQueue {
|
||||
func (q *EvictingQueue[T]) Add(v T) *EvictingQueue[T] {
|
||||
q.mu.Lock()
|
||||
if q.set[v] {
|
||||
q.mu.Unlock()
|
||||
@@ -54,7 +54,7 @@ func (q *EvictingStringQueue) Add(v string) *EvictingStringQueue {
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *EvictingStringQueue) Len() int {
|
||||
func (q *EvictingQueue[T]) Len() int {
|
||||
if q == nil {
|
||||
return 0
|
||||
}
|
||||
@@ -64,19 +64,22 @@ func (q *EvictingStringQueue) Len() int {
|
||||
}
|
||||
|
||||
// Contains returns whether the queue contains v.
|
||||
func (q *EvictingStringQueue) Contains(v string) bool {
|
||||
func (q *EvictingQueue[T]) Contains(v T) bool {
|
||||
if q == nil {
|
||||
return false
|
||||
}
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return q.set[v]
|
||||
}
|
||||
|
||||
// Peek looks at the last element added to the queue.
|
||||
func (q *EvictingStringQueue) Peek() string {
|
||||
func (q *EvictingQueue[T]) Peek() T {
|
||||
q.mu.Lock()
|
||||
l := len(q.vals)
|
||||
if l == 0 {
|
||||
q.mu.Unlock()
|
||||
return ""
|
||||
return q.zero
|
||||
}
|
||||
elem := q.vals[l-1]
|
||||
q.mu.Unlock()
|
||||
@@ -84,9 +87,12 @@ func (q *EvictingStringQueue) Peek() string {
|
||||
}
|
||||
|
||||
// PeekAll looks at all the elements in the queue, with the newest first.
|
||||
func (q *EvictingStringQueue) PeekAll() []string {
|
||||
func (q *EvictingQueue[T]) PeekAll() []T {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
q.mu.Lock()
|
||||
vals := make([]string, len(q.vals))
|
||||
vals := make([]T, len(q.vals))
|
||||
copy(vals, q.vals)
|
||||
q.mu.Unlock()
|
||||
for i, j := 0, len(vals)-1; i < j; i, j = i+1, j-1 {
|
||||
@@ -96,9 +102,9 @@ func (q *EvictingStringQueue) PeekAll() []string {
|
||||
}
|
||||
|
||||
// PeekAllSet returns PeekAll as a set.
|
||||
func (q *EvictingStringQueue) PeekAllSet() map[string]bool {
|
||||
func (q *EvictingQueue[T]) PeekAllSet() map[T]bool {
|
||||
all := q.PeekAll()
|
||||
set := make(map[string]bool)
|
||||
set := make(map[T]bool)
|
||||
for _, v := range all {
|
||||
set[v] = true
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
func TestEvictingStringQueue(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
queue := NewEvictingStringQueue(3)
|
||||
queue := NewEvictingQueue[string](3)
|
||||
|
||||
c.Assert(queue.Peek(), qt.Equals, "")
|
||||
queue.Add("a")
|
||||
@@ -53,7 +53,7 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
val := "someval"
|
||||
|
||||
queue := NewEvictingStringQueue(3)
|
||||
queue := NewEvictingQueue[string](3)
|
||||
|
||||
for j := 0; j < 100; j++ {
|
||||
wg.Add(1)
|
||||
|
||||
@@ -13,8 +13,24 @@
|
||||
|
||||
package hstring
|
||||
|
||||
type RenderedString string
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
func (s RenderedString) String() string {
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
var _ types.PrintableValueProvider = HTML("")
|
||||
|
||||
// HTML is a string that represents rendered HTML.
|
||||
// When printed in templates it will be rendered as template.HTML and considered safe so no need to pipe it into `safeHTML`.
|
||||
// This type was introduced as a wasy to prevent a common case of inifinite recursion in the template rendering
|
||||
// when the `linkify` option is enabled with a common (wrong) construct like `{{ .Text | .Page.RenderString }}` in a hook template.
|
||||
type HTML string
|
||||
|
||||
func (s HTML) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s HTML) PrintableValue() any {
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ func TestRenderedString(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
// Validate that it will behave like a string in Hugo settings.
|
||||
c.Assert(cast.ToString(RenderedString("Hugo")), qt.Equals, "Hugo")
|
||||
c.Assert(template.HTML(RenderedString("Hugo")), qt.Equals, template.HTML("Hugo"))
|
||||
c.Assert(cast.ToString(HTML("Hugo")), qt.Equals, "Hugo")
|
||||
c.Assert(template.HTML(HTML("Hugo")), qt.Equals, template.HTML("Hugo"))
|
||||
}
|
||||
|
||||
@@ -133,22 +133,3 @@ func NewBool(b bool) *bool {
|
||||
type PrintableValueProvider interface {
|
||||
PrintableValue() any
|
||||
}
|
||||
|
||||
var _ PrintableValueProvider = Result[any]{}
|
||||
|
||||
// Result is a generic result type.
|
||||
type Result[T any] struct {
|
||||
// The result value.
|
||||
Value T
|
||||
|
||||
// The error value.
|
||||
Err error
|
||||
}
|
||||
|
||||
// PrintableValue returns the value or panics if there is an error.
|
||||
func (r Result[T]) PrintableValue() any {
|
||||
if r.Err != nil {
|
||||
panic(r.Err)
|
||||
}
|
||||
return r.Value
|
||||
}
|
||||
|
||||
@@ -128,6 +128,9 @@ type Config struct {
|
||||
// <docsmeta>{"identifiers": ["markup"] }</docsmeta>
|
||||
Markup markup_config.Config `mapstructure:"-"`
|
||||
|
||||
// ContentTypes are the media types that's considered content in Hugo.
|
||||
ContentTypes *config.ConfigNamespace[map[string]media.ContentTypeConfig, media.ContentTypes] `mapstructure:"-"`
|
||||
|
||||
// The mediatypes configuration section maps the MIME type (a string) to a configuration object for that type.
|
||||
// <docsmeta>{"identifiers": ["mediatypes"], "refs": ["types:media:type"] }</docsmeta>
|
||||
MediaTypes *config.ConfigNamespace[map[string]media.MediaTypeConfig, media.Types] `mapstructure:"-"`
|
||||
@@ -143,7 +146,7 @@ type Config struct {
|
||||
|
||||
// The cascade configuration section contains the top level front matter cascade configuration options,
|
||||
// a slice of page matcher and params to apply to those pages.
|
||||
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, map[page.PageMatcher]maps.Params] `mapstructure:"-"`
|
||||
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, *maps.Ordered[page.PageMatcher, maps.Params]] `mapstructure:"-"`
|
||||
|
||||
// The segments defines segments for the site. Used for partial/segmented builds.
|
||||
Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, segments.Segments] `mapstructure:"-"`
|
||||
@@ -301,6 +304,18 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
}
|
||||
}
|
||||
|
||||
defaultOutputFormat := outputFormats[0]
|
||||
c.DefaultOutputFormat = strings.ToLower(c.DefaultOutputFormat)
|
||||
if c.DefaultOutputFormat != "" {
|
||||
f, found := outputFormats.GetByName(c.DefaultOutputFormat)
|
||||
if !found {
|
||||
return fmt.Errorf("unknown default output format %q", c.DefaultOutputFormat)
|
||||
}
|
||||
defaultOutputFormat = f
|
||||
} else {
|
||||
c.DefaultOutputFormat = defaultOutputFormat.Name
|
||||
}
|
||||
|
||||
disabledLangs := make(map[string]bool)
|
||||
for _, lang := range c.DisableLanguages {
|
||||
disabledLangs[lang] = true
|
||||
@@ -390,23 +405,54 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
c.Pagination.Path = c.PaginatePath
|
||||
}
|
||||
|
||||
// Legacy privacy values.
|
||||
if c.Privacy.Twitter.Disable {
|
||||
hugo.Deprecate("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0")
|
||||
c.Privacy.X.Disable = c.Privacy.Twitter.Disable
|
||||
}
|
||||
|
||||
if c.Privacy.Twitter.EnableDNT {
|
||||
hugo.Deprecate("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0")
|
||||
c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT
|
||||
}
|
||||
|
||||
if c.Privacy.Twitter.Simple {
|
||||
hugo.Deprecate("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0")
|
||||
c.Privacy.X.Simple = c.Privacy.Twitter.Simple
|
||||
}
|
||||
|
||||
// Legacy services values.
|
||||
if c.Services.Twitter.DisableInlineCSS {
|
||||
hugo.Deprecate("site config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0")
|
||||
c.Services.X.DisableInlineCSS = c.Services.Twitter.DisableInlineCSS
|
||||
}
|
||||
|
||||
// Legacy permalink tokens
|
||||
vs := fmt.Sprintf("%v", c.Permalinks)
|
||||
if strings.Contains(vs, ":filename") {
|
||||
hugo.Deprecate("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0")
|
||||
}
|
||||
if strings.Contains(vs, ":slugorfilename") {
|
||||
hugo.Deprecate("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0")
|
||||
}
|
||||
|
||||
c.C = &ConfigCompiled{
|
||||
Timeout: timeout,
|
||||
BaseURL: baseURL,
|
||||
BaseURLLiveReload: baseURL,
|
||||
DisabledKinds: disabledKinds,
|
||||
DisabledLanguages: disabledLangs,
|
||||
IgnoredLogs: ignoredLogIDs,
|
||||
KindOutputFormats: kindOutputFormats,
|
||||
ContentTypes: media.DefaultContentTypes.FromTypes(c.MediaTypes.Config),
|
||||
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
|
||||
IsUglyURLSection: isUglyURL,
|
||||
IgnoreFile: ignoreFile,
|
||||
SegmentFilter: c.Segments.Config.Get(func(s string) { logger.Warnf("Render segment %q not found in configuration", s) }, c.RootConfig.RenderSegments...),
|
||||
MainSections: c.MainSections,
|
||||
Clock: clock,
|
||||
HTTPCache: httpCache,
|
||||
transientErr: transientErr,
|
||||
Timeout: timeout,
|
||||
BaseURL: baseURL,
|
||||
BaseURLLiveReload: baseURL,
|
||||
DisabledKinds: disabledKinds,
|
||||
DisabledLanguages: disabledLangs,
|
||||
IgnoredLogs: ignoredLogIDs,
|
||||
KindOutputFormats: kindOutputFormats,
|
||||
DefaultOutputFormat: defaultOutputFormat,
|
||||
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
|
||||
IsUglyURLSection: isUglyURL,
|
||||
IgnoreFile: ignoreFile,
|
||||
SegmentFilter: c.Segments.Config.Get(func(s string) { logger.Warnf("Render segment %q not found in configuration", s) }, c.RootConfig.RenderSegments...),
|
||||
MainSections: c.MainSections,
|
||||
Clock: clock,
|
||||
HTTPCache: httpCache,
|
||||
transientErr: transientErr,
|
||||
}
|
||||
|
||||
for _, s := range allDecoderSetups {
|
||||
@@ -430,22 +476,22 @@ func (c *Config) IsLangDisabled(lang string) bool {
|
||||
|
||||
// ConfigCompiled holds values and functions that are derived from the config.
|
||||
type ConfigCompiled struct {
|
||||
Timeout time.Duration
|
||||
BaseURL urls.BaseURL
|
||||
BaseURLLiveReload urls.BaseURL
|
||||
ServerInterface string
|
||||
KindOutputFormats map[string]output.Formats
|
||||
ContentTypes media.ContentTypes
|
||||
DisabledKinds map[string]bool
|
||||
DisabledLanguages map[string]bool
|
||||
IgnoredLogs map[string]bool
|
||||
CreateTitle func(s string) string
|
||||
IsUglyURLSection func(section string) bool
|
||||
IgnoreFile func(filename string) bool
|
||||
SegmentFilter segments.SegmentFilter
|
||||
MainSections []string
|
||||
Clock time.Time
|
||||
HTTPCache httpcache.ConfigCompiled
|
||||
Timeout time.Duration
|
||||
BaseURL urls.BaseURL
|
||||
BaseURLLiveReload urls.BaseURL
|
||||
ServerInterface string
|
||||
KindOutputFormats map[string]output.Formats
|
||||
DefaultOutputFormat output.Format
|
||||
DisabledKinds map[string]bool
|
||||
DisabledLanguages map[string]bool
|
||||
IgnoredLogs map[string]bool
|
||||
CreateTitle func(s string) string
|
||||
IsUglyURLSection func(section string) bool
|
||||
IgnoreFile func(filename string) bool
|
||||
SegmentFilter segments.SegmentFilter
|
||||
MainSections []string
|
||||
Clock time.Time
|
||||
HTTPCache httpcache.ConfigCompiled
|
||||
|
||||
// This is set to the last transient error found during config compilation.
|
||||
// With themes/modules we compute the configuration in multiple passes, and
|
||||
@@ -505,6 +551,13 @@ type RootConfig struct {
|
||||
// Set this to true to put all languages below their language ID.
|
||||
DefaultContentLanguageInSubdir bool
|
||||
|
||||
// The default output format to use for the site.
|
||||
// If not set, we will use the first output format.
|
||||
DefaultOutputFormat string
|
||||
|
||||
// Disable generation of redirect to the default language when DefaultContentLanguageInSubdir is enabled.
|
||||
DisableDefaultLanguageRedirect bool
|
||||
|
||||
// Disable creation of alias redirect pages.
|
||||
DisableAliases bool
|
||||
|
||||
@@ -723,15 +776,16 @@ type Configs struct {
|
||||
}
|
||||
|
||||
func (c *Configs) Validate(logger loggers.Logger) error {
|
||||
for p := range c.Base.Cascade.Config {
|
||||
c.Base.Cascade.Config.Range(func(p page.PageMatcher, params maps.Params) bool {
|
||||
page.CheckCascadePattern(logger, p)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// transientErr returns the last transient error found during config compilation.
|
||||
func (c *Configs) transientErr() error {
|
||||
for _, l := range c.LanguageConfigSlice {
|
||||
for _, l := range c.LanguageConfigMap {
|
||||
if l.C.transientErr != nil {
|
||||
return l.C.transientErr
|
||||
}
|
||||
@@ -748,7 +802,6 @@ func (c *Configs) Init() error {
|
||||
var languages langs.Languages
|
||||
defaultContentLanguage := c.Base.DefaultContentLanguage
|
||||
for k, v := range c.LanguageConfigMap {
|
||||
c.LanguageConfigSlice = append(c.LanguageConfigSlice, v)
|
||||
languageConf := v.Languages[k]
|
||||
language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
|
||||
if err != nil {
|
||||
@@ -796,7 +849,7 @@ func (c *Configs) Init() error {
|
||||
c.Languages = languages
|
||||
c.LanguagesDefaultFirst = languagesDefaultFirst
|
||||
|
||||
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.C.ContentTypes.IsContentSuffix}
|
||||
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
|
||||
|
||||
c.configLangs = make([]config.AllProvider, len(c.Languages))
|
||||
for i, l := range c.LanguagesDefaultFirst {
|
||||
@@ -809,7 +862,7 @@ func (c *Configs) Init() error {
|
||||
}
|
||||
|
||||
if len(c.Modules) == 0 {
|
||||
return errors.New("no modules loaded (ned at least the main module)")
|
||||
return errors.New("no modules loaded (need at least the main module)")
|
||||
}
|
||||
|
||||
// Apply default project mounts.
|
||||
@@ -888,30 +941,17 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
var differentRootKeys []string
|
||||
switch x := v.(type) {
|
||||
case maps.Params:
|
||||
var params maps.Params
|
||||
pv, found := x["params"]
|
||||
if found {
|
||||
params = pv.(maps.Params)
|
||||
} else {
|
||||
params = maps.Params{
|
||||
_, found := x["params"]
|
||||
if !found {
|
||||
x["params"] = maps.Params{
|
||||
maps.MergeStrategyKey: maps.ParamsMergeStrategyDeep,
|
||||
}
|
||||
x["params"] = params
|
||||
}
|
||||
|
||||
for kk, vv := range x {
|
||||
if kk == "_merge" {
|
||||
continue
|
||||
}
|
||||
if kk != maps.MergeStrategyKey && !configLanguageKeys[kk] {
|
||||
// This should have been placed below params.
|
||||
// We accidentally allowed it in the past, so we need to support it a little longer,
|
||||
// But log a warning.
|
||||
if _, found := params[kk]; !found {
|
||||
hugo.Deprecate(fmt.Sprintf("config: languages.%s.%s: custom params on the language top level", k, kk), fmt.Sprintf("Put the value below [languages.%s.params]. See https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120", k), "v0.112.0")
|
||||
params[kk] = vv
|
||||
}
|
||||
}
|
||||
if kk == "baseurl" {
|
||||
// baseURL configure don the language level is a multihost setup.
|
||||
isMultihost = true
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config/allconfig"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
)
|
||||
|
||||
func TestDirsMount(t *testing.T) {
|
||||
@@ -97,37 +98,13 @@ suffixes = ["html", "xhtml"]
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
conf := b.H.Configs.Base
|
||||
contentTypes := conf.C.ContentTypes
|
||||
contentTypes := conf.ContentTypes.Config
|
||||
|
||||
b.Assert(contentTypes.HTML.Suffixes(), qt.DeepEquals, []string{"html", "xhtml"})
|
||||
b.Assert(contentTypes.Markdown.Suffixes(), qt.DeepEquals, []string{"md", "mdown", "markdown"})
|
||||
}
|
||||
|
||||
func TestPaginationConfigOld(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[languages.en]
|
||||
weight = 1
|
||||
paginatePath = "page-en"
|
||||
|
||||
[languages.de]
|
||||
weight = 2
|
||||
paginatePath = "page-de"
|
||||
paginate = 20
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
confEn := b.H.Sites[0].Conf.Pagination()
|
||||
confDe := b.H.Sites[1].Conf.Pagination()
|
||||
|
||||
b.Assert(confEn.Path, qt.Equals, "page-en")
|
||||
b.Assert(confEn.PagerSize, qt.Equals, 10)
|
||||
b.Assert(confDe.Path, qt.Equals, "page-de")
|
||||
b.Assert(confDe.PagerSize, qt.Equals, 20)
|
||||
}
|
||||
|
||||
func TestPaginationConfigNew(t *testing.T) {
|
||||
func TestPaginationConfig(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[languages.en]
|
||||
@@ -184,3 +161,76 @@ title: "p3"
|
||||
b.AssertFileExists("public/page/1/index.html", false)
|
||||
b.AssertFileContent("public/page/2/index.html", "pagination-default")
|
||||
}
|
||||
|
||||
func TestMapUglyURLs(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[uglyurls]
|
||||
posts = true
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
c := b.H.Configs.Base
|
||||
|
||||
b.Assert(c.C.IsUglyURLSection("posts"), qt.IsTrue)
|
||||
b.Assert(c.C.IsUglyURLSection("blog"), qt.IsFalse)
|
||||
}
|
||||
|
||||
// Issue 13199
|
||||
func TestInvalidOutputFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
[outputs]
|
||||
home = ['html','foo']
|
||||
-- layouts/index.html --
|
||||
x
|
||||
`
|
||||
|
||||
b, err := hugolib.TestE(t, files)
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
b.Assert(err.Error(), qt.Contains, `failed to create config: unknown output format "foo" for kind "home"`)
|
||||
}
|
||||
|
||||
// Issue 13201
|
||||
func TestLanguageConfigSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
[languages.en]
|
||||
title = 'TITLE_EN'
|
||||
weight = 2
|
||||
[languages.de]
|
||||
title = 'TITLE_DE'
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
title = 'TITLE_FR'
|
||||
weight = 3
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
b.Assert(b.H.Configs.LanguageConfigSlice[0].Title, qt.Equals, `TITLE_DE`)
|
||||
}
|
||||
|
||||
func TestContentTypesDefault(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
ct := b.H.Configs.Base.ContentTypes
|
||||
c := ct.Config
|
||||
s := ct.SourceStructure.(map[string]media.ContentTypeConfig)
|
||||
|
||||
b.Assert(c.IsContentFile("foo.md"), qt.Equals, true)
|
||||
b.Assert(len(s), qt.Equals, 6)
|
||||
}
|
||||
|
||||
@@ -163,6 +163,15 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return err
|
||||
},
|
||||
},
|
||||
"contenttypes": {
|
||||
key: "contenttypes",
|
||||
weight: 100, // This needs to be decoded after media types.
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.ContentTypes, err = media.DecodeContentTypes(p.p.GetStringMap(d.key), p.c.MediaTypes.Config)
|
||||
return err
|
||||
},
|
||||
},
|
||||
"mediatypes": {
|
||||
key: "mediatypes",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
@@ -419,6 +428,8 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
p.c.UglyURLs = vv
|
||||
case string:
|
||||
p.c.UglyURLs = vv == "true"
|
||||
case maps.Params:
|
||||
p.c.UglyURLs = cast.ToStringMapBool(maps.CleanConfigStringMap(vv))
|
||||
default:
|
||||
p.c.UglyURLs = cast.ToStringMapBool(v)
|
||||
}
|
||||
|
||||
@@ -137,15 +137,15 @@ func (c ConfigLanguage) Watching() bool {
|
||||
return c.m.Base.Internal.Watch
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) NewIdentityManager(name string) identity.Manager {
|
||||
func (c ConfigLanguage) NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager {
|
||||
if !c.Watching() {
|
||||
return identity.NopManager
|
||||
}
|
||||
return identity.NewManager(name)
|
||||
return identity.NewManager(name, opts...)
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider {
|
||||
return c.config.C.ContentTypes
|
||||
return c.config.ContentTypes.Config
|
||||
}
|
||||
|
||||
// GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.
|
||||
|
||||
+19
-15
@@ -64,7 +64,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
return nil, fmt.Errorf("failed to create config from result: %w", err)
|
||||
}
|
||||
|
||||
moduleConfig, modulesClient, err := l.loadModules(configs)
|
||||
moduleConfig, modulesClient, err := l.loadModules(configs, d.IgnoreModuleDoesNotExist)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load modules: %w", err)
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
return nil, fmt.Errorf("failed to init config: %w", err)
|
||||
}
|
||||
|
||||
loggers.InitGlobalLogger(d.Logger.Level(), configs.Base.PanicOnWarning)
|
||||
loggers.SetGlobalLogger(d.Logger)
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
@@ -116,6 +116,9 @@ type ConfigSourceDescriptor struct {
|
||||
|
||||
// Defaults to os.Environ if not set.
|
||||
Environ []string
|
||||
|
||||
// If set, this will be used to ignore the module does not exist error.
|
||||
IgnoreModuleDoesNotExist bool
|
||||
}
|
||||
|
||||
func (d ConfigSourceDescriptor) configFilenames() []string {
|
||||
@@ -453,7 +456,7 @@ func (l *configLoader) loadConfigMain(d ConfigSourceDescriptor) (config.LoadConf
|
||||
return res, l.ModulesConfig, err
|
||||
}
|
||||
|
||||
func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *modules.Client, error) {
|
||||
func (l *configLoader) loadModules(configs *Configs, ignoreModuleDoesNotExist bool) (modules.ModulesConfig, *modules.Client, error) {
|
||||
bcfg := configs.LoadingInfo.BaseConfig
|
||||
conf := configs.Base
|
||||
workingDir := bcfg.WorkingDir
|
||||
@@ -467,7 +470,7 @@ func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *mo
|
||||
ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
|
||||
}
|
||||
|
||||
ex := hexec.New(conf.Security, workingDir)
|
||||
ex := hexec.New(conf.Security, workingDir, l.Logger)
|
||||
|
||||
hook := func(m *modules.ModulesConfig) error {
|
||||
for _, tc := range m.AllModules {
|
||||
@@ -487,17 +490,18 @@ func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *mo
|
||||
}
|
||||
|
||||
modulesClient := modules.NewClient(modules.ClientConfig{
|
||||
Fs: l.Fs,
|
||||
Logger: l.Logger,
|
||||
Exec: ex,
|
||||
HookBeforeFinalize: hook,
|
||||
WorkingDir: workingDir,
|
||||
ThemesDir: themesDir,
|
||||
PublishDir: publishDir,
|
||||
Environment: l.Environment,
|
||||
CacheDir: conf.Caches.CacheDirModules(),
|
||||
ModuleConfig: conf.Module,
|
||||
IgnoreVendor: ignoreVendor,
|
||||
Fs: l.Fs,
|
||||
Logger: l.Logger,
|
||||
Exec: ex,
|
||||
HookBeforeFinalize: hook,
|
||||
WorkingDir: workingDir,
|
||||
ThemesDir: themesDir,
|
||||
PublishDir: publishDir,
|
||||
Environment: l.Environment,
|
||||
CacheDir: conf.Caches.CacheDirModules(),
|
||||
ModuleConfig: conf.Module,
|
||||
IgnoreVendor: ignoreVendor,
|
||||
IgnoreModuleDoesNotExist: ignoreModuleDoesNotExist,
|
||||
})
|
||||
|
||||
moduleConfig, err := modulesClient.Collect()
|
||||
|
||||
+94
-23
@@ -15,6 +15,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -226,7 +227,22 @@ type Server struct {
|
||||
Redirects []Redirect
|
||||
|
||||
compiledHeaders []glob.Glob
|
||||
compiledRedirects []glob.Glob
|
||||
compiledRedirects []redirect
|
||||
}
|
||||
|
||||
type redirect struct {
|
||||
from glob.Glob
|
||||
fromRe *regexp.Regexp
|
||||
headers map[string]glob.Glob
|
||||
}
|
||||
|
||||
func (r redirect) matchHeader(header http.Header) bool {
|
||||
for k, v := range r.headers {
|
||||
if !v.Match(header.Get(k)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) CompileConfig(logger loggers.Logger) error {
|
||||
@@ -234,10 +250,41 @@ func (s *Server) CompileConfig(logger loggers.Logger) error {
|
||||
return nil
|
||||
}
|
||||
for _, h := range s.Headers {
|
||||
s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For))
|
||||
g, err := glob.Compile(h.For)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile Headers glob %q: %w", h.For, err)
|
||||
}
|
||||
s.compiledHeaders = append(s.compiledHeaders, g)
|
||||
}
|
||||
for _, r := range s.Redirects {
|
||||
s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From))
|
||||
if r.From == "" && r.FromRe == "" {
|
||||
return fmt.Errorf("redirects must have either From or FromRe set")
|
||||
}
|
||||
rd := redirect{
|
||||
headers: make(map[string]glob.Glob),
|
||||
}
|
||||
if r.From != "" {
|
||||
g, err := glob.Compile(r.From)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile Redirect glob %q: %w", r.From, err)
|
||||
}
|
||||
rd.from = g
|
||||
}
|
||||
if r.FromRe != "" {
|
||||
re, err := regexp.Compile(r.FromRe)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile Redirect regexp %q: %w", r.FromRe, err)
|
||||
}
|
||||
rd.fromRe = re
|
||||
}
|
||||
for k, v := range r.FromHeaders {
|
||||
g, err := glob.Compile(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile Redirect header glob %q: %w", v, err)
|
||||
}
|
||||
rd.headers[k] = g
|
||||
}
|
||||
s.compiledRedirects = append(s.compiledRedirects, rd)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -266,22 +313,42 @@ func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr {
|
||||
return matches
|
||||
}
|
||||
|
||||
func (s *Server) MatchRedirect(pattern string) Redirect {
|
||||
func (s *Server) MatchRedirect(pattern string, header http.Header) Redirect {
|
||||
if s.compiledRedirects == nil {
|
||||
return Redirect{}
|
||||
}
|
||||
|
||||
pattern = strings.TrimSuffix(pattern, "index.html")
|
||||
|
||||
for i, g := range s.compiledRedirects {
|
||||
for i, r := range s.compiledRedirects {
|
||||
redir := s.Redirects[i]
|
||||
|
||||
// No redirect to self.
|
||||
if redir.To == pattern {
|
||||
return Redirect{}
|
||||
var found bool
|
||||
|
||||
if r.from != nil {
|
||||
if r.from.Match(pattern) {
|
||||
found = header == nil || r.matchHeader(header)
|
||||
// We need to do regexp group replacements if needed.
|
||||
}
|
||||
}
|
||||
|
||||
if g.Match(pattern) {
|
||||
if r.fromRe != nil {
|
||||
m := r.fromRe.FindStringSubmatch(pattern)
|
||||
if m != nil {
|
||||
if !found {
|
||||
found = header == nil || r.matchHeader(header)
|
||||
}
|
||||
|
||||
if found {
|
||||
// Replace $1, $2 etc. in To.
|
||||
for i, g := range m[1:] {
|
||||
redir.To = strings.ReplaceAll(redir.To, fmt.Sprintf("$%d", i+1), g)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
return redir
|
||||
}
|
||||
}
|
||||
@@ -295,8 +362,22 @@ type Headers struct {
|
||||
}
|
||||
|
||||
type Redirect struct {
|
||||
// From is the Glob pattern to match.
|
||||
// One of From or FromRe must be set.
|
||||
From string
|
||||
To string
|
||||
|
||||
// FromRe is the regexp to match.
|
||||
// This regexp can contain group matches (e.g. $1) that can be used in the To field.
|
||||
// One of From or FromRe must be set.
|
||||
FromRe string
|
||||
|
||||
// To is the target URL.
|
||||
To string
|
||||
|
||||
// Headers to match for the redirect.
|
||||
// This maps the HTTP header name to a Glob pattern with values to match.
|
||||
// If the map is empty, the redirect will always be triggered.
|
||||
FromHeaders map[string]string
|
||||
|
||||
// HTTP status code to use for the redirect.
|
||||
// A status code of 200 will trigger a URL rewrite.
|
||||
@@ -369,7 +450,7 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error {
|
||||
}
|
||||
|
||||
func (r Redirect) IsZero() bool {
|
||||
return r.From == ""
|
||||
return r.From == "" && r.FromRe == ""
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -383,17 +464,7 @@ func DecodeServer(cfg Provider) (Server, error) {
|
||||
_ = mapstructure.WeakDecode(cfg.GetStringMap("server"), s)
|
||||
|
||||
for i, redir := range s.Redirects {
|
||||
// Get it in line with the Hugo server for OK responses.
|
||||
// We currently treat the 404 as a special case, they are always "ugly", so keep them as is.
|
||||
if redir.Status != 404 {
|
||||
redir.To = strings.TrimSuffix(redir.To, "index.html")
|
||||
if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") {
|
||||
// There are some tricky infinite loop situations when dealing
|
||||
// when the target does not have a trailing slash.
|
||||
// This can certainly be handled better, but not time for that now.
|
||||
return Server{}, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To)
|
||||
}
|
||||
}
|
||||
redir.To = strings.TrimSuffix(redir.To, "index.html")
|
||||
s.Redirects[i] = redir
|
||||
}
|
||||
|
||||
@@ -401,7 +472,7 @@ func DecodeServer(cfg Provider) (Server, error) {
|
||||
// Set up a default redirect for 404s.
|
||||
s.Redirects = []Redirect{
|
||||
{
|
||||
From: "**",
|
||||
From: "/**",
|
||||
To: "/404.html",
|
||||
Status: 404,
|
||||
},
|
||||
|
||||
+41
-35
@@ -71,7 +71,28 @@ X-Content-Type-Options = "nosniff"
|
||||
|
||||
[[server.redirects]]
|
||||
from = "/foo/**"
|
||||
to = "/foo/index.html"
|
||||
to = "/baz/index.html"
|
||||
status = 200
|
||||
|
||||
[[server.redirects]]
|
||||
from = "/loop/**"
|
||||
to = "/loop/foo/"
|
||||
status = 200
|
||||
|
||||
[[server.redirects]]
|
||||
from = "/b/**"
|
||||
fromRe = "/b/(.*)/"
|
||||
to = "/baz/$1/"
|
||||
status = 200
|
||||
|
||||
[[server.redirects]]
|
||||
fromRe = "/c/(.*)/"
|
||||
to = "/boo/$1/"
|
||||
status = 200
|
||||
|
||||
[[server.redirects]]
|
||||
fromRe = "/d/(.*)/"
|
||||
to = "/boo/$1/"
|
||||
status = 200
|
||||
|
||||
[[server.redirects]]
|
||||
@@ -79,11 +100,6 @@ from = "/google/**"
|
||||
to = "https://google.com/"
|
||||
status = 301
|
||||
|
||||
[[server.redirects]]
|
||||
from = "/**"
|
||||
to = "/default/index.html"
|
||||
status = 301
|
||||
|
||||
|
||||
|
||||
`, "toml")
|
||||
@@ -100,45 +116,35 @@ status = 301
|
||||
{Key: "X-XSS-Protection", Value: "1; mode=block"},
|
||||
})
|
||||
|
||||
c.Assert(s.MatchRedirect("/foo/bar/baz"), qt.DeepEquals, Redirect{
|
||||
c.Assert(s.MatchRedirect("/foo/bar/baz", nil), qt.DeepEquals, Redirect{
|
||||
From: "/foo/**",
|
||||
To: "/foo/",
|
||||
To: "/baz/",
|
||||
Status: 200,
|
||||
})
|
||||
|
||||
c.Assert(s.MatchRedirect("/someother"), qt.DeepEquals, Redirect{
|
||||
From: "/**",
|
||||
To: "/default/",
|
||||
Status: 301,
|
||||
c.Assert(s.MatchRedirect("/foo/bar/", nil), qt.DeepEquals, Redirect{
|
||||
From: "/foo/**",
|
||||
To: "/baz/",
|
||||
Status: 200,
|
||||
})
|
||||
|
||||
c.Assert(s.MatchRedirect("/google/foo"), qt.DeepEquals, Redirect{
|
||||
c.Assert(s.MatchRedirect("/b/c/", nil), qt.DeepEquals, Redirect{
|
||||
From: "/b/**",
|
||||
FromRe: "/b/(.*)/",
|
||||
To: "/baz/c/",
|
||||
Status: 200,
|
||||
})
|
||||
|
||||
c.Assert(s.MatchRedirect("/c/d/", nil).To, qt.Equals, "/boo/d/")
|
||||
c.Assert(s.MatchRedirect("/c/d/e/", nil).To, qt.Equals, "/boo/d/e/")
|
||||
|
||||
c.Assert(s.MatchRedirect("/someother", nil), qt.DeepEquals, Redirect{})
|
||||
|
||||
c.Assert(s.MatchRedirect("/google/foo", nil), qt.DeepEquals, Redirect{
|
||||
From: "/google/**",
|
||||
To: "https://google.com/",
|
||||
Status: 301,
|
||||
})
|
||||
|
||||
// No redirect loop, please.
|
||||
c.Assert(s.MatchRedirect("/default/index.html"), qt.DeepEquals, Redirect{})
|
||||
c.Assert(s.MatchRedirect("/default/"), qt.DeepEquals, Redirect{})
|
||||
|
||||
for _, errorCase := range []string{
|
||||
`[[server.redirects]]
|
||||
from = "/**"
|
||||
to = "/file"
|
||||
status = 301`,
|
||||
`[[server.redirects]]
|
||||
from = "/**"
|
||||
to = "/foo/file.html"
|
||||
status = 301`,
|
||||
} {
|
||||
|
||||
cfg, err := FromConfigString(errorCase, "toml")
|
||||
c.Assert(err, qt.IsNil)
|
||||
_, err = DecodeServer(cfg)
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigCacheBusters(t *testing.T) {
|
||||
|
||||
@@ -58,7 +58,7 @@ type AllProvider interface {
|
||||
BuildDrafts() bool
|
||||
Running() bool
|
||||
Watching() bool
|
||||
NewIdentityManager(name string) identity.Manager
|
||||
NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager
|
||||
FastRenderMode() bool
|
||||
PrintUnusedTemplates() bool
|
||||
EnableMissingTranslationPlaceholders() bool
|
||||
|
||||
@@ -15,7 +15,6 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -26,42 +25,6 @@ import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
// ConfigRootKeysSet contains all of the config map root keys.
|
||||
ConfigRootKeysSet = map[string]bool{
|
||||
"build": true,
|
||||
"caches": true,
|
||||
"cascade": true,
|
||||
"frontmatter": true,
|
||||
"languages": true,
|
||||
"imaging": true,
|
||||
"markup": true,
|
||||
"mediatypes": true,
|
||||
"menus": true,
|
||||
"minify": true,
|
||||
"module": true,
|
||||
"outputformats": true,
|
||||
"params": true,
|
||||
"permalinks": true,
|
||||
"related": true,
|
||||
"sitemap": true,
|
||||
"privacy": true,
|
||||
"security": true,
|
||||
"taxonomies": true,
|
||||
}
|
||||
|
||||
// ConfigRootKeys is a sorted version of ConfigRootKeysSet.
|
||||
ConfigRootKeys []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
for k := range ConfigRootKeysSet {
|
||||
ConfigRootKeys = append(ConfigRootKeys, k)
|
||||
}
|
||||
sort.Strings(ConfigRootKeys)
|
||||
}
|
||||
|
||||
// New creates a Provider backed by an empty maps.Params.
|
||||
func New() Provider {
|
||||
return &defaultConfigProvider{
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import (
|
||||
func DecodeNamespace[S, C any](configSource any, buildConfig func(any) (C, any, error)) (*ConfigNamespace[S, C], error) {
|
||||
// Calculate the hash of the input (not including any defaults applied later).
|
||||
// This allows us to introduce new config options without breaking the hash.
|
||||
h := hashing.HashString(configSource)
|
||||
h := hashing.HashStringHex(configSource)
|
||||
|
||||
// Build the config
|
||||
c, ext, err := buildConfig(configSource)
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestNamespace(t *testing.T) {
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(ns, qt.Not(qt.IsNil))
|
||||
c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]interface{}{"foo": "bar"})
|
||||
c.Assert(ns.SourceHash, qt.Equals, "1450430416588600409")
|
||||
c.Assert(ns.SourceHash, qt.Equals, "1420f6c7782f7459")
|
||||
c.Assert(ns.Config, qt.DeepEquals, &tstNsExt{Foo: "bar"})
|
||||
c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil))
|
||||
}
|
||||
|
||||
@@ -30,9 +30,10 @@ type Config struct {
|
||||
Disqus Disqus
|
||||
GoogleAnalytics GoogleAnalytics
|
||||
Instagram Instagram
|
||||
Twitter Twitter
|
||||
Twitter Twitter // deprecated in favor of X in v0.141.0
|
||||
Vimeo Vimeo
|
||||
YouTube YouTube
|
||||
X X
|
||||
}
|
||||
|
||||
// Disqus holds the privacy configuration settings related to the Disqus template.
|
||||
@@ -58,7 +59,8 @@ type Instagram struct {
|
||||
Simple bool
|
||||
}
|
||||
|
||||
// Twitter holds the privacy configuration settingsrelated to the Twitter shortcode.
|
||||
// Twitter holds the privacy configuration settings related to the Twitter shortcode.
|
||||
// Deprecated in favor of X in v0.141.0.
|
||||
type Twitter struct {
|
||||
Service `mapstructure:",squash"`
|
||||
|
||||
@@ -70,7 +72,7 @@ type Twitter struct {
|
||||
Simple bool
|
||||
}
|
||||
|
||||
// Vimeo holds the privacy configuration settingsrelated to the Vimeo shortcode.
|
||||
// Vimeo holds the privacy configuration settings related to the Vimeo shortcode.
|
||||
type Vimeo struct {
|
||||
Service `mapstructure:",squash"`
|
||||
|
||||
@@ -84,7 +86,7 @@ type Vimeo struct {
|
||||
Simple bool
|
||||
}
|
||||
|
||||
// YouTube holds the privacy configuration settingsrelated to the YouTube shortcode.
|
||||
// YouTube holds the privacy configuration settings related to the YouTube shortcode.
|
||||
type YouTube struct {
|
||||
Service `mapstructure:",squash"`
|
||||
|
||||
@@ -94,6 +96,20 @@ type YouTube struct {
|
||||
PrivacyEnhanced bool
|
||||
}
|
||||
|
||||
// X holds the privacy configuration settings related to the X shortcode.
|
||||
type X struct {
|
||||
Service `mapstructure:",squash"`
|
||||
|
||||
// When set to true, the X post and its embedded page on your site are not
|
||||
// used for purposes that include personalized suggestions and personalized
|
||||
// ads.
|
||||
EnableDNT bool
|
||||
|
||||
// If simple mode is enabled, a static and no-JS version of the X post will
|
||||
// be built.
|
||||
Simple bool
|
||||
}
|
||||
|
||||
// DecodeConfig creates a privacy Config from a given Hugo configuration.
|
||||
func DecodeConfig(cfg config.Provider) (pc Config, err error) {
|
||||
if !cfg.IsSet(privacyConfigKey) {
|
||||
|
||||
@@ -36,7 +36,7 @@ respectDoNotTrack = true
|
||||
[privacy.instagram]
|
||||
disable = true
|
||||
simple = true
|
||||
[privacy.twitter]
|
||||
[privacy.x]
|
||||
disable = true
|
||||
enableDNT = true
|
||||
simple = true
|
||||
@@ -59,9 +59,10 @@ simple = true
|
||||
got := []bool{
|
||||
pc.Disqus.Disable, pc.GoogleAnalytics.Disable,
|
||||
pc.GoogleAnalytics.RespectDoNotTrack, pc.Instagram.Disable,
|
||||
pc.Instagram.Simple, pc.Twitter.Disable, pc.Twitter.EnableDNT,
|
||||
pc.Twitter.Simple, pc.Vimeo.Disable, pc.Vimeo.EnableDNT, pc.Vimeo.Simple,
|
||||
pc.YouTube.PrivacyEnhanced, pc.YouTube.Disable,
|
||||
pc.Instagram.Simple,
|
||||
pc.Vimeo.Disable, pc.Vimeo.EnableDNT, pc.Vimeo.Simple,
|
||||
pc.YouTube.PrivacyEnhanced, pc.YouTube.Disable, pc.X.Disable, pc.X.EnableDNT,
|
||||
pc.X.Simple,
|
||||
}
|
||||
|
||||
c.Assert(got, qt.All(qt.Equals), true)
|
||||
|
||||
@@ -31,7 +31,8 @@ type Config struct {
|
||||
Disqus Disqus
|
||||
GoogleAnalytics GoogleAnalytics
|
||||
Instagram Instagram
|
||||
Twitter Twitter
|
||||
Twitter Twitter // deprecated in favor of X in v0.141.0
|
||||
X X
|
||||
RSS RSS
|
||||
}
|
||||
|
||||
@@ -61,6 +62,7 @@ type Instagram struct {
|
||||
}
|
||||
|
||||
// Twitter holds the functional configuration settings related to the Twitter shortcodes.
|
||||
// Deprecated in favor of X in v0.141.0.
|
||||
type Twitter struct {
|
||||
// The Simple variant of Twitter is decorated with a basic set of inline styles.
|
||||
// This means that if you want to provide your own CSS, you want
|
||||
@@ -68,6 +70,14 @@ type Twitter struct {
|
||||
DisableInlineCSS bool
|
||||
}
|
||||
|
||||
// X holds the functional configuration settings related to the X shortcodes.
|
||||
type X struct {
|
||||
// The Simple variant of X is decorated with a basic set of inline styles.
|
||||
// This means that if you want to provide your own CSS, you want
|
||||
// to disable the inline CSS provided by Hugo.
|
||||
DisableInlineCSS bool
|
||||
}
|
||||
|
||||
// RSS holds the functional configuration settings related to the RSS feeds.
|
||||
type RSS struct {
|
||||
// Limit the number of pages.
|
||||
|
||||
@@ -36,6 +36,8 @@ id = "ga_id"
|
||||
disableInlineCSS = true
|
||||
[services.twitter]
|
||||
disableInlineCSS = true
|
||||
[services.x]
|
||||
disableInlineCSS = true
|
||||
`
|
||||
cfg, err := config.FromConfigString(tomlConfig, "toml")
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
+6
-4
@@ -82,11 +82,13 @@ func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error
|
||||
b.setArcheTypeFilenameToUse(ext)
|
||||
|
||||
withBuildLock := func() (string, error) {
|
||||
unlock, err := h.BaseFs.LockBuild()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to acquire a build lock: %s", err)
|
||||
if !h.Configs.Base.NoBuildLock {
|
||||
unlock, err := h.BaseFs.LockBuild()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to acquire a build lock: %s", err)
|
||||
}
|
||||
defer unlock()
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
if b.isDir {
|
||||
return "", b.buildDir()
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
+++
|
||||
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
date = {{ .Date }}
|
||||
draft = true
|
||||
+++
|
||||
@@ -76,6 +76,11 @@ func CreateSite(createpath string, sourceFs afero.Fs, force bool, format string)
|
||||
return err
|
||||
}
|
||||
|
||||
err = newSiteCreateArchetype(sourceFs, createpath, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return copyFiles(createpath, sourceFs, siteFs)
|
||||
}
|
||||
|
||||
@@ -109,3 +114,19 @@ func newSiteCreateConfig(fs afero.Fs, createpath string, format string) (err err
|
||||
|
||||
return helpers.WriteToDisk(filepath.Join(createpath, "hugo."+format), &buf, fs)
|
||||
}
|
||||
|
||||
func newSiteCreateArchetype(fs afero.Fs, createpath string, format string) (err error) {
|
||||
in := map[string]any{
|
||||
"title": "{{ replace .File.ContentBaseName \"-\" \" \" | title }}",
|
||||
"date": "{{ .Date }}",
|
||||
"draft": true,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = parser.InterfaceToFrontMatter(in, metadecoders.FormatFromString(format), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), &buf, fs)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
//go:build withdeploy
|
||||
|
||||
package deploy
|
||||
|
||||
|
||||
+1
-2
@@ -11,8 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
//go:build withdeploy
|
||||
|
||||
package deploy
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !solaris && !nodeploy
|
||||
// +build !solaris,!nodeploy
|
||||
//go:build !solaris && withdeploy
|
||||
|
||||
package deploy
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
//go:build withdeploy
|
||||
|
||||
package deploy
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
//go:build withdeploy
|
||||
|
||||
package deployconfig
|
||||
|
||||
|
||||
+1
-2
@@ -11,8 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
//go:build withdeploy
|
||||
|
||||
package deploy
|
||||
|
||||
|
||||
Vendored
+62
-13
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/internal/js"
|
||||
"github.com/gohugoio/hugo/internal/warpc"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
@@ -47,6 +49,9 @@ type Deps struct {
|
||||
// The templates to use. This will usually implement the full tpl.TemplateManager.
|
||||
tmplHandlers *tpl.TemplateHandlers
|
||||
|
||||
// The template funcs.
|
||||
TmplFuncMap map[string]any
|
||||
|
||||
// The file systems to use.
|
||||
Fs *hugofs.Fs `json:"-"`
|
||||
|
||||
@@ -83,10 +88,13 @@ type Deps struct {
|
||||
Metrics metrics.Provider
|
||||
|
||||
// BuildStartListeners will be notified before a build starts.
|
||||
BuildStartListeners *Listeners
|
||||
BuildStartListeners *Listeners[any]
|
||||
|
||||
// BuildEndListeners will be notified after a build finishes.
|
||||
BuildEndListeners *Listeners
|
||||
BuildEndListeners *Listeners[any]
|
||||
|
||||
// OnChangeListeners will be notified when something changes.
|
||||
OnChangeListeners *Listeners[identity.Identity]
|
||||
|
||||
// Resources that gets closed when the build is done or the server shuts down.
|
||||
BuildClosers *types.Closers
|
||||
@@ -98,6 +106,14 @@ type Deps struct {
|
||||
// TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
|
||||
WasmDispatchers *warpc.Dispatchers
|
||||
|
||||
// The JS batcher client.
|
||||
JSBatcherClient js.BatcherClient
|
||||
|
||||
// The JS batcher client.
|
||||
// JSBatcherClient *esbuild.BatcherClient
|
||||
|
||||
isClosed bool
|
||||
|
||||
*globalErrHandler
|
||||
}
|
||||
|
||||
@@ -152,23 +168,27 @@ func (d *Deps) Init() error {
|
||||
}
|
||||
|
||||
if d.BuildStartListeners == nil {
|
||||
d.BuildStartListeners = &Listeners{}
|
||||
d.BuildStartListeners = &Listeners[any]{}
|
||||
}
|
||||
|
||||
if d.BuildEndListeners == nil {
|
||||
d.BuildEndListeners = &Listeners{}
|
||||
d.BuildEndListeners = &Listeners[any]{}
|
||||
}
|
||||
|
||||
if d.BuildClosers == nil {
|
||||
d.BuildClosers = &types.Closers{}
|
||||
}
|
||||
|
||||
if d.OnChangeListeners == nil {
|
||||
d.OnChangeListeners = &Listeners[identity.Identity]{}
|
||||
}
|
||||
|
||||
if d.Metrics == nil && d.Conf.TemplateMetrics() {
|
||||
d.Metrics = metrics.NewProvider(d.Conf.TemplateMetricsHints())
|
||||
}
|
||||
|
||||
if d.ExecHelper == nil {
|
||||
d.ExecHelper = hexec.New(d.Conf.GetConfigSection("security").(security.Config), d.Conf.WorkingDir())
|
||||
d.ExecHelper = hexec.New(d.Conf.GetConfigSection("security").(security.Config), d.Conf.WorkingDir(), d.Log)
|
||||
}
|
||||
|
||||
if d.MemCache == nil {
|
||||
@@ -266,6 +286,23 @@ func (d *Deps) Compile(prototype *Deps) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MkdirTemp returns a temporary directory path that will be cleaned up on exit.
|
||||
func (d Deps) MkdirTemp(pattern string) (string, error) {
|
||||
filename, err := os.MkdirTemp("", pattern)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
d.BuildClosers.Add(
|
||||
types.CloserFunc(
|
||||
func() error {
|
||||
return os.RemoveAll(filename)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
type globalErrHandler struct {
|
||||
logger loggers.Logger
|
||||
|
||||
@@ -304,15 +341,16 @@ func (e *globalErrHandler) StopErrorCollector() {
|
||||
}
|
||||
|
||||
// Listeners represents an event listener.
|
||||
type Listeners struct {
|
||||
type Listeners[T any] struct {
|
||||
sync.Mutex
|
||||
|
||||
// A list of funcs to be notified about an event.
|
||||
listeners []func()
|
||||
// If the return value is true, the listener will be removed.
|
||||
listeners []func(...T) bool
|
||||
}
|
||||
|
||||
// Add adds a function to a Listeners instance.
|
||||
func (b *Listeners) Add(f func()) {
|
||||
func (b *Listeners[T]) Add(f func(...T) bool) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
@@ -322,12 +360,16 @@ func (b *Listeners) Add(f func()) {
|
||||
}
|
||||
|
||||
// Notify executes all listener functions.
|
||||
func (b *Listeners) Notify() {
|
||||
func (b *Listeners[T]) Notify(vs ...T) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
temp := b.listeners[:0]
|
||||
for _, notify := range b.listeners {
|
||||
notify()
|
||||
if !notify(vs...) {
|
||||
temp = append(temp, notify)
|
||||
}
|
||||
}
|
||||
b.listeners = temp
|
||||
}
|
||||
|
||||
// ResourceProvider is used to create and refresh, and clone resources needed.
|
||||
@@ -345,6 +387,11 @@ func (d *Deps) TextTmpl() tpl.TemplateParseFinder {
|
||||
}
|
||||
|
||||
func (d *Deps) Close() error {
|
||||
if d.isClosed {
|
||||
return nil
|
||||
}
|
||||
d.isClosed = true
|
||||
|
||||
if d.MemCache != nil {
|
||||
d.MemCache.Stop()
|
||||
}
|
||||
@@ -365,9 +412,11 @@ type DepsCfg struct {
|
||||
// The logging level to use.
|
||||
LogLevel logg.Level
|
||||
|
||||
// Where to write the logs.
|
||||
// Currently we typically write everything to stdout.
|
||||
LogOut io.Writer
|
||||
// Logging output.
|
||||
StdErr io.Writer
|
||||
|
||||
// The console output.
|
||||
StdOut io.Writer
|
||||
|
||||
// The file systems to use
|
||||
Fs *hugofs.Fs
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Config file for codespell.
|
||||
# https://github.com/codespell-project/codespell#using-a-config-file
|
||||
|
||||
[codespell]
|
||||
|
||||
# Comma separated list of dirs to be skipped.
|
||||
skip = _vendor,.cspell.json,chroma.css,chroma_dark.css
|
||||
|
||||
# Comma separated list of words to be ignored. Words must be lowercased.
|
||||
ignore-words-list = abl,edn,te,ue,trys,januar,womens,crossreferences
|
||||
|
||||
# Check file names as well.
|
||||
check-filenames = true
|
||||
@@ -68,6 +68,7 @@
|
||||
"# ----------------------------------------------------------------------",
|
||||
"# cspell: ignore hugo terminology",
|
||||
"# ----------------------------------------------------------------------",
|
||||
"alignx",
|
||||
"attrlink",
|
||||
"canonify",
|
||||
"codeowners",
|
||||
@@ -85,6 +86,8 @@
|
||||
"stringifier",
|
||||
"struct",
|
||||
"toclevels",
|
||||
"unmarshal",
|
||||
"unpublishdate",
|
||||
"zgotmplz",
|
||||
"# ----------------------------------------------------------------------",
|
||||
"# cspell: ignore foreign language words",
|
||||
@@ -129,6 +132,7 @@
|
||||
"Samsa",
|
||||
"Stucki",
|
||||
"Thénardier",
|
||||
"WASI",
|
||||
"# ----------------------------------------------------------------------",
|
||||
"# cspell: ignore operating systems and software packages",
|
||||
"# ----------------------------------------------------------------------",
|
||||
@@ -156,17 +160,22 @@
|
||||
"# cspell: ignore miscellaneous",
|
||||
"# ----------------------------------------------------------------------",
|
||||
"achristie",
|
||||
"ccpa",
|
||||
"crpa",
|
||||
"ddmaurier",
|
||||
"dring",
|
||||
"fleqn",
|
||||
"inor",
|
||||
"jausten",
|
||||
"jdoe",
|
||||
"jsmith",
|
||||
"leqno",
|
||||
"milli",
|
||||
"rgba",
|
||||
"rsmith",
|
||||
"tdewolff",
|
||||
"tjones",
|
||||
"vcard",
|
||||
"wcag",
|
||||
"xfeff"
|
||||
]
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: Default
|
||||
about: This is the default issue template.
|
||||
labels:
|
||||
- NeedsTriage
|
||||
---
|
||||
+5
@@ -20,3 +20,8 @@ jobs:
|
||||
incremental_files_only: true
|
||||
inline: warning
|
||||
strict: false
|
||||
- uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
check_filenames: true
|
||||
check_hidden: true
|
||||
# by default, codespell uses configuration from the .codespellrc
|
||||
|
||||
+9
-6
@@ -1,10 +1,13 @@
|
||||
.DS_Store
|
||||
.hugo_build.lock
|
||||
/.idea
|
||||
/.vscode
|
||||
/public
|
||||
/dist
|
||||
node_modules
|
||||
/public
|
||||
hugo_stats.json
|
||||
node_modules/
|
||||
nohup.out
|
||||
.DS_Store
|
||||
trace.out
|
||||
.hugo_build.lock
|
||||
resources/_gen/images/
|
||||
package-lock.json
|
||||
public/
|
||||
resources/
|
||||
trace.out
|
||||
@@ -23,3 +23,4 @@ MD046: false
|
||||
MD049: false
|
||||
MD050: false
|
||||
MD053: false
|
||||
MD055: false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user