mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 15:58:53 +00:00
Compare commits
383 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04efcb2a68 | |||
| 4560725521 | |||
| 4deb5c6066 | |||
| 397fce5603 | |||
| 0093eaa683 | |||
| d0f731c039 | |||
| c07f3626e7 | |||
| 42b5d16536 | |||
| d80d5a104f | |||
| 842262f653 | |||
| 2dbdf38a54 | |||
| f8c4e1690a | |||
| 627eed1d62 | |||
| 82ba634ed9 | |||
| d417a6cf74 | |||
| 13dac7f3cd | |||
| 30c2e54c25 | |||
| 3117e58595 | |||
| 5b5dcb8d5a | |||
| ec920363cd | |||
| ffe3eb9195 | |||
| d070bdf10f | |||
| b08193971a | |||
| 0a56f2af4e | |||
| 9e360d3844 | |||
| a8c221d33b | |||
| e58a540895 | |||
| 20162518c4 | |||
| 6b469cc8fb | |||
| 080dcac6b8 | |||
| 072fc8cc34 | |||
| 658e11ebaa | |||
| e0a882fd3b | |||
| d0657a436e | |||
| a6e2e38bb2 | |||
| 510e179003 | |||
| 21484f9e12 | |||
| da00e7714e | |||
| ed9aa374dd | |||
| 2a231b0b57 | |||
| 69c590080f | |||
| 2fd4a7d3d6 | |||
| 5a4ac2dac1 | |||
| db1562e134 | |||
| 5b18e10841 | |||
| d276e901b3 | |||
| d7497b28c1 | |||
| 94459680ba | |||
| 9202117ba0 | |||
| a6fa290f67 | |||
| 0bbc2fb523 | |||
| 94e8a90769 | |||
| a461e9d01a | |||
| 48c98a8d24 | |||
| bbd455fe79 | |||
| 8309a2b1c1 | |||
| 2b7231097f | |||
| 13ff4ded76 | |||
| c3289eb5b8 | |||
| 9539069f5e | |||
| 0e305d6958 | |||
| 5adb81ce39 | |||
| 1c0e7c1ae1 | |||
| cad2d8cc70 | |||
| b80853de90 | |||
| 423594e03a | |||
| 64afb7ca51 | |||
| 004bec2e9a | |||
| 9f2e76af65 | |||
| f1d1576822 | |||
| 5930173cd0 | |||
| 3476b53343 | |||
| e792d27017 | |||
| 9d6495d774 | |||
| 42cc5f88b6 | |||
| a6488e7bad | |||
| 65a78cae1e | |||
| 4d6d1d08da | |||
| b60e1bbdfe | |||
| 61cf3c9f63 | |||
| 31fbc081c9 | |||
| cebd886ac1 | |||
| 38f778cfcc | |||
| 48fb9e4de3 | |||
| 0958167b38 | |||
| b37183e48d | |||
| 04ccde3e94 | |||
| 0fcd9a5d87 | |||
| 4146084367 | |||
| 3bc742bea0 | |||
| 1a796d723c | |||
| 64b7b7a897 | |||
| 5ef8a9f32c | |||
| 3bc34666c8 | |||
| 63b236603e | |||
| b107b4b291 | |||
| a4ac188c19 | |||
| b82d95575d | |||
| 4e14cf7607 | |||
| 5697348e17 | |||
| f98e570b17 | |||
| cdb8b0842b | |||
| a360cab75a | |||
| c97fed08f9 | |||
| 53a6210d82 | |||
| 7182809d96 | |||
| 1f8cd2614d | |||
| 9d76b8fa34 | |||
| d706529720 | |||
| 4576c82ed4 | |||
| ff02d41721 | |||
| 63bb2a5b18 | |||
| 7d8011ed63 | |||
| b9a1be2f99 | |||
| 0978245039 | |||
| ff37df830c | |||
| 5857d5524a | |||
| d86eca5bb2 | |||
| 44e3c002a8 | |||
| 9177849f9b | |||
| 0e0d672bcb | |||
| 19f816f77e | |||
| 970f385c48 | |||
| 76c1248f70 | |||
| 9e76507da2 | |||
| 643b5ae9cf | |||
| 2f7feca9a1 | |||
| 673cde1eb1 | |||
| e46e9ceb29 | |||
| 883e71c96a | |||
| e9669fed14 | |||
| fefb1caacf | |||
| 0327da050f | |||
| 9b8b6d34e2 | |||
| 376704d382 | |||
| c1398b91a9 | |||
| 41b5bc9637 | |||
| 5a1b394f2a | |||
| 0746908247 | |||
| 75084bfc72 | |||
| 12d00d288c | |||
| 260ff1374d | |||
| 14915a0c32 | |||
| fd0c1a5e9b | |||
| 3ad39001df | |||
| 39261b689e | |||
| cff14144a6 | |||
| 5f67151550 | |||
| f7109771a0 | |||
| e1f696911e | |||
| 5f65c17a12 | |||
| 579ff9b652 | |||
| 928a896962 | |||
| 2e54c00933 | |||
| 10928a4f78 | |||
| afd63bf7d5 | |||
| 228126b7f5 | |||
| 0f80be341f | |||
| 78afdb88ab | |||
| 3ed83227ba | |||
| 1a257bb2ba | |||
| 97514f17d6 | |||
| 0df096b868 | |||
| 308ad611b6 | |||
| 6bffcdbd26 | |||
| 7248f43188 | |||
| 08fdca9d93 | |||
| 2c20f5bc00 | |||
| 723b2c485b | |||
| 06bac57ab0 | |||
| ec8b767fa6 | |||
| 6407b2cd01 | |||
| 6ff39fd908 | |||
| 96c0bdf3aa | |||
| e97d3c640f | |||
| 7732da9f93 | |||
| aebde49b88 | |||
| 4ada09415d | |||
| bddcfd911c | |||
| d485f95431 | |||
| a87be597af | |||
| b0eea00756 | |||
| ef8b781b14 | |||
| 3136ff67d8 | |||
| 929808190f | |||
| 667f3a4ba8 | |||
| c061b253a4 | |||
| 0927cf739f | |||
| 26a5e89faa | |||
| e9fa7e81b3 | |||
| ff545f4276 | |||
| b2a827c52c | |||
| 77c7059ff8 | |||
| f4c90bd6ba | |||
| d16228334d | |||
| ea6bcd6944 | |||
| f2e7b49acf | |||
| 923419d7fd | |||
| 837fdfdf45 | |||
| 6819feab6c | |||
| ea54a99ca5 | |||
| 9563c7d13b | |||
| 230a495941 | |||
| c707b71cdf | |||
| b0a9cf0a75 | |||
| 9433cc2562 | |||
| cdf6a0d622 | |||
| bf1fa71379 | |||
| 4f4cec73be | |||
| da4866c2be | |||
| d1109f590a | |||
| a7d182cea1 | |||
| a2a660ed12 | |||
| f7bc4cc505 | |||
| 54f8d8a70a | |||
| 3036d0ac9a | |||
| 6a238a7276 | |||
| f60714b5a1 | |||
| 215a715ddd | |||
| d128d260b5 | |||
| c4aaf1d516 | |||
| 94f10cf4f9 | |||
| b06c2103b8 | |||
| d22f7795c7 | |||
| 39f69ca7f0 | |||
| 64f75adcf6 | |||
| 9262719092 | |||
| 3336762939 | |||
| 5ca40c8f7c | |||
| ef7d14a241 | |||
| c05c99f0c5 | |||
| ff7689ce02 | |||
| 85e2e86266 | |||
| 515f8a6d8e | |||
| f22c4aba04 | |||
| 85d31f7bfb | |||
| 22055176d2 | |||
| 7a080b624e | |||
| 2655739940 | |||
| 348d300a71 | |||
| 6f07bdb152 | |||
| 55a9bc1e70 | |||
| 20a7ce7c1b | |||
| 8cd4492407 | |||
| 9d8f318aab | |||
| fcbbbef22d | |||
| 6041adc164 | |||
| 408da43657 | |||
| ed04ed5745 | |||
| b354940369 | |||
| bd89aef8ba | |||
| f2bc13dd9e | |||
| 098254f175 | |||
| a8e9fc699a | |||
| cdcd15b6c2 | |||
| 25d645f47a | |||
| fbb3c181cb | |||
| 9af4ca3861 | |||
| 7396aa945a | |||
| d82cef5c53 | |||
| 74f0777c5f | |||
| e334a40661 | |||
| 5bd3c8df40 | |||
| 0aca99fe01 | |||
| c8b5ab75b7 | |||
| 85c5b89593 | |||
| 965760835d | |||
| 4a0b55330c | |||
| 1651beb2c1 | |||
| 2b6063c3e3 | |||
| 56ab83a597 | |||
| dad0dc8d86 | |||
| d3c4fdb8ff | |||
| d632dd7d74 | |||
| 0671ef5593 | |||
| 672481f1f0 | |||
| 1dbfc0f930 | |||
| 1798bd3fd1 | |||
| f0b55a68ee | |||
| 623dda7174 | |||
| aee9e11a40 | |||
| bd63c1aa56 | |||
| 9eb05807c6 | |||
| 654f513a4c | |||
| 759cdf3fc8 | |||
| f423505746 | |||
| af165d5b6d | |||
| 6779117f72 | |||
| ce04011096 | |||
| cdc73526a8 | |||
| 425c7d90f0 | |||
| fca266ebbb | |||
| 0016e21cd6 | |||
| 728feaecf0 | |||
| d1dc0e9a54 | |||
| 072bca69d8 | |||
| e26e13fbb7 | |||
| 6df2f080c9 | |||
| b84745d497 | |||
| 44954497bc | |||
| 22ef5da20d | |||
| 5758c370ea | |||
| 8ee6de6d96 | |||
| a4b9f1a92c | |||
| f4389e48ce | |||
| 803f572e66 | |||
| 6183184b96 | |||
| 45e6fdb315 | |||
| a037be774d | |||
| 8a005538db | |||
| 657d0272e3 | |||
| 159120cddd | |||
| 3f0d49e502 | |||
| 4890788974 | |||
| 3075eaa3b0 | |||
| 3bc6830411 | |||
| e4d6ec94b5 | |||
| 6260455ba7 | |||
| c397975af8 | |||
| 965a6cbff9 | |||
| 34a96290fa | |||
| dd0d3fdbbe | |||
| b921752331 | |||
| 0fa40ce58f | |||
| bf537f1c6d | |||
| e86b331138 | |||
| 6c841a691e | |||
| 8d9511a08f | |||
| e71d715b9b | |||
| e61cdf335f | |||
| 6c3bc5ebad | |||
| 283394a4fd | |||
| 5538507e90 | |||
| b4f27ef8e7 | |||
| 3473e31eb1 | |||
| fa0da004a9 | |||
| 24a893cf83 | |||
| bf1564bd2c | |||
| cd44d409ba | |||
| 9a326d5636 | |||
| b10381fbe0 | |||
| 0eaaa8fee3 | |||
| 58adbeef88 | |||
| 93572e5318 | |||
| 94f149b21e | |||
| 66753416b5 | |||
| 133e4bfbee | |||
| f122771fb1 | |||
| 08a863e1e8 | |||
| 8aa7257f65 | |||
| 75a823a36a | |||
| 5e0947c5b5 | |||
| 0b70b46aad | |||
| ab01ba6e7e | |||
| cc08c09592 | |||
| f97da9eca9 | |||
| 2e70f61fb0 | |||
| 1ed8069a3a | |||
| c88cdb5610 | |||
| e1064d2195 | |||
| bf489b96d3 | |||
| b8155452ac | |||
| 08552a7a4c | |||
| fdad91fd96 | |||
| 5f3f60898c | |||
| 057d02de25 | |||
| 43ac59da85 | |||
| ab5c6990a5 | |||
| 9369d13e59 | |||
| 805c24c32c | |||
| 63e3a5ebb2 | |||
| eaa6c96a79 | |||
| cf3eb580bf | |||
| 2b01c85d14 | |||
| c09f5c5fd3 | |||
| 9232e28446 | |||
| b6a4ae4ad5 | |||
| 84de0c3255 | |||
| a074102262 | |||
| da4406ea50 | |||
| 166862a096 | |||
| 82c33c7105 | |||
| 41e9e9fe14 |
@@ -1,6 +1,6 @@
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: bepsays/ci-goreleaser:1.1700.200
|
||||
- image: bepsays/ci-goreleaser:1.1800.000
|
||||
environment:
|
||||
CGO_ENABLED: "0"
|
||||
|
||||
@@ -18,7 +18,6 @@ jobs:
|
||||
go mod download
|
||||
sleep 5
|
||||
go mod verify
|
||||
go test -p 1 ./...
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: .
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: 'Bug report'
|
||||
labels: 'bug'
|
||||
labels: 'Bug, NeedsTriage'
|
||||
assignees: ''
|
||||
about: Create a report to help us improve
|
||||
---
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
---
|
||||
name: Proposal
|
||||
about: Suggest an idea for Hugo
|
||||
about: Propose a new feature for Hugo
|
||||
title: ''
|
||||
labels: 'Proposal'
|
||||
labels: 'Proposal, NeedsTriage'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
<!-- Describe this new feature. Think about if it really belongs in the Hugo core module; you may want to discuss it on https://discourse.gohugo.io/ first. -->
|
||||
@@ -1,23 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 120
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 30
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- Keep
|
||||
- Security
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: Stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
|
||||
|
||||
If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
|
||||
|
||||
If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
|
||||
|
||||
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
@@ -0,0 +1,47 @@
|
||||
name: 'Close stale and lock closed issues and PRs'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@08e671be8ac8944d0e132aa71d0ae8ccfb347675
|
||||
with:
|
||||
issue-inactive-days: 21
|
||||
add-issue-labels: 'Outdated'
|
||||
issue-comment: >
|
||||
This issue has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
pr-comment: >
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
- uses: actions/stale@04a1828bc18ada028d85a0252a47cd2963a91abe
|
||||
with:
|
||||
operations-per-run: 999
|
||||
days-before-issue-stale: 365
|
||||
days-before-pr-stale: 200
|
||||
days-before-issue-close: 21
|
||||
days-before-pr-close: 21
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
|
||||
|
||||
If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
|
||||
|
||||
If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
|
||||
|
||||
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
stale-pr-message: This PR has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
|
||||
|
||||
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell is in a comment.
|
||||
|
||||
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
stale-issue-label: 'Stale'
|
||||
exempt-issue-labels: 'Keep,Security'
|
||||
stale-pr-label: 'Stale'
|
||||
exempt-pr-labels: 'Keep,Security'
|
||||
@@ -7,7 +7,11 @@ jobs:
|
||||
GO111MODULE: on
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x, 1.17.x]
|
||||
# Note: We upgraded to Go 1.18 in Hugo v0.95.0
|
||||
# Go 1.18 had some breaking changes on the source level which means Hugo cannot be built
|
||||
# with older Go versions, but the improvements in Go 1.18 were too good to pass on (e.g. break and continue).
|
||||
# Note that you don't need Go (or Go 1.18) to run a pre-built binary.
|
||||
go-version: [1.18.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -24,7 +28,7 @@ jobs:
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install Mage
|
||||
run: go get github.com/magefile/mage@07afc7d24f4d6d6442305d49552f04fbda5ccb3e
|
||||
run: go install github.com/magefile/mage@07afc7d24f4d6d6442305d49552f04fbda5ccb3e
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@7570212ae20b63653481675fb1ff62d1073632b0
|
||||
- name: Checkout code
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/hugo
|
||||
docs/public*
|
||||
/.idea
|
||||
.vscode/*
|
||||
hugo.exe
|
||||
*.test
|
||||
*.prof
|
||||
@@ -25,3 +26,4 @@ resources/sunset.jpg
|
||||
|
||||
vendor
|
||||
|
||||
.hugo_build.lock
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
# Contributing to Hugo
|
||||
|
||||
**Note March 16th 2022:** We are currently very constrained on human resources to do code reviews, so we currently require any new Pull Requests to be limited to bug fixes closing an existing issue. Also, we have updated to Go 1.18, but we will currently not accept any generic rewrites, "interface{} to any" replacements and similar.
|
||||
|
||||
We welcome contributions to Hugo of any kind including documentation, themes,
|
||||
organization, tutorials, blog posts, bug reports, issues, feature requests,
|
||||
feature implementations, pull requests, answering questions on the forum,
|
||||
@@ -124,7 +126,7 @@ go install
|
||||
For some convenient build and test targets, you also will want to install Mage:
|
||||
|
||||
```bash
|
||||
go get github.com/magefile/mage
|
||||
go install github.com/magefile/mage
|
||||
```
|
||||
|
||||
Now, to make a change to Hugo's source:
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
FROM golang:1.16-alpine AS build
|
||||
FROM golang:1.18-alpine AS build
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=extended .
|
||||
@@ -19,8 +19,8 @@ COPY . /go/src/github.com/gohugoio/hugo/
|
||||
|
||||
# gcc/g++ are required to build SASS libraries for extended version
|
||||
RUN apk update && \
|
||||
apk add --no-cache gcc g++ musl-dev && \
|
||||
go get github.com/magefile/mage
|
||||
apk add --no-cache gcc g++ musl-dev git && \
|
||||
go install github.com/magefile/mage
|
||||
|
||||
RUN mage hugo && mage install
|
||||
|
||||
|
||||
@@ -54,11 +54,11 @@ Use the [installation instructions in the Hugo documentation](https://gohugo.io/
|
||||
#### Prerequisite Tools
|
||||
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Go (we test it with the last 2 major versions; but note that Hugo 0.81.0 only builds with >= Go 1.16.)](https://golang.org/dl/)
|
||||
* [Go (we test it with the last 2 major versions; but note that Hugo 0.95.0 only builds with >= Go 1.18.)](https://golang.org/dl/)
|
||||
|
||||
#### Fetch from GitHub
|
||||
|
||||
Since Hugo 0.48, Hugo uses the Go Modules support built into Go 1.11 to build. The easiest is to clone Hugo in a directory outside of `GOPATH`, as in the following example:
|
||||
To fetch and build the source from GitHub:
|
||||
|
||||
```bash
|
||||
mkdir $HOME/src
|
||||
@@ -85,6 +85,8 @@ git clone git@github.com:gohugoio/hugo.git
|
||||
```
|
||||
## Contributing to Hugo
|
||||
|
||||
**Note March 16th 2022:** We are currently very constrained on human resources to do code reviews, so we currently require any new Pull Requests to be limited to bug fixes closing an existing issue. Also, we have updated to Go 1.18, but we will currently not accept any generic rewrites, "interface{} to any" replacements and similar.
|
||||
|
||||
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
We welcome contributions to Hugo of any kind including documentation, themes,
|
||||
@@ -128,97 +130,97 @@ Hugo stands on the shoulder of many great open source libraries.
|
||||
|
||||
If you run `hugo env -v` you will get a complete and up to date list.
|
||||
|
||||
In Hugo 0.83.0 that list is, in lexical order:
|
||||
In Hugo 0.89.0 that list is, in lexical order:
|
||||
|
||||
```json
|
||||
```
|
||||
cloud.google.com/go/storage="v1.10.0"
|
||||
cloud.google.com/go="v0.81.0"
|
||||
cloud.google.com/go="v0.87.0"
|
||||
github.com/Azure/azure-pipeline-go="v0.2.2"
|
||||
github.com/Azure/azure-storage-blob-go="v0.9.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/BurntSushi/toml="v0.3.1"
|
||||
github.com/PuerkitoBio/purell="v1.1.1"
|
||||
github.com/PuerkitoBio/urlesc="v0.0.0-20170810143723-de5bf2ad4578"
|
||||
github.com/alecthomas/chroma="v0.9.1"
|
||||
github.com/alecthomas/chroma="v0.9.4"
|
||||
github.com/armon/go-radix="v1.0.0"
|
||||
github.com/aws/aws-sdk-go="v1.38.23"
|
||||
github.com/aws/aws-sdk-go="v1.41.14"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.1.2"
|
||||
github.com/bep/godartsass="v0.12.0"
|
||||
github.com/bep/golibsass="v1.0.0"
|
||||
github.com/bep/gowebp="v0.1.0"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/cli/safeexec="v1.0.0"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.0"
|
||||
github.com/danwakefield/fnmatch="v0.0.0-20160403171240-cbb64ac3d964"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.4.0"
|
||||
github.com/dustin/go-humanize="v1.0.0"
|
||||
github.com/evanw/esbuild="v0.11.16"
|
||||
github.com/fsnotify/fsnotify="v1.4.9"
|
||||
github.com/getkin/kin-openapi="v0.61.0"
|
||||
github.com/evanw/esbuild="v0.13.12"
|
||||
github.com/fsnotify/fsnotify="v1.5.1"
|
||||
github.com/getkin/kin-openapi="v0.80.0"
|
||||
github.com/ghodss/yaml="v1.0.0"
|
||||
github.com/go-openapi/jsonpointer="v0.19.5"
|
||||
github.com/go-openapi/swag="v0.19.5"
|
||||
github.com/gobuffalo/flect="v0.2.2"
|
||||
github.com/gobuffalo/flect="v0.2.3"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20210430103248-4c28c89f8013"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v0.14.0"
|
||||
github.com/golang/groupcache="v0.0.0-20200121045136-8c9f03a8e57e"
|
||||
github.com/golang/protobuf="v1.5.2"
|
||||
github.com/google/go-cmp="v0.5.5"
|
||||
github.com/google/go-cmp="v0.5.6"
|
||||
github.com/google/uuid="v1.1.2"
|
||||
github.com/google/wire="v0.4.0"
|
||||
github.com/googleapis/gax-go/v2="v2.0.5"
|
||||
github.com/googleapis/gax-go="v2.0.2+incompatible"
|
||||
github.com/gorilla/websocket="v1.4.2"
|
||||
github.com/hashicorp/hcl="v1.0.0"
|
||||
github.com/inconshreveable/mousetrap="v1.0.0"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/jmespath/go-jmespath="v0.4.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.8"
|
||||
github.com/magiconair/properties="v1.8.1"
|
||||
github.com/mailru/easyjson="v0.0.0-20190626092158-b2ccc519800e"
|
||||
github.com/mattn/go-ieproxy="v0.0.1"
|
||||
github.com/mattn/go-isatty="v0.0.12"
|
||||
github.com/mattn/go-isatty="v0.0.14"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/miekg/mmark="v1.3.6"
|
||||
github.com/mitchellh/hashstructure="v1.0.0"
|
||||
github.com/mitchellh/mapstructure="v1.3.3"
|
||||
github.com/mitchellh/hashstructure="v1.1.0"
|
||||
github.com/mitchellh/mapstructure="v1.4.2"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.5.0"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/pelletier/go-toml="v1.9.0"
|
||||
github.com/pelletier/go-toml/v2="v2.0.0-beta.3.0.20210727221244-fa0796069526"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.8.0"
|
||||
github.com/russross/blackfriday/v2="v2.0.1"
|
||||
github.com/russross/blackfriday="v1.5.3-0.20200218234912-41c5fccfd6f6"
|
||||
github.com/rwcarlsen/goexif="v0.0.0-20190401172101-9e8deecbddbd"
|
||||
github.com/sanity-io/litter="v1.5.0"
|
||||
github.com/sanity-io/litter="v1.5.1"
|
||||
github.com/sass/libsass="3.6.5"
|
||||
github.com/shurcooL/sanitized_anchor_name="v1.0.0"
|
||||
github.com/spf13/afero="v1.6.0"
|
||||
github.com/spf13/cast="v1.3.1"
|
||||
github.com/spf13/cobra="v1.1.3"
|
||||
github.com/spf13/cast="v1.4.1"
|
||||
github.com/spf13/cobra="v1.2.1"
|
||||
github.com/spf13/fsync="v0.9.0"
|
||||
github.com/spf13/jwalterweatherman="v1.1.0"
|
||||
github.com/spf13/pflag="v1.0.5"
|
||||
github.com/spf13/viper="v1.7.1"
|
||||
github.com/subosito/gotenv="v1.2.0"
|
||||
github.com/tdewolff/minify/v2="v2.9.16"
|
||||
github.com/tdewolff/parse/v2="v2.5.14"
|
||||
github.com/tdewolff/minify/v2="v2.9.22"
|
||||
github.com/tdewolff/parse/v2="v2.5.21"
|
||||
github.com/webmproject/libwebp="v1.2.0"
|
||||
github.com/yuin/goldmark-highlighting="v0.0.0-20200307114337-60d527fdb691"
|
||||
github.com/yuin/goldmark="v1.3.5"
|
||||
github.com/yuin/goldmark="v1.4.2"
|
||||
go.opencensus.io="v0.23.0"
|
||||
gocloud.dev="v0.20.0"
|
||||
golang.org/x/image="v0.0.0-20210220032944-ac19c3e999fb"
|
||||
golang.org/x/net="v0.0.0-20210316092652-d523dce5a7f4"
|
||||
golang.org/x/oauth2="v0.0.0-20210413134643-5e61552d6c78"
|
||||
golang.org/x/net="v0.0.0-20210614182718-04defd469f4e"
|
||||
golang.org/x/oauth2="v0.0.0-20210628180205-a41e5a781914"
|
||||
golang.org/x/sync="v0.0.0-20210220032951-036812b2e83c"
|
||||
golang.org/x/sys="v0.0.0-20210412220455-f1c623a9e750"
|
||||
golang.org/x/text="v0.3.6"
|
||||
golang.org/x/sys="v0.0.0-20210908233432-aa78b53d3365"
|
||||
golang.org/x/text="v0.3.7"
|
||||
golang.org/x/xerrors="v0.0.0-20200804184101-5ec99f83aff1"
|
||||
google.golang.org/api="v0.45.0"
|
||||
google.golang.org/genproto="v0.0.0-20210413151531-c14fb6ef47c3"
|
||||
google.golang.org/grpc="v1.37.0"
|
||||
google.golang.org/protobuf="v1.26.0"
|
||||
gopkg.in/ini.v1="v1.51.1"
|
||||
google.golang.org/api="v0.51.0"
|
||||
google.golang.org/genproto="v0.0.0-20210716133855-ce7ef5c701ea"
|
||||
google.golang.org/grpc="v1.39.0"
|
||||
google.golang.org/protobuf="v1.27.1"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
var bufferPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return &bytes.Buffer{}
|
||||
},
|
||||
}
|
||||
|
||||
Vendored
+12
-2
@@ -175,7 +175,12 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
return info, r, nil
|
||||
}
|
||||
|
||||
r, err := create()
|
||||
var (
|
||||
r io.ReadCloser
|
||||
err error
|
||||
)
|
||||
|
||||
r, err = create()
|
||||
if err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
@@ -206,7 +211,12 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
return info, b, err
|
||||
}
|
||||
|
||||
b, err := create()
|
||||
var (
|
||||
b []byte
|
||||
err error
|
||||
)
|
||||
|
||||
b, err = create()
|
||||
if err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
Vendored
+17
-6
@@ -34,19 +34,21 @@ const (
|
||||
cachesConfigKey = "caches"
|
||||
|
||||
resourcesGenDir = ":resourceDir/_gen"
|
||||
cacheDirProject = ":cacheDir/:project"
|
||||
)
|
||||
|
||||
var defaultCacheConfig = Config{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: ":cacheDir/:project",
|
||||
Dir: cacheDirProject,
|
||||
}
|
||||
|
||||
const (
|
||||
cacheKeyGetJSON = "getjson"
|
||||
cacheKeyGetCSV = "getcsv"
|
||||
cacheKeyImages = "images"
|
||||
cacheKeyAssets = "assets"
|
||||
cacheKeyModules = "modules"
|
||||
cacheKeyGetJSON = "getjson"
|
||||
cacheKeyGetCSV = "getcsv"
|
||||
cacheKeyImages = "images"
|
||||
cacheKeyAssets = "assets"
|
||||
cacheKeyModules = "modules"
|
||||
cacheKeyGetResource = "getresource"
|
||||
)
|
||||
|
||||
type Configs map[string]Config
|
||||
@@ -70,6 +72,10 @@ var defaultCacheConfigs = Configs{
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
},
|
||||
cacheKeyGetResource: Config{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: cacheDirProject,
|
||||
},
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -111,6 +117,11 @@ func (f Caches) AssetsCache() *Cache {
|
||||
return f[cacheKeyAssets]
|
||||
}
|
||||
|
||||
// GetResourceCache gets the file cache for remote resources.
|
||||
func (f Caches) GetResourceCache() *Cache {
|
||||
return f[cacheKeyGetResource]
|
||||
}
|
||||
|
||||
func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
|
||||
c := make(Configs)
|
||||
valid := make(map[string]bool)
|
||||
|
||||
+12
-6
@@ -50,7 +50,8 @@ maxAge = "11h"
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
|
||||
[caches.getResource]
|
||||
dir = "/path/to/c4"
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
@@ -59,7 +60,7 @@ dir = "/path/to/c3"
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(len(decoded), qt.Equals, 5)
|
||||
c.Assert(len(decoded), qt.Equals, 6)
|
||||
|
||||
c2 := decoded["getcsv"]
|
||||
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
|
||||
@@ -68,6 +69,10 @@ dir = "/path/to/c3"
|
||||
c3 := decoded["images"]
|
||||
c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1))
|
||||
c.Assert(c3.Dir, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images"))
|
||||
|
||||
c4 := decoded["getresource"]
|
||||
c.Assert(c4.MaxAge, qt.Equals, time.Duration(-1))
|
||||
c.Assert(c4.Dir, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource"))
|
||||
}
|
||||
|
||||
func TestDecodeConfigIgnoreCache(t *testing.T) {
|
||||
@@ -94,7 +99,8 @@ maxAge = 3456
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
|
||||
[caches.getResource]
|
||||
dir = "/path/to/c4"
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
@@ -103,7 +109,7 @@ dir = "/path/to/c3"
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(len(decoded), qt.Equals, 5)
|
||||
c.Assert(len(decoded), qt.Equals, 6)
|
||||
|
||||
for _, v := range decoded {
|
||||
c.Assert(v.MaxAge, qt.Equals, time.Duration(0))
|
||||
@@ -129,7 +135,7 @@ func TestDecodeConfigDefault(t *testing.T) {
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(len(decoded), qt.Equals, 5)
|
||||
c.Assert(len(decoded), qt.Equals, 6)
|
||||
|
||||
imgConfig := decoded[cacheKeyImages]
|
||||
jsonConfig := decoded[cacheKeyGetJSON]
|
||||
@@ -178,7 +184,7 @@ dir = "/"
|
||||
}
|
||||
|
||||
func newTestConfig() config.Provider {
|
||||
cfg := config.New()
|
||||
cfg := config.NewWithTestDefaults()
|
||||
cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
|
||||
cfg.Set("contentDir", "content")
|
||||
cfg.Set("dataDir", "data")
|
||||
|
||||
Vendored
+1
@@ -342,6 +342,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
c.Assert(err, qt.IsNil)
|
||||
initConfig(fs, cfg)
|
||||
config.SetBaseTestDefaults(cfg)
|
||||
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, nil)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
|
||||
Vendored
+2
-2
@@ -30,7 +30,7 @@ type Cache struct {
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
value interface{}
|
||||
value any
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func (c *Cache) Clear() {
|
||||
// create will be called and cached.
|
||||
// This method is thread safe. It also guarantees that the create func for a given
|
||||
// key is invoked only once for this cache.
|
||||
func (c *Cache) GetOrCreate(key string, create func() (interface{}, error)) (interface{}, error) {
|
||||
func (c *Cache) GetOrCreate(key string, create func() (any, error)) (any, error) {
|
||||
c.mu.RLock()
|
||||
entry, found := c.cache[key]
|
||||
c.mu.RUnlock()
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ func TestNamedCache(t *testing.T) {
|
||||
cache := New()
|
||||
|
||||
counter := 0
|
||||
create := func() (interface{}, error) {
|
||||
create := func() (any, error) {
|
||||
counter++
|
||||
return counter, nil
|
||||
}
|
||||
@@ -58,8 +58,8 @@ func TestNamedCacheConcurrent(t *testing.T) {
|
||||
|
||||
cache := New()
|
||||
|
||||
create := func(i int) func() (interface{}, error) {
|
||||
return func() (interface{}, error) {
|
||||
create := func(i int) func() (any, error) {
|
||||
return func() (any, error) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -505,7 +505,7 @@ func typeName(name, pkg string) string {
|
||||
|
||||
func uniqueNonEmptyStrings(s []string) []string {
|
||||
var unique []string
|
||||
set := map[string]interface{}{}
|
||||
set := map[string]any{}
|
||||
for _, val := range s {
|
||||
if val == "" {
|
||||
continue
|
||||
|
||||
@@ -85,7 +85,7 @@ type I interface {
|
||||
Method3(myint int, mystring string)
|
||||
Method5() (string, error)
|
||||
Method6() *net.IP
|
||||
Method7() interface{}
|
||||
Method7() any
|
||||
Method8() herrors.ErrorContext
|
||||
method2()
|
||||
method9() os.FileInfo
|
||||
|
||||
+74
-15
@@ -17,6 +17,7 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -29,6 +30,7 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
@@ -41,6 +43,7 @@ import (
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/bep/overlayfs"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
@@ -61,6 +64,8 @@ type commandeer struct {
|
||||
logger loggers.Logger
|
||||
serverConfig *config.Server
|
||||
|
||||
buildLock func() (unlock func(), err error)
|
||||
|
||||
// Loading state
|
||||
mustHaveConfigFile bool
|
||||
failOnInitErr bool
|
||||
@@ -70,8 +75,10 @@ type commandeer struct {
|
||||
// be fast enough that we could maybe just add it for all server modes.
|
||||
changeDetector *fileChangeDetector
|
||||
|
||||
// We need to reuse this on server rebuilds.
|
||||
destinationFs afero.Fs
|
||||
// We need to reuse these on server rebuilds.
|
||||
// These 2 will be different if --renderStaticToDisk is set.
|
||||
publishDirFs afero.Fs
|
||||
publishDirServerFs afero.Fs
|
||||
|
||||
h *hugoBuilderCommon
|
||||
ftch flagsToConfigHandler
|
||||
@@ -86,10 +93,12 @@ type commandeer struct {
|
||||
// Used in cases where we get flooded with events in server mode.
|
||||
debounce func(f func())
|
||||
|
||||
serverPorts []int
|
||||
serverPorts []serverPortListener
|
||||
|
||||
languagesConfigured bool
|
||||
languages langs.Languages
|
||||
doLiveReload bool
|
||||
renderStaticToDisk bool
|
||||
fastRenderMode bool
|
||||
showErrorInBrowser bool
|
||||
wasError bool
|
||||
@@ -103,6 +112,11 @@ type commandeer struct {
|
||||
buildErr error
|
||||
}
|
||||
|
||||
type serverPortListener struct {
|
||||
p int
|
||||
ln net.Listener
|
||||
}
|
||||
|
||||
func newCommandeerHugoState() *commandeerHugoState {
|
||||
return &commandeerHugoState{
|
||||
created: make(chan struct{}),
|
||||
@@ -118,14 +132,14 @@ func (c *commandeer) errCount() int {
|
||||
return int(c.logger.LogCounters().ErrorCounter.Count())
|
||||
}
|
||||
|
||||
func (c *commandeer) getErrorWithContext() interface{} {
|
||||
func (c *commandeer) getErrorWithContext() any {
|
||||
errCount := c.errCount()
|
||||
|
||||
if errCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
m := make(map[string]any)
|
||||
|
||||
m["Error"] = errors.New(removeErrorPrefixFromLog(c.logger.Errors()))
|
||||
m["Version"] = hugo.BuildVersionString()
|
||||
@@ -144,7 +158,7 @@ func (c *commandeer) getErrorWithContext() interface{} {
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *commandeer) Set(key string, value interface{}) {
|
||||
func (c *commandeer) Set(key string, value any) {
|
||||
if c.configured {
|
||||
panic("commandeer cannot be changed")
|
||||
}
|
||||
@@ -152,7 +166,8 @@ func (c *commandeer) Set(key string, value interface{}) {
|
||||
}
|
||||
|
||||
func (c *commandeer) initFs(fs *hugofs.Fs) error {
|
||||
c.destinationFs = fs.Destination
|
||||
c.publishDirFs = fs.PublishDir
|
||||
c.publishDirServerFs = fs.PublishDirServer
|
||||
c.DepsCfg.Fs = fs
|
||||
|
||||
return nil
|
||||
@@ -366,21 +381,57 @@ func (c *commandeer) loadConfig() error {
|
||||
}
|
||||
|
||||
createMemFs := config.GetBool("renderToMemory")
|
||||
c.renderStaticToDisk = config.GetBool("renderStaticToDisk")
|
||||
|
||||
if createMemFs {
|
||||
// Rendering to memoryFS, publish to Root regardless of publishDir.
|
||||
config.Set("publishDir", "/")
|
||||
config.Set("publishDirStatic", "/")
|
||||
} else if c.renderStaticToDisk {
|
||||
// Hybrid, render dynamic content to Root.
|
||||
config.Set("publishDirStatic", config.Get("publishDir"))
|
||||
config.Set("publishDir", "/")
|
||||
|
||||
}
|
||||
|
||||
c.fsCreate.Do(func() {
|
||||
fs := hugofs.NewFrom(sourceFs, config)
|
||||
// Assume both source and destination are using same filesystem.
|
||||
fs := hugofs.NewFromSourceAndDestination(sourceFs, sourceFs, config)
|
||||
|
||||
if c.destinationFs != nil {
|
||||
if c.publishDirFs != nil {
|
||||
// Need to reuse the destination on server rebuilds.
|
||||
fs.Destination = c.destinationFs
|
||||
} else if createMemFs {
|
||||
// Hugo writes the output to memory instead of the disk.
|
||||
fs.Destination = new(afero.MemMapFs)
|
||||
fs.PublishDir = c.publishDirFs
|
||||
fs.PublishDirServer = c.publishDirServerFs
|
||||
} else {
|
||||
if c.renderStaticToDisk {
|
||||
publishDirStatic := config.GetString("publishDirStatic")
|
||||
workingDir := config.GetString("workingDir")
|
||||
absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
|
||||
|
||||
fs = hugofs.NewFromSourceAndDestination(sourceFs, afero.NewMemMapFs(), config)
|
||||
// Writes the dynamic output to memory,
|
||||
// while serve others directly from /public on disk.
|
||||
dynamicFs := fs.PublishDir
|
||||
staticFs := afero.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic)
|
||||
|
||||
// Serve from both the static and dynamic fs,
|
||||
// the first will take priority.
|
||||
// THis is a read-only filesystem,
|
||||
// we do all the writes to
|
||||
// fs.Destination and fs.DestinationStatic.
|
||||
fs.PublishDirServer = overlayfs.New(
|
||||
overlayfs.Options{
|
||||
Fss: []afero.Fs{
|
||||
dynamicFs,
|
||||
staticFs,
|
||||
},
|
||||
},
|
||||
)
|
||||
fs.PublishDirStatic = staticFs
|
||||
} else if createMemFs {
|
||||
// Hugo writes the output to memory instead of the disk.
|
||||
fs = hugofs.NewFromSourceAndDestination(sourceFs, afero.NewMemMapFs(), config)
|
||||
}
|
||||
}
|
||||
|
||||
if c.fastRenderMode {
|
||||
@@ -394,12 +445,15 @@ func (c *commandeer) loadConfig() error {
|
||||
}
|
||||
|
||||
changeDetector.PrepareNew()
|
||||
fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
|
||||
fs.PublishDir = hugofs.NewHashingFs(fs.PublishDir, changeDetector)
|
||||
fs.PublishDirStatic = hugofs.NewHashingFs(fs.PublishDirStatic, changeDetector)
|
||||
c.changeDetector = changeDetector
|
||||
}
|
||||
|
||||
if c.Cfg.GetBool("logPathWarnings") {
|
||||
fs.Destination = hugofs.NewCreateCountingFs(fs.Destination)
|
||||
// Note that we only care about the "dynamic creates" here,
|
||||
// so skip the static fs.
|
||||
fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir)
|
||||
}
|
||||
|
||||
// To debug hard-to-find path issues.
|
||||
@@ -418,7 +472,12 @@ func (c *commandeer) loadConfig() error {
|
||||
if h == nil || c.failOnInitErr {
|
||||
err = createErr
|
||||
}
|
||||
|
||||
c.hugoSites = h
|
||||
// TODO(bep) improve.
|
||||
if c.buildLock == nil && h != nil {
|
||||
c.buildLock = h.LockBuild
|
||||
}
|
||||
close(c.created)
|
||||
})
|
||||
|
||||
|
||||
+13
-16
@@ -18,10 +18,9 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib/paths"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
hpaths "github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -48,7 +47,6 @@ func (b *commandsBuilder) addAll() *commandsBuilder {
|
||||
newVersionCmd(),
|
||||
newEnvCmd(),
|
||||
b.newConfigCmd(),
|
||||
newCheckCmd(),
|
||||
b.newDeployCmd(),
|
||||
b.newConvertCmd(),
|
||||
b.newNewCmd(),
|
||||
@@ -131,8 +129,7 @@ type hugoCmd struct {
|
||||
|
||||
var _ cmder = (*nilCommand)(nil)
|
||||
|
||||
type nilCommand struct {
|
||||
}
|
||||
type nilCommand struct{}
|
||||
|
||||
func (c *nilCommand) getCommand() *cobra.Command {
|
||||
return nil
|
||||
@@ -152,7 +149,7 @@ func (b *commandsBuilder) newHugoCmd() *hugoCmd {
|
||||
Hugo is a Fast and Flexible Static Site Generator
|
||||
built with love by spf13 and friends in Go.
|
||||
|
||||
Complete documentation is available at http://gohugo.io/.`,
|
||||
Complete documentation is available at https://gohugo.io/.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
defer cc.timeTrack(time.Now(), "Total")
|
||||
cfgInit := func(c *commandeer) error {
|
||||
@@ -245,14 +242,14 @@ func (cc *hugoBuilderCommon) timeTrack(start time.Time, name string) {
|
||||
|
||||
func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string {
|
||||
if cc.cfgDir != "" {
|
||||
return paths.AbsPathify(baseDir, cc.cfgDir)
|
||||
return hpaths.AbsPathify(baseDir, cc.cfgDir)
|
||||
}
|
||||
|
||||
if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found {
|
||||
return paths.AbsPathify(baseDir, v)
|
||||
return hpaths.AbsPathify(baseDir, v)
|
||||
}
|
||||
|
||||
return paths.AbsPathify(baseDir, "config")
|
||||
return hpaths.AbsPathify(baseDir, "config")
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string {
|
||||
@@ -281,7 +278,6 @@ func (cc *hugoBuilderCommon) handleCommonBuilderFlags(cmd *cobra.Command) {
|
||||
cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
cmd.PersistentFlags().StringVarP(&cc.environment, "environment", "e", "", "build environment")
|
||||
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
|
||||
cmd.PersistentFlags().BoolP("ignoreVendor", "", false, "ignores any _vendor directory")
|
||||
cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
|
||||
}
|
||||
|
||||
@@ -297,21 +293,22 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
|
||||
cmd.Flags().StringP("destination", "d", "", "filesystem path to write files to")
|
||||
cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
|
||||
cmd.Flags().StringVarP(&cc.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. http://spf13.com/")
|
||||
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date and author info to the pages")
|
||||
cmd.Flags().StringVarP(&cc.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. https://spf13.com/")
|
||||
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date, author, and CODEOWNERS info to the pages")
|
||||
cmd.Flags().BoolVar(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
|
||||
cmd.Flags().StringVar(&cc.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
|
||||
|
||||
cmd.Flags().BoolVar(&loggers.PanicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
|
||||
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
|
||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||
cmd.Flags().BoolP("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("i18n-warnings", "", false, "print missing translations")
|
||||
cmd.Flags().BoolP("path-warnings", "", false, "print warnings on duplicate target paths etc.")
|
||||
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.")
|
||||
cmd.Flags().StringVarP(&cc.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
|
||||
cmd.Flags().StringVarP(&cc.memprofile, "profile-mem", "", "", "write memory profile to `file`")
|
||||
cmd.Flags().BoolVarP(&cc.printm, "print-mem", "", false, "print memory usage to screen at intervals")
|
||||
cmd.Flags().BoolVarP(&cc.printm, "printMemoryUsage", "", false, "print memory usage to screen at intervals")
|
||||
cmd.Flags().StringVarP(&cc.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`")
|
||||
cmd.Flags().StringVarP(&cc.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)")
|
||||
|
||||
|
||||
+71
-85
@@ -22,8 +22,6 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -38,15 +36,13 @@ import (
|
||||
func TestExecute(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
createSite := func(c *qt.C) (string, func()) {
|
||||
dir, clean, err := createSimpleTestSite(t, testSiteConfig{})
|
||||
c.Assert(err, qt.IsNil)
|
||||
return dir, clean
|
||||
createSite := func(c *qt.C) string {
|
||||
dir := createSimpleTestSite(t, testSiteConfig{})
|
||||
return dir
|
||||
}
|
||||
|
||||
c.Run("hugo", func(c *qt.C) {
|
||||
dir, clean := createSite(c)
|
||||
defer clean()
|
||||
dir := createSite(c)
|
||||
resp := Execute([]string{"-s=" + dir})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
result := resp.Result
|
||||
@@ -56,8 +52,7 @@ func TestExecute(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("hugo, set environment", func(c *qt.C) {
|
||||
dir, clean := createSite(c)
|
||||
defer clean()
|
||||
dir := createSite(c)
|
||||
resp := Execute([]string{"-s=" + dir, "-e=staging"})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
result := resp.Result
|
||||
@@ -65,9 +60,8 @@ func TestExecute(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("convert toJSON", func(c *qt.C) {
|
||||
dir, clean := createSite(c)
|
||||
dir := createSite(c)
|
||||
output := filepath.Join(dir, "myjson")
|
||||
defer clean()
|
||||
resp := Execute([]string{"convert", "toJSON", "-s=" + dir, "-e=staging", "-o=" + output})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
converted := readFileFrom(c, filepath.Join(output, "content", "p1.md"))
|
||||
@@ -75,8 +69,7 @@ func TestExecute(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("config, set environment", func(c *qt.C) {
|
||||
dir, clean := createSite(c)
|
||||
defer clean()
|
||||
dir := createSite(c)
|
||||
out, err := captureStdout(func() error {
|
||||
resp := Execute([]string{"config", "-s=" + dir, "-e=staging"})
|
||||
return resp.Err
|
||||
@@ -86,16 +79,14 @@ func TestExecute(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("deploy, environment set", func(c *qt.C) {
|
||||
dir, clean := createSite(c)
|
||||
defer clean()
|
||||
dir := createSite(c)
|
||||
resp := Execute([]string{"deploy", "-s=" + dir, "-e=staging", "--target=mydeployment", "--dryRun"})
|
||||
c.Assert(resp.Err, qt.Not(qt.IsNil))
|
||||
c.Assert(resp.Err.Error(), qt.Contains, `no driver registered for "hugocloud"`)
|
||||
})
|
||||
|
||||
c.Run("list", func(c *qt.C) {
|
||||
dir, clean := createSite(c)
|
||||
defer clean()
|
||||
dir := createSite(c)
|
||||
out, err := captureStdout(func() error {
|
||||
resp := Execute([]string{"list", "all", "-s=" + dir, "-e=staging"})
|
||||
return resp.Err
|
||||
@@ -105,8 +96,7 @@ func TestExecute(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("new theme", func(c *qt.C) {
|
||||
dir, clean := createSite(c)
|
||||
defer clean()
|
||||
dir := createSite(c)
|
||||
themesDir := filepath.Join(dir, "mythemes")
|
||||
resp := Execute([]string{"new", "theme", "mytheme", "-s=" + dir, "-e=staging", "--themesDir=" + themesDir})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
@@ -115,8 +105,7 @@ func TestExecute(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("new site", func(c *qt.C) {
|
||||
dir, clean := createSite(c)
|
||||
defer clean()
|
||||
dir := createSite(c)
|
||||
siteDir := filepath.Join(dir, "mysite")
|
||||
resp := Execute([]string{"new", "site", siteDir, "-e=staging"})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
@@ -162,22 +151,12 @@ func TestFlags(t *testing.T) {
|
||||
args []string
|
||||
check func(c *qt.C, cmd *serverCmd)
|
||||
}{
|
||||
{
|
||||
// https://github.com/gohugoio/hugo/issues/7642
|
||||
name: "ignoreVendor as bool",
|
||||
args: []string{"server", "--ignoreVendor"},
|
||||
check: func(c *qt.C, cmd *serverCmd) {
|
||||
cfg := config.New()
|
||||
cmd.flagsToConfig(cfg)
|
||||
c.Assert(cfg.Get("ignoreVendor"), qt.Equals, true)
|
||||
},
|
||||
},
|
||||
{
|
||||
// https://github.com/gohugoio/hugo/issues/7642
|
||||
name: "ignoreVendorPaths",
|
||||
args: []string{"server", "--ignoreVendorPaths=github.com/**"},
|
||||
check: func(c *qt.C, cmd *serverCmd) {
|
||||
cfg := config.New()
|
||||
cfg := config.NewWithTestDefaults()
|
||||
cmd.flagsToConfig(cfg)
|
||||
c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**")
|
||||
},
|
||||
@@ -200,13 +179,14 @@ func TestFlags(t *testing.T) {
|
||||
"--navigateToChanged",
|
||||
"--disableLiveReload",
|
||||
"--noHTTPCache",
|
||||
"--i18n-warnings",
|
||||
"--printI18nWarnings",
|
||||
"--destination=/tmp/mydestination",
|
||||
"-b=https://example.com/b/",
|
||||
"--port=1366",
|
||||
"--renderToDisk",
|
||||
"--source=mysource",
|
||||
"--path-warnings",
|
||||
"--printPathWarnings",
|
||||
"--printUnusedTemplates",
|
||||
},
|
||||
check: func(c *qt.C, sc *serverCmd) {
|
||||
c.Assert(sc, qt.Not(qt.IsNil))
|
||||
@@ -217,7 +197,7 @@ func TestFlags(t *testing.T) {
|
||||
c.Assert(sc.serverPort, qt.Equals, 1366)
|
||||
c.Assert(sc.environment, qt.Equals, "testing")
|
||||
|
||||
cfg := config.New()
|
||||
cfg := config.NewWithTestDefaults()
|
||||
sc.flagsToConfig(cfg)
|
||||
c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination")
|
||||
c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent")
|
||||
@@ -230,10 +210,10 @@ func TestFlags(t *testing.T) {
|
||||
|
||||
c.Assert(cfg.GetBool("gc"), qt.Equals, true)
|
||||
|
||||
// The flag is named path-warnings
|
||||
// The flag is named printPathWarnings
|
||||
c.Assert(cfg.GetBool("logPathWarnings"), qt.Equals, true)
|
||||
|
||||
// The flag is named i18n-warnings
|
||||
// The flag is named printI18nWarnings
|
||||
c.Assert(cfg.GetBool("logI18nWarnings"), qt.Equals, true)
|
||||
},
|
||||
},
|
||||
@@ -262,14 +242,8 @@ func TestFlags(t *testing.T) {
|
||||
func TestCommandsExecute(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
dir, clean, err := createSimpleTestSite(t, testSiteConfig{})
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
dirOut, clean2, err := htesting.CreateTempDir(hugofs.Os, "hugo-cli-out")
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
defer clean()
|
||||
defer clean2()
|
||||
dir := createSimpleTestSite(t, testSiteConfig{})
|
||||
dirOut := t.TempDir()
|
||||
|
||||
sourceFlag := fmt.Sprintf("-s=%s", dir)
|
||||
|
||||
@@ -284,11 +258,14 @@ func TestCommandsExecute(t *testing.T) {
|
||||
// no args = hugo build
|
||||
{nil, []string{sourceFlag}, ""},
|
||||
{nil, []string{sourceFlag, "--renderToMemory"}, ""},
|
||||
{[]string{"completion", "bash"}, nil, ""},
|
||||
{[]string{"completion", "fish"}, nil, ""},
|
||||
{[]string{"completion", "powershell"}, nil, ""},
|
||||
{[]string{"completion", "zsh"}, nil, ""},
|
||||
{[]string{"config"}, []string{sourceFlag}, ""},
|
||||
{[]string{"convert", "toTOML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "toml")}, ""},
|
||||
{[]string{"convert", "toYAML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "yaml")}, ""},
|
||||
{[]string{"convert", "toJSON"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "json")}, ""},
|
||||
{[]string{"gen", "autocomplete"}, []string{"--completionfile=" + filepath.Join(dirOut, "autocomplete.txt")}, ""},
|
||||
{[]string{"gen", "chromastyles"}, []string{"--style=manni"}, ""},
|
||||
{[]string{"gen", "doc"}, []string{"--dir=" + filepath.Join(dirOut, "doc")}, ""},
|
||||
{[]string{"gen", "man"}, []string{"--dir=" + filepath.Join(dirOut, "man")}, ""},
|
||||
@@ -303,29 +280,35 @@ func TestCommandsExecute(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
b := newCommandsBuilder().addAll().build()
|
||||
hugoCmd := b.getCommand()
|
||||
test.flags = append(test.flags, "--quiet")
|
||||
hugoCmd.SetArgs(append(test.commands, test.flags...))
|
||||
|
||||
// TODO(bep) capture output and add some simple asserts
|
||||
// TODO(bep) misspelled subcommands does not return an error. We should investigate this
|
||||
// but before that, check for "Error: unknown command".
|
||||
|
||||
_, err := hugoCmd.ExecuteC()
|
||||
if test.expectErrToContain != "" {
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
c.Assert(err.Error(), qt.Contains, test.expectErrToContain)
|
||||
} else {
|
||||
c.Assert(err, qt.IsNil)
|
||||
name := "hugo"
|
||||
if len(test.commands) > 0 {
|
||||
name = test.commands[0]
|
||||
}
|
||||
c.Run(name, func(c *qt.C) {
|
||||
b := newCommandsBuilder().addAll().build()
|
||||
hugoCmd := b.getCommand()
|
||||
test.flags = append(test.flags, "--quiet")
|
||||
hugoCmd.SetArgs(append(test.commands, test.flags...))
|
||||
|
||||
// Assert that we have not left any development debug artifacts in
|
||||
// the code.
|
||||
if b.c != nil {
|
||||
_, ok := b.c.destinationFs.(types.DevMarker)
|
||||
c.Assert(ok, qt.Equals, false)
|
||||
}
|
||||
// TODO(bep) capture output and add some simple asserts
|
||||
// TODO(bep) misspelled subcommands does not return an error. We should investigate this
|
||||
// but before that, check for "Error: unknown command".
|
||||
|
||||
_, err := hugoCmd.ExecuteC()
|
||||
if test.expectErrToContain != "" {
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
c.Assert(err.Error(), qt.Contains, test.expectErrToContain)
|
||||
} else {
|
||||
c.Assert(err, qt.IsNil)
|
||||
}
|
||||
|
||||
// Assert that we have not left any development debug artifacts in
|
||||
// the code.
|
||||
if b.c != nil {
|
||||
_, ok := b.c.publishDirFs.(types.DevMarker)
|
||||
c.Assert(ok, qt.Equals, false)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
@@ -335,11 +318,8 @@ type testSiteConfig struct {
|
||||
contentDir string
|
||||
}
|
||||
|
||||
func createSimpleTestSite(t *testing.T, cfg testSiteConfig) (string, func(), error) {
|
||||
d, clean, e := htesting.CreateTempDir(hugofs.Os, "hugo-cli")
|
||||
if e != nil {
|
||||
return "", nil, e
|
||||
}
|
||||
func createSimpleTestSite(t testing.TB, cfg testSiteConfig) string {
|
||||
dir := t.TempDir()
|
||||
|
||||
cfgStr := `
|
||||
|
||||
@@ -358,21 +338,23 @@ title = "Hugo Commands"
|
||||
contentDir = cfg.contentDir
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Join(d, "public"), 0777)
|
||||
os.MkdirAll(filepath.Join(dir, "public"), 0777)
|
||||
|
||||
// Just the basic. These are for CLI tests, not site testing.
|
||||
writeFile(t, filepath.Join(d, "config.toml"), cfgStr)
|
||||
writeFile(t, filepath.Join(d, "config", "staging", "params.toml"), `myparam="paramstaging"`)
|
||||
writeFile(t, filepath.Join(d, "config", "staging", "deployment.toml"), `
|
||||
writeFile(t, filepath.Join(dir, "config.toml"), cfgStr)
|
||||
writeFile(t, filepath.Join(dir, "config", "staging", "params.toml"), `myparam="paramstaging"`)
|
||||
writeFile(t, filepath.Join(dir, "config", "staging", "deployment.toml"), `
|
||||
[[targets]]
|
||||
name = "mydeployment"
|
||||
URL = "hugocloud://hugotestbucket"
|
||||
`)
|
||||
|
||||
writeFile(t, filepath.Join(d, "config", "testing", "params.toml"), `myparam="paramtesting"`)
|
||||
writeFile(t, filepath.Join(d, "config", "production", "params.toml"), `myparam="paramproduction"`)
|
||||
writeFile(t, filepath.Join(dir, "config", "testing", "params.toml"), `myparam="paramtesting"`)
|
||||
writeFile(t, filepath.Join(dir, "config", "production", "params.toml"), `myparam="paramproduction"`)
|
||||
|
||||
writeFile(t, filepath.Join(d, contentDir, "p1.md"), `
|
||||
writeFile(t, filepath.Join(dir, "static", "myfile.txt"), `Hello World!`)
|
||||
|
||||
writeFile(t, filepath.Join(dir, contentDir, "p1.md"), `
|
||||
---
|
||||
title: "P1"
|
||||
weight: 1
|
||||
@@ -382,28 +364,32 @@ Content
|
||||
|
||||
`)
|
||||
|
||||
writeFile(t, filepath.Join(d, "layouts", "_default", "single.html"), `
|
||||
writeFile(t, filepath.Join(dir, "layouts", "_default", "single.html"), `
|
||||
|
||||
Single: {{ .Title }}
|
||||
|
||||
`)
|
||||
|
||||
writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), `
|
||||
writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), `
|
||||
|
||||
List: {{ .Title }}
|
||||
Environment: {{ hugo.Environment }}
|
||||
|
||||
For issue 9788:
|
||||
{{ $foo :="abc" | resources.FromString "foo.css" | minify | resources.PostProcess }}
|
||||
PostProcess: {{ $foo.RelPermalink }}
|
||||
|
||||
`)
|
||||
|
||||
return d, clean, nil
|
||||
return dir
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, filename, content string) {
|
||||
func writeFile(t testing.TB, filename, content string) {
|
||||
must(t, os.MkdirAll(filepath.Dir(filename), os.FileMode(0755)))
|
||||
must(t, ioutil.WriteFile(filename, []byte(content), os.FileMode(0755)))
|
||||
}
|
||||
|
||||
func must(t *testing.T, err error) {
|
||||
func must(t testing.TB, err error) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+7
-7
@@ -145,13 +145,13 @@ func (m *modMounts) MarshalJSON() ([]byte, error) {
|
||||
if m.verbose {
|
||||
config := m.m.Config()
|
||||
return json.Marshal(&struct {
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Time time.Time `json:"time"`
|
||||
Owner string `json:"owner"`
|
||||
Dir string `json:"dir"`
|
||||
Meta map[string]interface{} `json:"meta"`
|
||||
HugoVersion modules.HugoVersion `json:"hugoVersion"`
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Time time.Time `json:"time"`
|
||||
Owner string `json:"owner"`
|
||||
Dir string `json:"dir"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
HugoVersion modules.HugoVersion `json:"hugoVersion"`
|
||||
|
||||
Mounts []modMount `json:"mounts"`
|
||||
}{
|
||||
|
||||
+4
-4
@@ -142,7 +142,7 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target
|
||||
return nil
|
||||
}
|
||||
|
||||
errMsg := fmt.Errorf("Error processing file %q", p.Path())
|
||||
errMsg := fmt.Errorf("Error processing file %q", p.File().Path())
|
||||
|
||||
site.Log.Infoln("Attempting to convert", p.File().Filename())
|
||||
|
||||
@@ -185,10 +185,10 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target
|
||||
newFilename := p.File().Filename()
|
||||
|
||||
if cc.outputDir != "" {
|
||||
contentDir := strings.TrimSuffix(newFilename, p.Path())
|
||||
contentDir := strings.TrimSuffix(newFilename, p.File().Path())
|
||||
contentDir = filepath.Base(contentDir)
|
||||
|
||||
newFilename = filepath.Join(cc.outputDir, contentDir, p.Path())
|
||||
newFilename = filepath.Join(cc.outputDir, contentDir, p.File().Path())
|
||||
}
|
||||
|
||||
fs := hugofs.Os
|
||||
@@ -202,7 +202,7 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target
|
||||
type parsedFile struct {
|
||||
frontMatterFormat metadecoders.Format
|
||||
frontMatterSource []byte
|
||||
frontMatter map[string]interface{}
|
||||
frontMatter map[string]any
|
||||
|
||||
// Everything after Front Matter
|
||||
content []byte
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
|
||||
package commands
|
||||
|
||||
@@ -31,7 +31,6 @@ func newGenCmd() *genCmd {
|
||||
})
|
||||
|
||||
cc.cmd.AddCommand(
|
||||
newGenautocompleteCmd().getCommand(),
|
||||
newGenDocCmd().getCommand(),
|
||||
newGenManCmd().getCommand(),
|
||||
createGenDocsHelper().getCommand(),
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// 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 (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var _ cmder = (*genautocompleteCmd)(nil)
|
||||
|
||||
type genautocompleteCmd struct {
|
||||
autocompleteTarget string
|
||||
|
||||
// bash, zsh, fish or powershell
|
||||
autocompleteType string
|
||||
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newGenautocompleteCmd() *genautocompleteCmd {
|
||||
cc := &genautocompleteCmd{}
|
||||
|
||||
cc.baseCmd = newBaseCmd(&cobra.Command{
|
||||
Use: "autocomplete",
|
||||
Short: "Generate shell autocompletion script for Hugo",
|
||||
Long: `Generates a shell autocompletion script for Hugo.
|
||||
|
||||
The script is written to the console (stdout).
|
||||
|
||||
To write to file, add the ` + "`--completionfile=/path/to/file`" + ` flag.
|
||||
|
||||
Add ` + "`--type={bash, zsh, fish or powershell}`" + ` flag to set alternative
|
||||
shell type.
|
||||
|
||||
Logout and in again to reload the completion scripts,
|
||||
or just source them in directly:
|
||||
|
||||
$ . /etc/bash_completion or /path/to/file`,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
var target io.Writer
|
||||
|
||||
if cc.autocompleteTarget == "" {
|
||||
target = os.Stdout
|
||||
} else {
|
||||
target, _ = os.OpenFile(cc.autocompleteTarget, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
}
|
||||
|
||||
switch cc.autocompleteType {
|
||||
case "bash":
|
||||
err = cmd.Root().GenBashCompletion(target)
|
||||
case "zsh":
|
||||
err = cmd.Root().GenZshCompletion(target)
|
||||
case "fish":
|
||||
err = cmd.Root().GenFishCompletion(target, true)
|
||||
case "powershell":
|
||||
err = cmd.Root().GenPowerShellCompletion(target)
|
||||
default:
|
||||
return newUserError("Unsupported completion type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cc.autocompleteTarget != "" {
|
||||
jww.FEEDBACK.Println(cc.autocompleteType+" completion file for Hugo saved to", cc.autocompleteTarget)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteTarget, "completionfile", "f", "", "autocompletion file, defaults to stdout")
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteType, "type", "t", "bash", "autocompletion type (bash, zsh, fish, or powershell)")
|
||||
|
||||
return cc
|
||||
}
|
||||
+1
-1
@@ -49,7 +49,7 @@ url: %s
|
||||
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 http://gohugo.io/.
|
||||
of Hugo's command-line interface for https://gohugo.io/.
|
||||
|
||||
It creates one Markdown file per command with front matter suitable
|
||||
for rendering in Hugo.`,
|
||||
|
||||
+4
-4
@@ -53,22 +53,22 @@ func (c commandError) isUserError() bool {
|
||||
return c.userError
|
||||
}
|
||||
|
||||
func newUserError(a ...interface{}) commandError {
|
||||
func newUserError(a ...any) commandError {
|
||||
return commandError{s: fmt.Sprintln(a...), userError: true}
|
||||
}
|
||||
|
||||
func newSystemError(a ...interface{}) commandError {
|
||||
func newSystemError(a ...any) commandError {
|
||||
return commandError{s: fmt.Sprintln(a...), userError: false}
|
||||
}
|
||||
|
||||
func newSystemErrorF(format string, a ...interface{}) commandError {
|
||||
func newSystemErrorF(format string, a ...any) commandError {
|
||||
return commandError{s: fmt.Sprintf(format, a...), userError: false}
|
||||
}
|
||||
|
||||
// Catch some of the obvious user errors from Cobra.
|
||||
// We don't want to show the usage message for every error.
|
||||
// The below may be to generic. Time will show.
|
||||
var userErrorRegexp = regexp.MustCompile("argument|flag|shorthand")
|
||||
var userErrorRegexp = regexp.MustCompile("unknown flag")
|
||||
|
||||
func isUserError(err error) bool {
|
||||
if cErr, ok := err.(commandError); ok && cErr.isUserError() {
|
||||
|
||||
+24
-17
@@ -31,6 +31,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
@@ -199,7 +200,6 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
|
||||
"forceSyncStatic",
|
||||
"noTimes",
|
||||
"noChmod",
|
||||
"ignoreVendor",
|
||||
"ignoreVendorPaths",
|
||||
"templateMetrics",
|
||||
"templateMetricsHints",
|
||||
@@ -217,7 +217,8 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
|
||||
"dryRun",
|
||||
"force",
|
||||
"gc",
|
||||
"i18n-warnings",
|
||||
"printI18nWarnings",
|
||||
"printUnusedTemplates",
|
||||
"invalidateCDN",
|
||||
"layoutDir",
|
||||
"logFile",
|
||||
@@ -244,8 +245,8 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
|
||||
|
||||
// Set some "config aliases"
|
||||
setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
|
||||
setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings", false)
|
||||
setValueFromFlag(cmd.Flags(), "path-warnings", cfg, "logPathWarnings", false)
|
||||
setValueFromFlag(cmd.Flags(), "printI18nWarnings", cfg, "logI18nWarnings", false)
|
||||
setValueFromFlag(cmd.Flags(), "printPathWarnings", cfg, "logPathWarnings", false)
|
||||
}
|
||||
|
||||
func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
|
||||
@@ -281,7 +282,6 @@ func isTerminal() bool {
|
||||
}
|
||||
|
||||
func (c *commandeer) fullBuild(noBuildLock bool) error {
|
||||
|
||||
var (
|
||||
g errgroup.Group
|
||||
langCount map[string]uint64
|
||||
@@ -503,18 +503,22 @@ func (c *commandeer) build() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(bep) Feedback?
|
||||
if !c.h.quiet {
|
||||
fmt.Println()
|
||||
c.hugo().PrintProcessingStats(os.Stdout)
|
||||
fmt.Println()
|
||||
|
||||
if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok {
|
||||
if createCounter, ok := c.publishDirFs.(hugofs.DuplicatesReporter); ok {
|
||||
dupes := createCounter.ReportDuplicates()
|
||||
if dupes != "" {
|
||||
c.logger.Warnln("Duplicate target paths:", dupes)
|
||||
}
|
||||
}
|
||||
|
||||
unusedTemplates := c.hugo().Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
|
||||
for _, unusedTemplate := range unusedTemplates {
|
||||
c.logger.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
|
||||
}
|
||||
}
|
||||
|
||||
if c.h.buildWatch {
|
||||
@@ -542,7 +546,6 @@ func (c *commandeer) build() error {
|
||||
}
|
||||
|
||||
func (c *commandeer) serverBuild() error {
|
||||
|
||||
stopProfiling, err := c.initProfiling()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -631,11 +634,7 @@ func chmodFilter(dst, src os.FileInfo) bool {
|
||||
}
|
||||
|
||||
func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
|
||||
publishDir := c.hugo().PathSpec.PublishDir
|
||||
// If root, remove the second '/'
|
||||
if publishDir == "//" {
|
||||
publishDir = helpers.FilePathSeparator
|
||||
}
|
||||
publishDir := helpers.FilePathSeparator
|
||||
|
||||
if sourceFs.PublishFolder != "" {
|
||||
publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
|
||||
@@ -648,7 +647,10 @@ func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint6
|
||||
syncer.NoChmod = c.Cfg.GetBool("noChmod")
|
||||
syncer.ChmodFilter = chmodFilter
|
||||
syncer.SrcFs = fs
|
||||
syncer.DestFs = c.Fs.Destination
|
||||
syncer.DestFs = c.Fs.PublishDir
|
||||
if c.renderStaticToDisk {
|
||||
syncer.DestFs = c.Fs.PublishDirStatic
|
||||
}
|
||||
// Now that we are using a unionFs for the static directories
|
||||
// We can effectively clean the publishDir on initial sync
|
||||
syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
|
||||
@@ -739,7 +741,6 @@ func (c *commandeer) handleBuildErr(err error, msg string) {
|
||||
}
|
||||
|
||||
func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
|
||||
|
||||
c.buildErr = nil
|
||||
visited := c.visitedURLs.PeekAllSet()
|
||||
if c.fastRenderMode {
|
||||
@@ -765,7 +766,9 @@ func (c *commandeer) partialReRender(urls ...string) error {
|
||||
for _, url := range urls {
|
||||
visited[url] = true
|
||||
}
|
||||
return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
|
||||
|
||||
// Note: We do not set NoBuildLock as the file lock is not acquired at this stage.
|
||||
return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
|
||||
}
|
||||
|
||||
func (c *commandeer) fullRebuild(changeType string) {
|
||||
@@ -843,6 +846,10 @@ func (c *commandeer) newWatcher(pollIntervalStr string, dirList ...string) (*wat
|
||||
c.logger.Printf("Use watcher with poll interval %v", pollInterval)
|
||||
}
|
||||
|
||||
if pollInterval == 0 {
|
||||
pollInterval = 500 * time.Millisecond
|
||||
}
|
||||
|
||||
watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -867,7 +874,7 @@ func (c *commandeer) newWatcher(pollIntervalStr string, dirList ...string) (*wat
|
||||
for {
|
||||
select {
|
||||
case evs := <-watcher.Events:
|
||||
unlock, err := c.hugo().BaseFs.LockBuild()
|
||||
unlock, err := c.buildLock()
|
||||
if err != nil {
|
||||
c.logger.Errorln("Failed to acquire a build lock: %s", err)
|
||||
return
|
||||
|
||||
@@ -36,12 +36,10 @@ title = "Hugo Commands"
|
||||
contentDir = "thisdoesnotexist"
|
||||
|
||||
`
|
||||
dir, clean, err := createSimpleTestSite(t, testSiteConfig{configTOML: cfgStr, contentDir: contentDir})
|
||||
c.Assert(err, qt.IsNil)
|
||||
defer clean()
|
||||
dir := createSimpleTestSite(t, testSiteConfig{configTOML: cfgStr, contentDir: contentDir})
|
||||
|
||||
cmd.SetArgs([]string{"-s=" + dir, "-c=" + contentDir})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
_, err := cmd.ExecuteC()
|
||||
c.Assert(err, qt.IsNil)
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ func (i *importCmd) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPos
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]interface{} {
|
||||
func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]any {
|
||||
path := filepath.Join(jekyllRoot, "_config.yml")
|
||||
|
||||
exists, err := helpers.Exists(path, fs)
|
||||
@@ -265,7 +265,7 @@ func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]
|
||||
return c
|
||||
}
|
||||
|
||||
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]interface{}) (err error) {
|
||||
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
|
||||
title := "My New Hugo Site"
|
||||
baseURL := "http://example.org/"
|
||||
|
||||
@@ -285,7 +285,7 @@ func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind meta
|
||||
}
|
||||
}
|
||||
|
||||
in := map[string]interface{}{
|
||||
in := map[string]any{
|
||||
"baseURL": baseURL,
|
||||
"title": title,
|
||||
"languageCode": "en-us",
|
||||
@@ -423,7 +423,7 @@ func convertJekyllPost(path, relPath, targetDir string, draft bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertJekyllMetaData(m interface{}, postName string, postDate time.Time, draft bool) (interface{}, error) {
|
||||
func convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
|
||||
metadata, err := maps.ToStringMapE(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -475,7 +475,7 @@ func convertJekyllMetaData(m interface{}, postName string, postDate time.Time, d
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func convertJekyllContent(m interface{}, content string) (string, error) {
|
||||
func convertJekyllContent(m any, content string) (string, error) {
|
||||
metadata, _ := maps.ToStringMapE(m)
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
@@ -47,44 +47,44 @@ func TestParseJekyllFilename(t *testing.T) {
|
||||
func TestConvertJekyllMetadata(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
testDataList := []struct {
|
||||
metadata interface{}
|
||||
metadata any
|
||||
postName string
|
||||
postDate time.Time
|
||||
draft bool
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true,
|
||||
`{"date":"2015-10-01T00:00:00Z","draft":true}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"Permalink": "/permalink.html", "layout": "post"},
|
||||
map[any]any{"Permalink": "/permalink.html", "layout": "post"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"permalink": "/permalink.html"},
|
||||
map[any]any{"permalink": "/permalink.html"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"category": nil, "permalink": 123},
|
||||
map[any]any{"category": nil, "permalink": 123},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"Excerpt_Separator": "sep"},
|
||||
map[any]any{"Excerpt_Separator": "sep"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","excerpt_separator":"sep"}`,
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"},
|
||||
map[any]any{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"Others":"Goods","categories":["book"],"date":"2015-10-01T12:13:11Z"}`,
|
||||
},
|
||||
@@ -102,69 +102,69 @@ func TestConvertJekyllMetadata(t *testing.T) {
|
||||
func TestConvertJekyllContent(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
testDataList := []struct {
|
||||
metadata interface{}
|
||||
metadata any
|
||||
content string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"Test content\r\n<!-- more -->\npart2 content", "Test content\n<!--more-->\npart2 content",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"Test content\n<!-- More -->\npart2 content", "Test content\n<!--more-->\npart2 content",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"excerpt_separator": "<!--sep-->"},
|
||||
map[any]any{"excerpt_separator": "<!--sep-->"},
|
||||
"Test content\n<!--sep-->\npart2 content",
|
||||
"---\nexcerpt_separator: <!--sep-->\n---\nTest content\n<!--more-->\npart2 content",
|
||||
},
|
||||
{map[interface{}]interface{}{}, "{% raw %}text{% endraw %}", "text"},
|
||||
{map[interface{}]interface{}{}, "{%raw%} text2 {%endraw %}", "text2"},
|
||||
{map[any]any{}, "{% raw %}text{% endraw %}", "text"},
|
||||
{map[any]any{}, "{%raw%} text2 {%endraw %}", "text2"},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"{% highlight go %}\nvar s int\n{% endhighlight %}",
|
||||
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"{% highlight go linenos hl_lines=\"1 2\" %}\nvar s string\nvar i int\n{% endhighlight %}",
|
||||
"{{< highlight go \"linenos=table,hl_lines=1 2\" >}}\nvar s string\nvar i int\n{{< / highlight >}}",
|
||||
},
|
||||
|
||||
// Octopress image tag
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"{% img http://placekitten.com/890/280 %}",
|
||||
"{{< figure src=\"http://placekitten.com/890/280\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"{% img left http://placekitten.com/320/250 Place Kitten #2 %}",
|
||||
"{{< figure class=\"left\" src=\"http://placekitten.com/320/250\" title=\"Place Kitten #2\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #3' %}",
|
||||
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #3\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
|
||||
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"{% img http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
|
||||
"{{< figure src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{},
|
||||
map[any]any{},
|
||||
"{% img right /placekitten/300/500 'Place Kitten #4' 'An image of a very cute kitten' %}",
|
||||
"{{< figure class=\"right\" src=\"/placekitten/300/500\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
|
||||
},
|
||||
{
|
||||
map[interface{}]interface{}{"category": "book", "layout": "post", "Date": "2015-10-01 12:13:11"},
|
||||
map[any]any{"category": "book", "layout": "post", "Date": "2015-10-01 12:13:11"},
|
||||
"somecontent",
|
||||
"---\nDate: \"2015-10-01 12:13:11\"\ncategory: book\nlayout: post\n---\nsomecontent",
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !darwin
|
||||
// +build !darwin
|
||||
|
||||
package commands
|
||||
|
||||
+5
-5
@@ -32,7 +32,7 @@ type listCmd struct {
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (lc *listCmd) buildSites(config map[string]interface{}) (*hugolib.HugoSites, error) {
|
||||
func (lc *listCmd) buildSites(config map[string]any) (*hugolib.HugoSites, error) {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
for key, value := range config {
|
||||
c.Set(key, value)
|
||||
@@ -75,7 +75,7 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
Short: "List all drafts",
|
||||
Long: `List all of the drafts in your content directory.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sites, err := cc.buildSites(map[string]interface{}{"buildDrafts": true})
|
||||
sites, err := cc.buildSites(map[string]any{"buildDrafts": true})
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
@@ -94,7 +94,7 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
Short: "List all posts dated in the future",
|
||||
Long: `List all of the posts in your content directory which will be posted in the future.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sites, err := cc.buildSites(map[string]interface{}{"buildFuture": true})
|
||||
sites, err := cc.buildSites(map[string]any{"buildFuture": true})
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
Short: "List all posts already expired",
|
||||
Long: `List all of the posts in your content directory which has already expired.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sites, err := cc.buildSites(map[string]interface{}{"buildExpired": true})
|
||||
sites, err := cc.buildSites(map[string]any{"buildExpired": true})
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
@@ -150,7 +150,7 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
Short: "List all posts",
|
||||
Long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sites, err := cc.buildSites(map[string]interface{}{
|
||||
sites, err := cc.buildSites(map[string]any{
|
||||
"buildExpired": true,
|
||||
"buildDrafts": true,
|
||||
"buildFuture": true,
|
||||
|
||||
@@ -29,10 +29,7 @@ func captureStdout(f func() error) (string, error) {
|
||||
|
||||
func TestListAll(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
dir, clean, err := createSimpleTestSite(t, testSiteConfig{})
|
||||
defer clean()
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
dir := createSimpleTestSite(t, testSiteConfig{})
|
||||
|
||||
hugoCmd := newCommandsBuilder().addAll().build()
|
||||
cmd := hugoCmd.getCommand()
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ func (b *commandsBuilder) newModCmd() *modCmd {
|
||||
|
||||
const commonUsage = `
|
||||
Note that Hugo will always start out by resolving the components defined in the site
|
||||
configuration, provided by a _vendor directory (if no --ignoreVendor flag provided),
|
||||
configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided),
|
||||
Go Modules, or a folder inside the themes directory, in that order.
|
||||
|
||||
See https://gohugo.io/hugo-modules/ for more information.
|
||||
|
||||
@@ -52,7 +52,7 @@ Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
RunE: cc.newSite,
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&cc.configFormat, "format", "f", "toml", "config & frontmatter format")
|
||||
cmd.Flags().StringVarP(&cc.configFormat, "format", "f", "toml", "config file format")
|
||||
cmd.Flags().Bool("force", false, "init inside non-empty directory")
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
@@ -122,8 +122,10 @@ func (n *newSiteCmd) newSite(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
forceNew, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
return n.doNewSite(hugofs.NewDefault(config.New()), createpath, forceNew)
|
||||
cfg := config.New()
|
||||
cfg.Set("workingDir", createpath)
|
||||
cfg.Set("publishDir", "public")
|
||||
return n.doNewSite(hugofs.NewDefault(cfg), createpath, forceNew)
|
||||
}
|
||||
|
||||
func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build nodeploy
|
||||
// +build nodeploy
|
||||
|
||||
package commands
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build release
|
||||
// +build release
|
||||
|
||||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !release
|
||||
// +build !release
|
||||
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
|
||||
+94
-53
@@ -15,6 +15,7 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@@ -32,6 +34,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -46,17 +49,18 @@ import (
|
||||
|
||||
type serverCmd struct {
|
||||
// Can be used to stop the server. Useful in tests
|
||||
stop <-chan bool
|
||||
stop chan bool
|
||||
|
||||
disableLiveReload bool
|
||||
navigateToChanged bool
|
||||
renderToDisk bool
|
||||
serverAppend bool
|
||||
serverInterface string
|
||||
serverPort int
|
||||
liveReloadPort int
|
||||
serverWatch bool
|
||||
noHTTPCache bool
|
||||
disableLiveReload bool
|
||||
navigateToChanged bool
|
||||
renderToDisk bool
|
||||
renderStaticToDisk bool
|
||||
serverAppend bool
|
||||
serverInterface string
|
||||
serverPort int
|
||||
liveReloadPort int
|
||||
serverWatch bool
|
||||
noHTTPCache bool
|
||||
|
||||
disableFastRender bool
|
||||
disableBrowserError bool
|
||||
@@ -68,7 +72,7 @@ func (b *commandsBuilder) newServerCmd() *serverCmd {
|
||||
return b.newServerCmdSignaled(nil)
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newServerCmdSignaled(stop <-chan bool) *serverCmd {
|
||||
func (b *commandsBuilder) newServerCmdSignaled(stop chan bool) *serverCmd {
|
||||
cc := &serverCmd{stop: stop}
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderCmd(&cobra.Command{
|
||||
@@ -87,7 +91,13 @@ By default hugo will also watch your files for any changes you make and
|
||||
automatically rebuild the site. It will then live reload any open browser pages
|
||||
and push the latest content to them. As most Hugo sites are built in a fraction
|
||||
of a second, you will be able to save and see your changes nearly instantly.`,
|
||||
RunE: cc.server,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := cc.server(cmd, args)
|
||||
if err != nil && cc.stop != nil {
|
||||
cc.stop <- true
|
||||
}
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen")
|
||||
@@ -98,7 +108,8 @@ of a second, you will be able to save and see your changes nearly instantly.`,
|
||||
cc.cmd.Flags().BoolVarP(&cc.serverAppend, "appendPort", "", true, "append port to baseURL")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
|
||||
cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
|
||||
cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)")
|
||||
cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "serve all files from disk (default is from memory)")
|
||||
cc.cmd.Flags().BoolVar(&cc.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
|
||||
|
||||
@@ -128,8 +139,6 @@ func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var serverPorts []int
|
||||
|
||||
func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
// If a Destination is provided via flag write to disk
|
||||
destination, _ := cmd.Flags().GetString("destination")
|
||||
@@ -139,8 +148,9 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var serverCfgInit sync.Once
|
||||
|
||||
cfgInit := func(c *commandeer) error {
|
||||
c.Set("renderToMemory", !sc.renderToDisk)
|
||||
cfgInit := func(c *commandeer) (rerr error) {
|
||||
c.Set("renderToMemory", !(sc.renderToDisk || sc.renderStaticToDisk))
|
||||
c.Set("renderStaticToDisk", sc.renderStaticToDisk)
|
||||
if cmd.Flags().Changed("navigateToChanged") {
|
||||
c.Set("navigateToChanged", sc.navigateToChanged)
|
||||
}
|
||||
@@ -162,59 +172,60 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// We can only do this once.
|
||||
serverCfgInit.Do(func() {
|
||||
serverPorts = make([]int, 1)
|
||||
c.serverPorts = make([]serverPortListener, 1)
|
||||
|
||||
if c.languages.IsMultihost() {
|
||||
if !sc.serverAppend {
|
||||
err = newSystemError("--appendPort=false not supported when in multihost mode")
|
||||
rerr = newSystemError("--appendPort=false not supported when in multihost mode")
|
||||
}
|
||||
serverPorts = make([]int, len(c.languages))
|
||||
c.serverPorts = make([]serverPortListener, len(c.languages))
|
||||
}
|
||||
|
||||
currentServerPort := sc.serverPort
|
||||
|
||||
for i := 0; i < len(serverPorts); i++ {
|
||||
for i := 0; i < len(c.serverPorts); i++ {
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort)))
|
||||
if err == nil {
|
||||
l.Close()
|
||||
serverPorts[i] = currentServerPort
|
||||
c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
|
||||
} else {
|
||||
if i == 0 && sc.cmd.Flags().Changed("port") {
|
||||
// port set explicitly by user -- he/she probably meant it!
|
||||
err = newSystemErrorF("Server startup failed: %s", err)
|
||||
rerr = newSystemErrorF("Server startup failed: %s", err)
|
||||
return
|
||||
}
|
||||
c.logger.Println("port", sc.serverPort, "already in use, attempting to use an available port")
|
||||
sp, err := helpers.FindAvailablePort()
|
||||
l, sp, err := helpers.TCPListen()
|
||||
if err != nil {
|
||||
err = newSystemError("Unable to find alternative port to use:", err)
|
||||
rerr = newSystemError("Unable to find alternative port to use:", err)
|
||||
return
|
||||
}
|
||||
serverPorts[i] = sp.Port
|
||||
c.serverPorts[i] = serverPortListener{ln: l, p: sp.Port}
|
||||
}
|
||||
|
||||
currentServerPort = serverPorts[i] + 1
|
||||
currentServerPort = c.serverPorts[i].p + 1
|
||||
}
|
||||
})
|
||||
|
||||
c.serverPorts = serverPorts
|
||||
if rerr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("port", sc.serverPort)
|
||||
if sc.liveReloadPort != -1 {
|
||||
c.Set("liveReloadPort", sc.liveReloadPort)
|
||||
} else {
|
||||
c.Set("liveReloadPort", serverPorts[0])
|
||||
c.Set("liveReloadPort", c.serverPorts[0].p)
|
||||
}
|
||||
|
||||
isMultiHost := c.languages.IsMultihost()
|
||||
for i, language := range c.languages {
|
||||
var serverPort int
|
||||
if isMultiHost {
|
||||
serverPort = serverPorts[i]
|
||||
serverPort = c.serverPorts[i].p
|
||||
} else {
|
||||
serverPort = serverPorts[0]
|
||||
serverPort = c.serverPorts[0].p
|
||||
}
|
||||
|
||||
baseURL, err := sc.fixURL(language, sc.baseURL, serverPort)
|
||||
@@ -229,7 +240,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
if err := memStats(); err != nil {
|
||||
@@ -298,7 +309,7 @@ func getRootWatchDirsStr(baseDir string, watchDirs []string) string {
|
||||
type fileServer struct {
|
||||
baseURLs []string
|
||||
roots []string
|
||||
errorTemplate func(err interface{}) (io.Reader, error)
|
||||
errorTemplate func(err any) (io.Reader, error)
|
||||
c *commandeer
|
||||
s *serverCmd
|
||||
}
|
||||
@@ -314,31 +325,39 @@ func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Reques
|
||||
return r2
|
||||
}
|
||||
|
||||
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) {
|
||||
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string, string, error) {
|
||||
baseURL := f.baseURLs[i]
|
||||
root := f.roots[i]
|
||||
port := f.c.serverPorts[i]
|
||||
port := f.c.serverPorts[i].p
|
||||
listener := f.c.serverPorts[i].ln
|
||||
|
||||
// For logging only.
|
||||
// TODO(bep) consolidate.
|
||||
publishDir := f.c.Cfg.GetString("publishDir")
|
||||
publishDirStatic := f.c.Cfg.GetString("publishDirStatic")
|
||||
workingDir := f.c.Cfg.GetString("workingDir")
|
||||
|
||||
if root != "" {
|
||||
publishDir = filepath.Join(publishDir, root)
|
||||
publishDirStatic = filepath.Join(publishDirStatic, root)
|
||||
}
|
||||
|
||||
absPublishDir := f.c.hugo().PathSpec.AbsPathify(publishDir)
|
||||
absPublishDir := paths.AbsPathify(workingDir, publishDir)
|
||||
absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
|
||||
|
||||
jww.FEEDBACK.Printf("Environment: %q", f.c.hugo().Deps.Site.Hugo().Environment)
|
||||
|
||||
if i == 0 {
|
||||
if f.s.renderToDisk {
|
||||
jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
|
||||
} else if f.s.renderStaticToDisk {
|
||||
jww.FEEDBACK.Println("Serving pages from memory and static files from " + absPublishDirStatic)
|
||||
} else {
|
||||
jww.FEEDBACK.Println("Serving pages from memory")
|
||||
}
|
||||
}
|
||||
|
||||
httpFs := afero.NewHttpFs(f.c.destinationFs)
|
||||
fs := filesOnlyFs{httpFs.Dir(absPublishDir)}
|
||||
httpFs := afero.NewHttpFs(f.c.publishDirServerFs)
|
||||
fs := filesOnlyFs{httpFs.Dir(path.Join("/", root))}
|
||||
|
||||
if i == 0 && f.c.fastRenderMode {
|
||||
jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
|
||||
@@ -347,7 +366,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
// We're only interested in the path
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, "", "", errors.Wrap(err, "Invalid baseURL")
|
||||
return nil, nil, "", "", errors.Wrap(err, "Invalid baseURL")
|
||||
}
|
||||
|
||||
decorate := func(h http.Handler) http.Handler {
|
||||
@@ -453,7 +472,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
|
||||
endpoint := net.JoinHostPort(f.s.serverInterface, strconv.Itoa(port))
|
||||
|
||||
return mu, u.String(), endpoint, nil
|
||||
return mu, listener, u.String(), endpoint, nil
|
||||
}
|
||||
|
||||
var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
|
||||
@@ -491,7 +510,7 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
roots: roots,
|
||||
c: c,
|
||||
s: s,
|
||||
errorTemplate: func(ctx interface{}) (io.Reader, error) {
|
||||
errorTemplate: func(ctx any) (io.Reader, error) {
|
||||
b := &bytes.Buffer{}
|
||||
err := c.hugo().Tmpl().Execute(templ, b, ctx)
|
||||
return b, err
|
||||
@@ -506,9 +525,17 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
var servers []*http.Server
|
||||
|
||||
wg1, ctx := errgroup.WithContext(context.Background())
|
||||
|
||||
for i := range baseURLs {
|
||||
mu, serverURL, endpoint, err := srv.createEndpoint(i)
|
||||
mu, listener, serverURL, endpoint, err := srv.createEndpoint(i)
|
||||
srv := &http.Server{
|
||||
Addr: endpoint,
|
||||
Handler: mu,
|
||||
}
|
||||
servers = append(servers, srv)
|
||||
|
||||
if doLiveReload {
|
||||
u, err := url.Parse(helpers.SanitizeURL(baseURLs[i]))
|
||||
@@ -520,13 +547,13 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
mu.HandleFunc(u.Path+"/livereload", livereload.Handler)
|
||||
}
|
||||
jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", serverURL, s.serverInterface)
|
||||
go func() {
|
||||
err = http.ListenAndServe(endpoint, mu)
|
||||
if err != nil {
|
||||
c.logger.Errorf("Error: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
wg1.Go(func() error {
|
||||
err = srv.Serve(listener)
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Press Ctrl+C to stop")
|
||||
@@ -542,7 +569,21 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
|
||||
c.hugo().Close()
|
||||
|
||||
return nil
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
wg2, ctx := errgroup.WithContext(ctx)
|
||||
for _, srv := range servers {
|
||||
srv := srv
|
||||
wg2.Go(func() error {
|
||||
return srv.Shutdown(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
err1, err2 := wg1.Wait(), wg2.Wait()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
// fixURL massages the baseURL into a form needed for serving
|
||||
|
||||
+148
-28
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -24,22 +25,120 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
if isWindowsCI() {
|
||||
// TODO(bep) not sure why server tests have started to fail on the Windows CI server.
|
||||
t.Skip("Skip server test on appveyor")
|
||||
}
|
||||
// Issue 9518
|
||||
func TestServerPanicOnConfigError(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
dir, clean, err := createSimpleTestSite(t, testSiteConfig{})
|
||||
defer clean()
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
// Let us hope that this port is available on all systems ...
|
||||
port := 1331
|
||||
config := `
|
||||
[markup]
|
||||
[markup.highlight]
|
||||
linenos='table'
|
||||
`
|
||||
|
||||
r := runServerTest(c, false, config)
|
||||
|
||||
c.Assert(r.err, qt.IsNotNil)
|
||||
c.Assert(r.err.Error(), qt.Contains, "cannot parse 'Highlight.LineNos' as bool:")
|
||||
}
|
||||
|
||||
func TestServerFlags(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
assertPublic := func(c *qt.C, r serverTestResult, renderStaticToDisk bool) {
|
||||
c.Assert(r.err, qt.IsNil)
|
||||
c.Assert(r.homeContent, qt.Contains, "Environment: development")
|
||||
c.Assert(r.publicDirnames["myfile.txt"], qt.Equals, renderStaticToDisk)
|
||||
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
flag string
|
||||
assert func(c *qt.C, r serverTestResult)
|
||||
}{
|
||||
{"", func(c *qt.C, r serverTestResult) {
|
||||
assertPublic(c, r, false)
|
||||
}},
|
||||
{"--renderToDisk", func(c *qt.C, r serverTestResult) {
|
||||
assertPublic(c, r, true)
|
||||
}},
|
||||
{"--renderStaticToDisk", func(c *qt.C, r serverTestResult) {
|
||||
assertPublic(c, r, true)
|
||||
}},
|
||||
} {
|
||||
c.Run(test.flag, func(c *qt.C) {
|
||||
config := `
|
||||
baseURL="https://example.org"
|
||||
`
|
||||
|
||||
var args []string
|
||||
if test.flag != "" {
|
||||
args = strings.Split(test.flag, "=")
|
||||
}
|
||||
|
||||
r := runServerTest(c, true, config, args...)
|
||||
|
||||
test.assert(c, r)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestServerBugs(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
flag string
|
||||
assert func(c *qt.C, r serverTestResult)
|
||||
}{
|
||||
// Issue 9788
|
||||
{"PostProcess, memory", "", func(c *qt.C, r serverTestResult) {
|
||||
c.Assert(r.err, qt.IsNil)
|
||||
c.Assert(r.homeContent, qt.Contains, "PostProcess: /foo.min.css")
|
||||
}},
|
||||
{"PostProcess, disk", "--renderToDisk", func(c *qt.C, r serverTestResult) {
|
||||
c.Assert(r.err, qt.IsNil)
|
||||
c.Assert(r.homeContent, qt.Contains, "PostProcess: /foo.min.css")
|
||||
}},
|
||||
} {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
config := `
|
||||
baseURL="https://example.org"
|
||||
`
|
||||
|
||||
var args []string
|
||||
if test.flag != "" {
|
||||
args = strings.Split(test.flag, "=")
|
||||
}
|
||||
r := runServerTest(c, true, config, args...)
|
||||
test.assert(c, r)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type serverTestResult struct {
|
||||
err error
|
||||
homeContent string
|
||||
publicDirnames map[string]bool
|
||||
}
|
||||
|
||||
func runServerTest(c *qt.C, getHome bool, config string, args ...string) (result serverTestResult) {
|
||||
dir := createSimpleTestSite(c, testSiteConfig{configTOML: config})
|
||||
|
||||
sp, err := helpers.FindAvailablePort()
|
||||
c.Assert(err, qt.IsNil)
|
||||
port := sp.Port
|
||||
|
||||
defer func() {
|
||||
os.RemoveAll(dir)
|
||||
@@ -51,28 +150,49 @@ func TestServer(t *testing.T) {
|
||||
scmd := b.newServerCmdSignaled(stop)
|
||||
|
||||
cmd := scmd.getCommand()
|
||||
cmd.SetArgs([]string{"-s=" + dir, fmt.Sprintf("-p=%d", port)})
|
||||
args = append([]string{"-s=" + dir, fmt.Sprintf("-p=%d", port)}, args...)
|
||||
cmd.SetArgs(args)
|
||||
|
||||
go func() {
|
||||
_, err = cmd.ExecuteC()
|
||||
c.Assert(err, qt.IsNil)
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
wg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// There is no way to know exactly when the server is ready for connections.
|
||||
// We could improve by something like https://golang.org/pkg/net/http/httptest/#Server
|
||||
// But for now, let us sleep and pray!
|
||||
time.Sleep(2 * time.Second)
|
||||
wg.Go(func() error {
|
||||
_, err := cmd.ExecuteC()
|
||||
return err
|
||||
})
|
||||
|
||||
resp, err := http.Get("http://localhost:1331/")
|
||||
c.Assert(err, qt.IsNil)
|
||||
defer resp.Body.Close()
|
||||
homeContent := helpers.ReaderToString(resp.Body)
|
||||
if getHome {
|
||||
// Esp. on slow CI machines, we need to wait a little before the web
|
||||
// server is ready.
|
||||
time.Sleep(567 * time.Millisecond)
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port))
|
||||
c.Check(err, qt.IsNil)
|
||||
c.Check(resp.StatusCode, qt.Equals, http.StatusOK)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
result.homeContent = helpers.ReaderToString(resp.Body)
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(homeContent, qt.Contains, "List: Hugo Commands")
|
||||
c.Assert(homeContent, qt.Contains, "Environment: development")
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
case stop <- true:
|
||||
}
|
||||
|
||||
pubFiles, err := os.ReadDir(filepath.Join(dir, "public"))
|
||||
c.Check(err, qt.IsNil)
|
||||
result.publicDirnames = make(map[string]bool)
|
||||
for _, f := range pubFiles {
|
||||
result.publicDirnames[f.Name()] = true
|
||||
}
|
||||
|
||||
result.err = wg.Wait()
|
||||
|
||||
return
|
||||
|
||||
// Stop the server.
|
||||
stop <- true
|
||||
}
|
||||
|
||||
func TestFixURL(t *testing.T) {
|
||||
@@ -101,7 +221,7 @@ func TestFixURL(t *testing.T) {
|
||||
t.Run(test.TestName, func(t *testing.T) {
|
||||
b := newCommandsBuilder()
|
||||
s := b.newServerCmd()
|
||||
v := config.New()
|
||||
v := config.NewWithTestDefaults()
|
||||
baseURL := test.CLIBaseURL
|
||||
v.Set("baseURL", test.CfgBaseURL)
|
||||
s.serverAppend = test.AppendPort
|
||||
|
||||
@@ -40,11 +40,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
c := s.c
|
||||
|
||||
syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
|
||||
publishDir := c.hugo().PathSpec.PublishDir
|
||||
// If root, remove the second '/'
|
||||
if publishDir == "//" {
|
||||
publishDir = helpers.FilePathSeparator
|
||||
}
|
||||
publishDir := helpers.FilePathSeparator
|
||||
|
||||
if sourceFs.PublishFolder != "" {
|
||||
publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
|
||||
@@ -55,7 +51,10 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
syncer.NoChmod = c.Cfg.GetBool("noChmod")
|
||||
syncer.ChmodFilter = chmodFilter
|
||||
syncer.SrcFs = sourceFs.Fs
|
||||
syncer.DestFs = c.Fs.Destination
|
||||
syncer.DestFs = c.Fs.PublishDir
|
||||
if c.renderStaticToDisk {
|
||||
syncer.DestFs = c.Fs.PublishDirStatic
|
||||
}
|
||||
|
||||
// prevent spamming the log on changes
|
||||
logger := helpers.NewDistinctErrorLogger()
|
||||
@@ -98,15 +97,14 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
|
||||
if _, err := sourceFs.Fs.Stat(relPath); os.IsNotExist(err) {
|
||||
// If file doesn't exist in any static dir, remove it
|
||||
toRemove := filepath.Join(publishDir, relPath)
|
||||
logger.Println("File no longer exists in static dir, removing", relPath)
|
||||
_ = c.Fs.PublishDirStatic.RemoveAll(relPath)
|
||||
|
||||
logger.Println("File no longer exists in static dir, removing", toRemove)
|
||||
_ = c.Fs.Destination.RemoveAll(toRemove)
|
||||
} else if err == nil {
|
||||
// If file still exists, sync it
|
||||
logger.Println("Syncing", relPath, "to", publishDir)
|
||||
|
||||
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
|
||||
if err := syncer.Sync(relPath, relPath); err != nil {
|
||||
c.logger.Errorln(err)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// Append appends from to a slice to and returns the resulting slice.
|
||||
// If length of from is one and the only element is a slice of same type as to,
|
||||
// it will be appended.
|
||||
func Append(to interface{}, from ...interface{}) (interface{}, error) {
|
||||
func Append(to any, from ...any) (any, error) {
|
||||
tov, toIsNil := indirect(reflect.ValueOf(to))
|
||||
|
||||
toIsNil = toIsNil || to == nil
|
||||
@@ -73,8 +73,8 @@ func Append(to interface{}, from ...interface{}) (interface{}, error) {
|
||||
return tov.Interface(), nil
|
||||
}
|
||||
|
||||
func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]interface{}, error) {
|
||||
var tos []interface{}
|
||||
func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, error) {
|
||||
var tos []any
|
||||
|
||||
for _, slice := range []reflect.Value{slice1, slice2} {
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
@@ -85,8 +85,8 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]interface
|
||||
return tos, nil
|
||||
}
|
||||
|
||||
func appendToInterfaceSlice(tov reflect.Value, from ...interface{}) ([]interface{}, error) {
|
||||
var tos []interface{}
|
||||
func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
|
||||
var tos []any
|
||||
|
||||
for i := 0; i < tov.Len(); i++ {
|
||||
tos = append(tos, tov.Index(i).Interface())
|
||||
|
||||
@@ -25,25 +25,25 @@ func TestAppend(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
start interface{}
|
||||
addend []interface{}
|
||||
expected interface{}
|
||||
start any
|
||||
addend []any
|
||||
expected any
|
||||
}{
|
||||
{[]string{"a", "b"}, []interface{}{"c"}, []string{"a", "b", "c"}},
|
||||
{[]string{"a", "b"}, []interface{}{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}},
|
||||
{[]string{"a", "b"}, []interface{}{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}},
|
||||
{[]string{"a"}, []interface{}{"b", template.HTML("c")}, []interface{}{"a", "b", template.HTML("c")}},
|
||||
{nil, []interface{}{"a", "b"}, []string{"a", "b"}},
|
||||
{nil, []interface{}{nil}, []interface{}{nil}},
|
||||
{[]interface{}{}, []interface{}{[]string{"c", "d", "e"}}, []string{"c", "d", "e"}},
|
||||
{[]string{"a", "b"}, []any{"c"}, []string{"a", "b", "c"}},
|
||||
{[]string{"a", "b"}, []any{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}},
|
||||
{[]string{"a", "b"}, []any{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}},
|
||||
{[]string{"a"}, []any{"b", template.HTML("c")}, []any{"a", "b", template.HTML("c")}},
|
||||
{nil, []any{"a", "b"}, []string{"a", "b"}},
|
||||
{nil, []any{nil}, []any{nil}},
|
||||
{[]any{}, []any{[]string{"c", "d", "e"}}, []string{"c", "d", "e"}},
|
||||
{
|
||||
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]interface{}{&tstSlicer{"c"}},
|
||||
[]any{&tstSlicer{"c"}},
|
||||
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}},
|
||||
},
|
||||
{
|
||||
&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]interface{}{&tstSlicer{"c"}},
|
||||
[]any{&tstSlicer{"c"}},
|
||||
tstSlicers{
|
||||
&tstSlicer{"a"},
|
||||
&tstSlicer{"b"},
|
||||
@@ -52,26 +52,26 @@ func TestAppend(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}},
|
||||
[]interface{}{&tstSlicerIn1{"c"}},
|
||||
[]any{&tstSlicerIn1{"c"}},
|
||||
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}},
|
||||
},
|
||||
//https://github.com/gohugoio/hugo/issues/5361
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
[]interface{}{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
[]interface{}{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]any{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
[]any{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
},
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
[]interface{}{&tstSlicer{"a"}},
|
||||
[]interface{}{"a", "b", &tstSlicer{"a"}},
|
||||
[]any{&tstSlicer{"a"}},
|
||||
[]any{"a", "b", &tstSlicer{"a"}},
|
||||
},
|
||||
// Errors
|
||||
{"", []interface{}{[]string{"a", "b"}}, false},
|
||||
{"", []any{[]string{"a", "b"}}, false},
|
||||
// No string concatenation.
|
||||
{
|
||||
"ab",
|
||||
[]interface{}{"c"},
|
||||
[]any{"c"},
|
||||
false,
|
||||
},
|
||||
} {
|
||||
|
||||
@@ -17,5 +17,5 @@ package collections
|
||||
|
||||
// Grouper defines a very generic way to group items by a given key.
|
||||
type Grouper interface {
|
||||
Group(key interface{}, items interface{}) (interface{}, error)
|
||||
Group(key any, items any) (any, error)
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
// in collections.Slice template func to get types such as Pages, PageGroups etc.
|
||||
// instead of the less useful []interface{}.
|
||||
type Slicer interface {
|
||||
Slice(items interface{}) (interface{}, error)
|
||||
Slice(items any) (any, error)
|
||||
}
|
||||
|
||||
// Slice returns a slice of all passed arguments.
|
||||
func Slice(args ...interface{}) interface{} {
|
||||
func Slice(args ...any) any {
|
||||
if len(args) == 0 {
|
||||
return args
|
||||
}
|
||||
@@ -64,3 +64,13 @@ func Slice(args ...interface{}) interface{} {
|
||||
}
|
||||
return slice.Interface()
|
||||
}
|
||||
|
||||
// StringSliceToInterfaceSlice converts ss to []interface{}.
|
||||
func StringSliceToInterfaceSlice(ss []string) []any {
|
||||
result := make([]any, len(ss))
|
||||
for i, s := range ss {
|
||||
result[i] = s
|
||||
}
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ type tstSlicer struct {
|
||||
TheName string
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn1) Slice(in interface{}) (interface{}, error) {
|
||||
items := in.([]interface{})
|
||||
func (p *tstSlicerIn1) Slice(in any) (any, error) {
|
||||
items := in.([]any)
|
||||
result := make(testSlicerInterfaces, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
@@ -60,8 +60,8 @@ func (p *tstSlicerIn1) Slice(in interface{}) (interface{}, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn2) Slice(in interface{}) (interface{}, error) {
|
||||
items := in.([]interface{})
|
||||
func (p *tstSlicerIn2) Slice(in any) (any, error) {
|
||||
items := in.([]any)
|
||||
result := make(testSlicerInterfaces, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
@@ -82,8 +82,8 @@ func (p *tstSlicerIn2) Name() string {
|
||||
return p.TheName
|
||||
}
|
||||
|
||||
func (p *tstSlicer) Slice(in interface{}) (interface{}, error) {
|
||||
items := in.([]interface{})
|
||||
func (p *tstSlicer) Slice(in any) (any, error) {
|
||||
items := in.([]any)
|
||||
result := make(tstSlicers, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
@@ -103,17 +103,17 @@ func TestSlice(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for i, test := range []struct {
|
||||
args []interface{}
|
||||
expected interface{}
|
||||
args []any
|
||||
expected any
|
||||
}{
|
||||
{[]interface{}{"a", "b"}, []string{"a", "b"}},
|
||||
{[]interface{}{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
{[]interface{}{&tstSlicer{"a"}, "b"}, []interface{}{&tstSlicer{"a"}, "b"}},
|
||||
{[]interface{}{}, []interface{}{}},
|
||||
{[]interface{}{nil}, []interface{}{nil}},
|
||||
{[]interface{}{5, "b"}, []interface{}{5, "b"}},
|
||||
{[]interface{}{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}},
|
||||
{[]interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}, []interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}},
|
||||
{[]any{"a", "b"}, []string{"a", "b"}},
|
||||
{[]any{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
{[]any{&tstSlicer{"a"}, "b"}, []any{&tstSlicer{"a"}, "b"}},
|
||||
{[]any{}, []any{}},
|
||||
{[]any{nil}, []any{nil}},
|
||||
{[]any{5, "b"}, []any{5, "b"}},
|
||||
{[]any{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}},
|
||||
{[]any{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}, []any{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}},
|
||||
} {
|
||||
errMsg := qt.Commentf("[%d] %v", i, test.args)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ type ErrorSender interface {
|
||||
// Recover is a helper function that can be used to capture panics.
|
||||
// Put this at the top of a method/function that crashes in a template:
|
||||
// defer herrors.Recover()
|
||||
func Recover(args ...interface{}) {
|
||||
func Recover(args ...any) {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("ERR:", r)
|
||||
args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
|
||||
@@ -88,3 +88,10 @@ func GetGID() uint64 {
|
||||
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
|
||||
// and this error is used to signal those situations.
|
||||
var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
|
||||
|
||||
// Must panics if err != nil.
|
||||
func Must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright 2020 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 hexec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/security"
|
||||
)
|
||||
|
||||
var WithDir = func(dir string) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.dir = dir
|
||||
}
|
||||
}
|
||||
|
||||
var WithContext = func(ctx context.Context) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
var WithStdout = func(w io.Writer) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.stdout = w
|
||||
}
|
||||
}
|
||||
|
||||
var WithStderr = func(w io.Writer) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.stderr = w
|
||||
}
|
||||
}
|
||||
|
||||
var WithStdin = func(r io.Reader) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.stdin = r
|
||||
}
|
||||
}
|
||||
|
||||
var WithEnviron = func(env []string) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
setOrAppend := func(s string) {
|
||||
k1, _ := config.SplitEnvVar(s)
|
||||
var found bool
|
||||
for i, v := range c.env {
|
||||
k2, _ := config.SplitEnvVar(v)
|
||||
if k1 == k2 {
|
||||
found = true
|
||||
c.env[i] = s
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
c.env = append(c.env, s)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range env {
|
||||
setOrAppend(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Exec using the provided security config.
|
||||
func New(cfg security.Config) *Exec {
|
||||
var baseEnviron []string
|
||||
for _, v := range os.Environ() {
|
||||
k, _ := config.SplitEnvVar(v)
|
||||
if cfg.Exec.OsEnv.Accept(k) {
|
||||
baseEnviron = append(baseEnviron, v)
|
||||
}
|
||||
}
|
||||
|
||||
return &Exec{
|
||||
sc: cfg,
|
||||
baseEnviron: baseEnviron,
|
||||
}
|
||||
}
|
||||
|
||||
// IsNotFound reports whether this is an error about a binary not found.
|
||||
func IsNotFound(err error) bool {
|
||||
var notFoundErr *NotFoundError
|
||||
return errors.As(err, ¬FoundErr)
|
||||
}
|
||||
|
||||
// SafeCommand is a wrapper around os/exec Command which uses a LookPath
|
||||
// implementation that does not search in current directory before looking in PATH.
|
||||
// See https://github.com/cli/safeexec and the linked issues.
|
||||
func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
|
||||
bin, err := safeexec.LookPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exec.Command(bin, arg...), nil
|
||||
}
|
||||
|
||||
// Exec encorces a security policy for commands run via os/exec.
|
||||
type Exec struct {
|
||||
sc security.Config
|
||||
|
||||
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
|
||||
baseEnviron []string
|
||||
}
|
||||
|
||||
// New will fail if name is not allowed according to the configured security policy.
|
||||
// Else a configured Runner will be returned ready to be Run.
|
||||
func (e *Exec) New(name string, arg ...any) (Runner, error) {
|
||||
if err := e.sc.CheckAllowedExec(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
env := make([]string, len(e.baseEnviron))
|
||||
copy(env, e.baseEnviron)
|
||||
|
||||
cm := &commandeer{
|
||||
name: name,
|
||||
env: env,
|
||||
}
|
||||
|
||||
return cm.command(arg...)
|
||||
|
||||
}
|
||||
|
||||
// Npx is a convenience method to create a Runner running npx --no-install <name> <args.
|
||||
func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
|
||||
arg = append(arg[:0], append([]any{"--no-install", name}, arg[0:]...)...)
|
||||
return e.New("npx", arg...)
|
||||
}
|
||||
|
||||
// Sec returns the security policies this Exec is configured with.
|
||||
func (e *Exec) Sec() security.Config {
|
||||
return e.sc
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return fmt.Sprintf("binary with name %q not found", e.name)
|
||||
}
|
||||
|
||||
// Runner wraps a *os.Cmd.
|
||||
type Runner interface {
|
||||
Run() error
|
||||
StdinPipe() (io.WriteCloser, error)
|
||||
}
|
||||
|
||||
type cmdWrapper struct {
|
||||
name string
|
||||
c *exec.Cmd
|
||||
|
||||
outerr *bytes.Buffer
|
||||
}
|
||||
|
||||
var notFoundRe = regexp.MustCompile(`(?s)not found:|could not determine executable`)
|
||||
|
||||
func (c *cmdWrapper) Run() error {
|
||||
err := c.c.Run()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if notFoundRe.MatchString(c.outerr.String()) {
|
||||
return &NotFoundError{name: c.name}
|
||||
}
|
||||
return fmt.Errorf("failed to execute binary %q with args %v: %s", c.name, c.c.Args[1:], c.outerr.String())
|
||||
}
|
||||
|
||||
func (c *cmdWrapper) StdinPipe() (io.WriteCloser, error) {
|
||||
return c.c.StdinPipe()
|
||||
}
|
||||
|
||||
type commandeer struct {
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
stdin io.Reader
|
||||
dir string
|
||||
ctx context.Context
|
||||
|
||||
name string
|
||||
env []string
|
||||
}
|
||||
|
||||
func (c *commandeer) command(arg ...any) (*cmdWrapper, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var args []string
|
||||
for _, a := range arg {
|
||||
switch v := a.(type) {
|
||||
case string:
|
||||
args = append(args, v)
|
||||
case func(*commandeer):
|
||||
v(c)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid argument to command: %T", a)
|
||||
}
|
||||
}
|
||||
|
||||
bin, err := safeexec.LookPath(c.name)
|
||||
if err != nil {
|
||||
return nil, &NotFoundError{
|
||||
name: c.name,
|
||||
}
|
||||
}
|
||||
|
||||
outerr := &bytes.Buffer{}
|
||||
if c.stderr == nil {
|
||||
c.stderr = outerr
|
||||
} else {
|
||||
c.stderr = io.MultiWriter(c.stderr, outerr)
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if c.ctx != nil {
|
||||
cmd = exec.CommandContext(c.ctx, bin, args...)
|
||||
} else {
|
||||
cmd = exec.Command(bin, args...)
|
||||
}
|
||||
|
||||
cmd.Stdin = c.stdin
|
||||
cmd.Stderr = c.stderr
|
||||
cmd.Stdout = c.stdout
|
||||
cmd.Env = c.env
|
||||
cmd.Dir = c.dir
|
||||
|
||||
return &cmdWrapper{outerr: outerr, c: cmd, name: c.name}, nil
|
||||
}
|
||||
|
||||
// InPath reports whether binaryName is in $PATH.
|
||||
func InPath(binaryName string) bool {
|
||||
if strings.Contains(binaryName, "/") {
|
||||
panic("binary name should not contain any slash")
|
||||
}
|
||||
_, err := safeexec.LookPath(binaryName)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// LookPath finds the path to binaryName in $PATH.
|
||||
// Returns "" if not found.
|
||||
func LookPath(binaryName string) string {
|
||||
if strings.Contains(binaryName, "/") {
|
||||
panic("binary name should not contain any slash")
|
||||
}
|
||||
s, err := safeexec.LookPath(binaryName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright 2020 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 hexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"os/exec"
|
||||
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
// SafeCommand is a wrapper around os/exec Command which uses a LookPath
|
||||
// implementation that does not search in current directory before looking in PATH.
|
||||
// See https://github.com/cli/safeexec and the linked issues.
|
||||
func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
|
||||
bin, err := safeexec.LookPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exec.Command(bin, arg...), nil
|
||||
}
|
||||
|
||||
// SafeCommandContext wraps CommandContext
|
||||
// See SafeCommand for more context.
|
||||
func SafeCommandContext(ctx context.Context, name string, arg ...string) (*exec.Cmd, error) {
|
||||
bin, err := safeexec.LookPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exec.CommandContext(ctx, bin, arg...), nil
|
||||
}
|
||||
@@ -17,7 +17,9 @@
|
||||
package hreflect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
@@ -60,7 +62,7 @@ func IsFloat(kind reflect.Kind) bool {
|
||||
|
||||
// IsTruthful returns whether in represents a truthful value.
|
||||
// See IsTruthfulValue
|
||||
func IsTruthful(in interface{}) bool {
|
||||
func IsTruthful(in any) bool {
|
||||
switch v := in.(type) {
|
||||
case reflect.Value:
|
||||
return IsTruthfulValue(v)
|
||||
@@ -114,6 +116,58 @@ func IsTruthfulValue(val reflect.Value) (truth bool) {
|
||||
return
|
||||
}
|
||||
|
||||
type methodKey struct {
|
||||
typ reflect.Type
|
||||
name string
|
||||
}
|
||||
|
||||
type methods struct {
|
||||
sync.RWMutex
|
||||
cache map[methodKey]int
|
||||
}
|
||||
|
||||
var methodCache = &methods{cache: make(map[methodKey]int)}
|
||||
|
||||
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
|
||||
// type lookup.
|
||||
func GetMethodByName(v reflect.Value, name string) reflect.Value {
|
||||
index := GetMethodIndexByName(v.Type(), name)
|
||||
|
||||
if index == -1 {
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
return v.Method(index)
|
||||
}
|
||||
|
||||
// GetMethodIndexByName returns the index of the method with the given name, or
|
||||
// -1 if no such method exists.
|
||||
func GetMethodIndexByName(tp reflect.Type, name string) int {
|
||||
k := methodKey{tp, name}
|
||||
methodCache.RLock()
|
||||
index, found := methodCache.cache[k]
|
||||
methodCache.RUnlock()
|
||||
if found {
|
||||
return index
|
||||
}
|
||||
|
||||
methodCache.Lock()
|
||||
defer methodCache.Unlock()
|
||||
|
||||
m, ok := tp.MethodByName(name)
|
||||
index = m.Index
|
||||
if !ok {
|
||||
index = -1
|
||||
}
|
||||
methodCache.cache[k] = index
|
||||
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
|
||||
return m.Index
|
||||
}
|
||||
|
||||
// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
|
||||
func indirectInterface(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Interface {
|
||||
@@ -124,3 +178,5 @@ func indirectInterface(v reflect.Value) reflect.Value {
|
||||
}
|
||||
return v.Elem()
|
||||
}
|
||||
|
||||
var ContextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
|
||||
@@ -30,6 +30,16 @@ func TestIsTruthful(t *testing.T) {
|
||||
c.Assert(IsTruthful(time.Time{}), qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestGetMethodByName(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
v := reflect.ValueOf(&testStruct{})
|
||||
tp := v.Type()
|
||||
|
||||
c.Assert(GetMethodIndexByName(tp, "Method1"), qt.Equals, 0)
|
||||
c.Assert(GetMethodIndexByName(tp, "Method3"), qt.Equals, 2)
|
||||
c.Assert(GetMethodIndexByName(tp, "Foo"), qt.Equals, -1)
|
||||
}
|
||||
|
||||
func BenchmarkIsTruthFul(b *testing.B) {
|
||||
v := reflect.ValueOf("Hugo")
|
||||
|
||||
@@ -40,3 +50,37 @@ func BenchmarkIsTruthFul(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testStruct struct{}
|
||||
|
||||
func (t *testStruct) Method1() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func (t *testStruct) Method2() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func (t *testStruct) Method3() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func (t *testStruct) Method4() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func (t *testStruct) Method5() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func BenchmarkGetMethodByName(b *testing.B) {
|
||||
v := reflect.ValueOf(&testStruct{})
|
||||
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, method := range methods {
|
||||
_ = GetMethodByName(v, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func (f TimeFormatter) Format(t time.Time, layout string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func ToTimeInDefaultLocationE(i interface{}, location *time.Location) (tim time.Time, err error) {
|
||||
func ToTimeInDefaultLocationE(i any, location *time.Location) (tim time.Time, err error) {
|
||||
switch vv := i.(type) {
|
||||
case toml.LocalDate:
|
||||
return vv.AsTime(location), nil
|
||||
|
||||
@@ -31,6 +31,11 @@ type ReadSeekCloser interface {
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// ReadSeekCloserProvider provides a ReadSeekCloser.
|
||||
type ReadSeekCloserProvider interface {
|
||||
ReadSeekCloser() (ReadSeekCloser, error)
|
||||
}
|
||||
|
||||
// ReadSeekerNoOpCloser implements ReadSeekCloser by doing nothing in Close.
|
||||
// TODO(bep) rename this and similar to ReadSeekerNopCloser, naming used in stdlib, which kind of makes sense.
|
||||
type ReadSeekerNoOpCloser struct {
|
||||
|
||||
@@ -18,6 +18,14 @@ import (
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// As implemented by strings.Builder.
|
||||
type FlexiWriter interface {
|
||||
io.Writer
|
||||
io.ByteWriter
|
||||
WriteString(s string) (int, error)
|
||||
WriteRune(r rune) (int, error)
|
||||
}
|
||||
|
||||
type multiWriteCloser struct {
|
||||
io.Writer
|
||||
closers []io.WriteCloser
|
||||
|
||||
+107
-17
@@ -21,6 +21,8 @@ import (
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
@@ -36,13 +38,6 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// commitHash contains the current Git revision.
|
||||
// Use mage to build to make sure this gets set.
|
||||
commitHash string
|
||||
|
||||
// buildDate contains the date of the current build.
|
||||
buildDate string
|
||||
|
||||
// vendorInfo contains vendor notes about the current build.
|
||||
vendorInfo string
|
||||
)
|
||||
@@ -57,6 +52,8 @@ type Info struct {
|
||||
// This can also be set by the user.
|
||||
// It can be any string, but it will be all lower case.
|
||||
Environment string
|
||||
|
||||
deps []*Dependency
|
||||
}
|
||||
|
||||
// Version returns the current version as a comparable version string.
|
||||
@@ -77,20 +74,39 @@ func (i Info) IsExtended() bool {
|
||||
return IsExtended
|
||||
}
|
||||
|
||||
// Deps gets a list of dependencies for this Hugo build.
|
||||
func (i Info) Deps() []*Dependency {
|
||||
return i.deps
|
||||
}
|
||||
|
||||
// NewInfo creates a new Hugo Info object.
|
||||
func NewInfo(environment string) Info {
|
||||
func NewInfo(environment string, deps []*Dependency) Info {
|
||||
if environment == "" {
|
||||
environment = EnvironmentProduction
|
||||
}
|
||||
var (
|
||||
commitHash string
|
||||
buildDate string
|
||||
)
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi != nil {
|
||||
commitHash = bi.Revision
|
||||
buildDate = bi.RevisionTime
|
||||
}
|
||||
|
||||
return Info{
|
||||
CommitHash: commitHash,
|
||||
BuildDate: buildDate,
|
||||
Environment: environment,
|
||||
deps: deps,
|
||||
}
|
||||
}
|
||||
|
||||
// GetExecEnviron creates and gets the common os/exec environment used in the
|
||||
// external programs we interact with via os/exec, e.g. postcss.
|
||||
func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
|
||||
env := os.Environ()
|
||||
var env []string
|
||||
nodepath := filepath.Join(workDir, "node_modules")
|
||||
if np := os.Getenv("NODE_PATH"); np != "" {
|
||||
nodepath = workDir + string(os.PathListSeparator) + np
|
||||
@@ -98,18 +114,68 @@ func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
|
||||
config.SetEnvVars(&env, "NODE_PATH", nodepath)
|
||||
config.SetEnvVars(&env, "PWD", workDir)
|
||||
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.GetString("environment"))
|
||||
fis, err := afero.ReadDir(fs, files.FolderJSConfig)
|
||||
if err == nil {
|
||||
for _, fi := range fis {
|
||||
key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_"))
|
||||
value := fi.(hugofs.FileMetaInfo).Meta().Filename
|
||||
config.SetEnvVars(&env, key, value)
|
||||
config.SetEnvVars(&env, "HUGO_ENV", cfg.GetString("environment"))
|
||||
|
||||
if fs != nil {
|
||||
fis, err := afero.ReadDir(fs, files.FolderJSConfig)
|
||||
if err == nil {
|
||||
for _, fi := range fis {
|
||||
key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_"))
|
||||
value := fi.(hugofs.FileMetaInfo).Meta().Filename
|
||||
config.SetEnvVars(&env, key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
type buildInfo struct {
|
||||
VersionControlSystem string
|
||||
Revision string
|
||||
RevisionTime string
|
||||
Modified bool
|
||||
|
||||
GoOS string
|
||||
GoArch string
|
||||
|
||||
*debug.BuildInfo
|
||||
}
|
||||
|
||||
var bInfo *buildInfo
|
||||
var bInfoInit sync.Once
|
||||
|
||||
func getBuildInfo() *buildInfo {
|
||||
bInfoInit.Do(func() {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
bInfo = &buildInfo{BuildInfo: bi}
|
||||
|
||||
for _, s := range bInfo.Settings {
|
||||
switch s.Key {
|
||||
case "vcs":
|
||||
bInfo.VersionControlSystem = s.Value
|
||||
case "vcs.revision":
|
||||
bInfo.Revision = s.Value
|
||||
case "vcs.time":
|
||||
bInfo.RevisionTime = s.Value
|
||||
case "vcs.modified":
|
||||
bInfo.Modified = s.Value == "true"
|
||||
case "GOOS":
|
||||
bInfo.GoOS = s.Value
|
||||
case "GOARCH":
|
||||
bInfo.GoArch = s.Value
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return bInfo
|
||||
}
|
||||
|
||||
// GetDependencyList returns a sorted dependency list on the format package="version".
|
||||
// It includes both Go dependencies and (a manually maintained) list of C(++) dependencies.
|
||||
func GetDependencyList() []string {
|
||||
@@ -128,8 +194,8 @@ func GetDependencyList() []string {
|
||||
)
|
||||
}
|
||||
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
bi := getBuildInfo()
|
||||
if bi == nil {
|
||||
return deps
|
||||
}
|
||||
|
||||
@@ -151,3 +217,27 @@ func IsRunningAsTest() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Dependency is a single dependency, which can be either a Hugo Module or a local theme.
|
||||
type Dependency struct {
|
||||
// Returns the path to this module.
|
||||
// This will either be the module path, e.g. "github.com/gohugoio/myshortcodes",
|
||||
// or the path below your /theme folder, e.g. "mytheme".
|
||||
Path string
|
||||
|
||||
// The module version.
|
||||
Version string
|
||||
|
||||
// Whether this dependency is vendored.
|
||||
Vendor bool
|
||||
|
||||
// Time version was created.
|
||||
Time time.Time
|
||||
|
||||
// In the dependency tree, this is the first module that defines this module
|
||||
// as a dependency.
|
||||
Owner *Dependency
|
||||
|
||||
// Replaced by this dependency.
|
||||
Replace *Dependency
|
||||
}
|
||||
|
||||
@@ -23,17 +23,21 @@ import (
|
||||
func TestHugoInfo(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
hugoInfo := NewInfo("")
|
||||
hugoInfo := NewInfo("", nil)
|
||||
|
||||
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
|
||||
c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.CommitHash, qt.Equals, commitHash)
|
||||
c.Assert(hugoInfo.BuildDate, qt.Equals, buildDate)
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi != nil {
|
||||
c.Assert(hugoInfo.CommitHash, qt.Equals, bi.Revision)
|
||||
c.Assert(hugoInfo.BuildDate, qt.Equals, bi.RevisionTime)
|
||||
}
|
||||
c.Assert(hugoInfo.Environment, qt.Equals, "production")
|
||||
c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
|
||||
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
|
||||
|
||||
devHugoInfo := NewInfo("development")
|
||||
devHugoInfo := NewInfo("development", nil)
|
||||
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build extended
|
||||
// +build extended
|
||||
|
||||
package hugo
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !extended
|
||||
// +build !extended
|
||||
|
||||
package hugo
|
||||
|
||||
+14
-9
@@ -58,13 +58,13 @@ func (h VersionString) String() string {
|
||||
}
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other interface{}) int {
|
||||
func (h VersionString) Compare(other any) int {
|
||||
v := MustParseVersion(h.String())
|
||||
return compareVersionsWithSuffix(v.Number, v.PatchLevel, v.Suffix, other)
|
||||
}
|
||||
|
||||
// Eq implements the compare.Eqer interface.
|
||||
func (h VersionString) Eq(other interface{}) bool {
|
||||
func (h VersionString) Eq(other any) bool {
|
||||
s, err := cast.ToStringE(other)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -131,16 +131,21 @@ func BuildVersionString() string {
|
||||
program := "hugo"
|
||||
|
||||
version := "v" + CurrentVersion.String()
|
||||
if commitHash != "" {
|
||||
version += "-" + strings.ToUpper(commitHash)
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi == nil {
|
||||
return version
|
||||
}
|
||||
if bi.Revision != "" {
|
||||
version += "-" + bi.Revision
|
||||
}
|
||||
if IsExtended {
|
||||
version += "+extended"
|
||||
}
|
||||
|
||||
osArch := runtime.GOOS + "/" + runtime.GOARCH
|
||||
osArch := bi.GoOS + "/" + bi.GoArch
|
||||
|
||||
date := buildDate
|
||||
date := bi.RevisionTime
|
||||
if date == "" {
|
||||
date = "unknown"
|
||||
}
|
||||
@@ -166,15 +171,15 @@ func version(version float32, patchVersion int, suffix string) string {
|
||||
// running Hugo version.
|
||||
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
|
||||
// the running version.
|
||||
func CompareVersion(version interface{}) int {
|
||||
func CompareVersion(version any) int {
|
||||
return compareVersionsWithSuffix(CurrentVersion.Number, CurrentVersion.PatchLevel, CurrentVersion.Suffix, version)
|
||||
}
|
||||
|
||||
func compareVersions(inVersion float32, inPatchVersion int, in interface{}) int {
|
||||
func compareVersions(inVersion float32, inPatchVersion int, in any) int {
|
||||
return compareVersionsWithSuffix(inVersion, inPatchVersion, "", in)
|
||||
}
|
||||
|
||||
func compareVersionsWithSuffix(inVersion float32, inPatchVersion int, suffix string, in interface{}) int {
|
||||
func compareVersionsWithSuffix(inVersion float32, inPatchVersion int, suffix string, in any) int {
|
||||
var c int
|
||||
switch d := in.(type) {
|
||||
case float64:
|
||||
|
||||
@@ -16,7 +16,7 @@ package hugo
|
||||
// CurrentVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Number: 0.89,
|
||||
PatchLevel: 0,
|
||||
Number: 0.97,
|
||||
PatchLevel: 1,
|
||||
Suffix: "",
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// IgnorableLogger is a logger that ignores certain log statements.
|
||||
type IgnorableLogger interface {
|
||||
Logger
|
||||
Errorsf(statementID, format string, v ...interface{})
|
||||
Errorsf(statementID, format string, v ...any)
|
||||
Apply(logger Logger) IgnorableLogger
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func NewIgnorableLogger(logger Logger, statements ...string) IgnorableLogger {
|
||||
}
|
||||
|
||||
// Errorsf logs statementID as an ERROR if not configured as ignoreable.
|
||||
func (l ignorableLogger) Errorsf(statementID, format string, v ...interface{}) {
|
||||
func (l ignorableLogger) Errorsf(statementID, format string, v ...any) {
|
||||
if l.statements[statementID] {
|
||||
// Ignore.
|
||||
return
|
||||
|
||||
+33
-22
@@ -29,8 +29,11 @@ import (
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
// Counts ERROR logs to the global jww logger.
|
||||
var GlobalErrorCounter *jww.Counter
|
||||
var (
|
||||
// Counts ERROR logs to the global jww logger.
|
||||
GlobalErrorCounter *jww.Counter
|
||||
PanicOnWarning bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
GlobalErrorCounter = &jww.Counter{}
|
||||
@@ -55,21 +58,21 @@ func (w prefixWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
Println(v ...interface{})
|
||||
Printf(format string, v ...any)
|
||||
Println(v ...any)
|
||||
PrintTimerIfDelayed(start time.Time, name string)
|
||||
Debug() *log.Logger
|
||||
Debugf(format string, v ...interface{})
|
||||
Debugln(v ...interface{})
|
||||
Debugf(format string, v ...any)
|
||||
Debugln(v ...any)
|
||||
Info() *log.Logger
|
||||
Infof(format string, v ...interface{})
|
||||
Infoln(v ...interface{})
|
||||
Infof(format string, v ...any)
|
||||
Infoln(v ...any)
|
||||
Warn() *log.Logger
|
||||
Warnf(format string, v ...interface{})
|
||||
Warnln(v ...interface{})
|
||||
Warnf(format string, v ...any)
|
||||
Warnln(v ...any)
|
||||
Error() *log.Logger
|
||||
Errorf(format string, v ...interface{})
|
||||
Errorln(v ...interface{})
|
||||
Errorf(format string, v ...any)
|
||||
Errorln(v ...any)
|
||||
Errors() string
|
||||
|
||||
Out() io.Writer
|
||||
@@ -98,11 +101,11 @@ type logger struct {
|
||||
errors *bytes.Buffer
|
||||
}
|
||||
|
||||
func (l *logger) Printf(format string, v ...interface{}) {
|
||||
func (l *logger) Printf(format string, v ...any) {
|
||||
l.FEEDBACK.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Println(v ...interface{}) {
|
||||
func (l *logger) Println(v ...any) {
|
||||
l.FEEDBACK.Println(v...)
|
||||
}
|
||||
|
||||
@@ -110,19 +113,19 @@ func (l *logger) Debug() *log.Logger {
|
||||
return l.DEBUG
|
||||
}
|
||||
|
||||
func (l *logger) Debugf(format string, v ...interface{}) {
|
||||
func (l *logger) Debugf(format string, v ...any) {
|
||||
l.DEBUG.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Debugln(v ...interface{}) {
|
||||
func (l *logger) Debugln(v ...any) {
|
||||
l.DEBUG.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Infof(format string, v ...interface{}) {
|
||||
func (l *logger) Infof(format string, v ...any) {
|
||||
l.INFO.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Infoln(v ...interface{}) {
|
||||
func (l *logger) Infoln(v ...any) {
|
||||
l.INFO.Println(v...)
|
||||
}
|
||||
|
||||
@@ -130,23 +133,31 @@ func (l *logger) Info() *log.Logger {
|
||||
return l.INFO
|
||||
}
|
||||
|
||||
func (l *logger) Warnf(format string, v ...interface{}) {
|
||||
const panicOnWarningMessage = "Warning trapped. Remove the --panicOnWarning flag to continue."
|
||||
|
||||
func (l *logger) Warnf(format string, v ...any) {
|
||||
l.WARN.Printf(format, v...)
|
||||
if PanicOnWarning {
|
||||
panic(panicOnWarningMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) Warnln(v ...interface{}) {
|
||||
func (l *logger) Warnln(v ...any) {
|
||||
l.WARN.Println(v...)
|
||||
if PanicOnWarning {
|
||||
panic(panicOnWarningMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) Warn() *log.Logger {
|
||||
return l.WARN
|
||||
}
|
||||
|
||||
func (l *logger) Errorf(format string, v ...interface{}) {
|
||||
func (l *logger) Errorf(format string, v ...any) {
|
||||
l.ERROR.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Errorln(v ...interface{}) {
|
||||
func (l *logger) Errorln(v ...any) {
|
||||
l.ERROR.Println(v...)
|
||||
}
|
||||
|
||||
|
||||
+33
-19
@@ -24,12 +24,12 @@ import (
|
||||
)
|
||||
|
||||
// ToStringMapE converts in to map[string]interface{}.
|
||||
func ToStringMapE(in interface{}) (map[string]interface{}, error) {
|
||||
func ToStringMapE(in any) (map[string]any, error) {
|
||||
switch vv := in.(type) {
|
||||
case Params:
|
||||
return vv, nil
|
||||
case map[string]string:
|
||||
var m = map[string]interface{}{}
|
||||
var m = map[string]any{}
|
||||
for k, v := range vv {
|
||||
m[k] = v
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func ToStringMapE(in interface{}) (map[string]interface{}, error) {
|
||||
// ToParamsAndPrepare converts in to Params and prepares it for use.
|
||||
// If in is nil, an empty map is returned.
|
||||
// See PrepareParams.
|
||||
func ToParamsAndPrepare(in interface{}) (Params, bool) {
|
||||
func ToParamsAndPrepare(in any) (Params, bool) {
|
||||
if types.IsNil(in) {
|
||||
return Params{}, true
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func ToParamsAndPrepare(in interface{}) (Params, bool) {
|
||||
}
|
||||
|
||||
// MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails.
|
||||
func MustToParamsAndPrepare(in interface{}) Params {
|
||||
func MustToParamsAndPrepare(in any) Params {
|
||||
if p, ok := ToParamsAndPrepare(in); ok {
|
||||
return p
|
||||
} else {
|
||||
@@ -65,13 +65,13 @@ func MustToParamsAndPrepare(in interface{}) Params {
|
||||
}
|
||||
|
||||
// ToStringMap converts in to map[string]interface{}.
|
||||
func ToStringMap(in interface{}) map[string]interface{} {
|
||||
func ToStringMap(in any) map[string]any {
|
||||
m, _ := ToStringMapE(in)
|
||||
return m
|
||||
}
|
||||
|
||||
// ToStringMapStringE converts in to map[string]string.
|
||||
func ToStringMapStringE(in interface{}) (map[string]string, error) {
|
||||
func ToStringMapStringE(in any) (map[string]string, error) {
|
||||
m, err := ToStringMapE(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -80,26 +80,26 @@ func ToStringMapStringE(in interface{}) (map[string]string, error) {
|
||||
}
|
||||
|
||||
// ToStringMapString converts in to map[string]string.
|
||||
func ToStringMapString(in interface{}) map[string]string {
|
||||
func ToStringMapString(in any) map[string]string {
|
||||
m, _ := ToStringMapStringE(in)
|
||||
return m
|
||||
}
|
||||
|
||||
// ToStringMapBool converts in to bool.
|
||||
func ToStringMapBool(in interface{}) map[string]bool {
|
||||
func ToStringMapBool(in any) map[string]bool {
|
||||
m, _ := ToStringMapE(in)
|
||||
return cast.ToStringMapBool(m)
|
||||
}
|
||||
|
||||
// ToSliceStringMap converts in to []map[string]interface{}.
|
||||
func ToSliceStringMap(in interface{}) ([]map[string]interface{}, error) {
|
||||
func ToSliceStringMap(in any) ([]map[string]any, error) {
|
||||
switch v := in.(type) {
|
||||
case []map[string]interface{}:
|
||||
case []map[string]any:
|
||||
return v, nil
|
||||
case []interface{}:
|
||||
var s []map[string]interface{}
|
||||
case []any:
|
||||
var s []map[string]any
|
||||
for _, entry := range v {
|
||||
if vv, ok := entry.(map[string]interface{}); ok {
|
||||
if vv, ok := entry.(map[string]any); ok {
|
||||
s = append(s, vv)
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,20 @@ func ToSliceStringMap(in interface{}) ([]map[string]interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// LookupEqualFold finds key in m with case insensitive equality checks.
|
||||
func LookupEqualFold[T any | string](m map[string]T, key string) (T, bool) {
|
||||
if v, found := m[key]; found {
|
||||
return v, true
|
||||
}
|
||||
for k, v := range m {
|
||||
if strings.EqualFold(k, key) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
var s T
|
||||
return s, false
|
||||
}
|
||||
|
||||
type keyRename struct {
|
||||
pattern glob.Glob
|
||||
newKey string
|
||||
@@ -146,7 +160,7 @@ func (r KeyRenamer) getNewKey(keyPath string) string {
|
||||
|
||||
// Rename renames the keys in the given map according
|
||||
// to the patterns in the current KeyRenamer.
|
||||
func (r KeyRenamer) Rename(m map[string]interface{}) {
|
||||
func (r KeyRenamer) Rename(m map[string]any) {
|
||||
r.renamePath("", m)
|
||||
}
|
||||
|
||||
@@ -158,15 +172,15 @@ func (KeyRenamer) keyPath(k1, k2 string) string {
|
||||
return k1 + "/" + k2
|
||||
}
|
||||
|
||||
func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) {
|
||||
func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]any) {
|
||||
for key, val := range m {
|
||||
keyPath := r.keyPath(parentKeyPath, key)
|
||||
switch val.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
case map[any]any:
|
||||
val = cast.ToStringMap(val)
|
||||
r.renamePath(keyPath, val.(map[string]interface{}))
|
||||
case map[string]interface{}:
|
||||
r.renamePath(keyPath, val.(map[string]interface{}))
|
||||
r.renamePath(keyPath, val.(map[string]any))
|
||||
case map[string]any:
|
||||
r.renamePath(keyPath, val.(map[string]any))
|
||||
}
|
||||
|
||||
newKey := r.getNewKey(keyPath)
|
||||
|
||||
+44
-21
@@ -27,7 +27,7 @@ func TestPrepareParams(t *testing.T) {
|
||||
expected Params
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"abC": 32,
|
||||
},
|
||||
Params{
|
||||
@@ -35,16 +35,16 @@ func TestPrepareParams(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"abC": 32,
|
||||
"deF": map[interface{}]interface{}{
|
||||
"deF": map[any]any{
|
||||
23: "A value",
|
||||
24: map[string]interface{}{
|
||||
24: map[string]any{
|
||||
"AbCDe": "A value",
|
||||
"eFgHi": "Another value",
|
||||
},
|
||||
},
|
||||
"gHi": map[string]interface{}{
|
||||
"gHi": map[string]any{
|
||||
"J": 25,
|
||||
},
|
||||
"jKl": map[string]string{
|
||||
@@ -85,23 +85,23 @@ func TestToSliceStringMap(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
tests := []struct {
|
||||
input interface{}
|
||||
expected []map[string]interface{}
|
||||
input any
|
||||
expected []map[string]any
|
||||
}{
|
||||
{
|
||||
input: []map[string]interface{}{
|
||||
input: []map[string]any{
|
||||
{"abc": 123},
|
||||
},
|
||||
expected: []map[string]interface{}{
|
||||
expected: []map[string]any{
|
||||
{"abc": 123},
|
||||
},
|
||||
}, {
|
||||
input: []interface{}{
|
||||
map[string]interface{}{
|
||||
input: []any{
|
||||
map[string]any{
|
||||
"def": 456,
|
||||
},
|
||||
},
|
||||
expected: []map[string]interface{}{
|
||||
expected: []map[string]any{
|
||||
{"def": 456},
|
||||
},
|
||||
},
|
||||
@@ -116,7 +116,7 @@ func TestToSliceStringMap(t *testing.T) {
|
||||
|
||||
func TestToParamsAndPrepare(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
_, ok := ToParamsAndPrepare(map[string]interface{}{"A": "av"})
|
||||
_, ok := ToParamsAndPrepare(map[string]any{"A": "av"})
|
||||
c.Assert(ok, qt.IsTrue)
|
||||
|
||||
params, ok := ToParamsAndPrepare(nil)
|
||||
@@ -127,33 +127,33 @@ func TestToParamsAndPrepare(t *testing.T) {
|
||||
func TestRenameKeys(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := map[string]interface{}{
|
||||
m := map[string]any{
|
||||
"a": 32,
|
||||
"ren1": "m1",
|
||||
"ren2": "m1_2",
|
||||
"sub": map[string]interface{}{
|
||||
"subsub": map[string]interface{}{
|
||||
"sub": map[string]any{
|
||||
"subsub": map[string]any{
|
||||
"REN1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
},
|
||||
"no": map[string]interface{}{
|
||||
"no": map[string]any{
|
||||
"ren1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
expected := map[string]any{
|
||||
"a": 32,
|
||||
"new1": "m1",
|
||||
"new2": "m1_2",
|
||||
"sub": map[string]interface{}{
|
||||
"subsub": map[string]interface{}{
|
||||
"sub": map[string]any{
|
||||
"subsub": map[string]any{
|
||||
"new1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
},
|
||||
"no": map[string]interface{}{
|
||||
"no": map[string]any{
|
||||
"ren1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
@@ -171,3 +171,26 @@ func TestRenameKeys(t *testing.T) {
|
||||
t.Errorf("Expected\n%#v, got\n%#v\n", expected, m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupEqualFold(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m1 := map[string]any{
|
||||
"a": "av",
|
||||
"B": "bv",
|
||||
}
|
||||
|
||||
v, found := LookupEqualFold(m1, "b")
|
||||
c.Assert(found, qt.IsTrue)
|
||||
c.Assert(v, qt.Equals, "bv")
|
||||
|
||||
m2 := map[string]string{
|
||||
"a": "av",
|
||||
"B": "bv",
|
||||
}
|
||||
|
||||
v, found = LookupEqualFold(m2, "b")
|
||||
c.Assert(found, qt.IsTrue)
|
||||
c.Assert(v, qt.Equals, "bv")
|
||||
|
||||
}
|
||||
|
||||
+11
-11
@@ -21,11 +21,11 @@ import (
|
||||
)
|
||||
|
||||
// Params is a map where all keys are lower case.
|
||||
type Params map[string]interface{}
|
||||
type Params map[string]any
|
||||
|
||||
// Get does a lower case and nested search in this map.
|
||||
// It will return nil if none found.
|
||||
func (p Params) Get(indices ...string) interface{} {
|
||||
func (p Params) Get(indices ...string) any {
|
||||
v, _, _ := getNested(p, indices)
|
||||
return v
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) {
|
||||
p[mergeStrategyKey] = s
|
||||
}
|
||||
|
||||
func getNested(m map[string]interface{}, indices []string) (interface{}, string, map[string]interface{}) {
|
||||
func getNested(m map[string]any, indices []string) (any, string, map[string]any) {
|
||||
if len(indices) == 0 {
|
||||
return nil, "", nil
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func getNested(m map[string]interface{}, indices []string) (interface{}, string,
|
||||
switch m2 := v.(type) {
|
||||
case Params:
|
||||
return getNested(m2, indices[1:])
|
||||
case map[string]interface{}:
|
||||
case map[string]any:
|
||||
return getNested(m2, indices[1:])
|
||||
default:
|
||||
return nil, "", nil
|
||||
@@ -175,7 +175,7 @@ func getNested(m map[string]interface{}, indices []string) (interface{}, string,
|
||||
// It will first try the exact match and then try to find it as a nested map value,
|
||||
// using the given separator, e.g. "mymap.name".
|
||||
// It assumes that all the maps given have lower cased keys.
|
||||
func GetNestedParam(keyStr, separator string, candidates ...Params) (interface{}, error) {
|
||||
func GetNestedParam(keyStr, separator string, candidates ...Params) (any, error) {
|
||||
keyStr = strings.ToLower(keyStr)
|
||||
|
||||
// Try exact match first
|
||||
@@ -195,7 +195,7 @@ func GetNestedParam(keyStr, separator string, candidates ...Params) (interface{}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) {
|
||||
func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) any) (any, string, map[string]any, error) {
|
||||
keySegments := strings.Split(keyStr, separator)
|
||||
if len(keySegments) == 0 {
|
||||
return nil, "", nil, nil
|
||||
@@ -211,7 +211,7 @@ func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interf
|
||||
}
|
||||
|
||||
switch m := first.(type) {
|
||||
case map[string]interface{}:
|
||||
case map[string]any:
|
||||
v, key, owner := getNested(m, keySegments[1:])
|
||||
return v, key, owner, nil
|
||||
case Params:
|
||||
@@ -236,7 +236,7 @@ const (
|
||||
mergeStrategyKey = "_merge"
|
||||
)
|
||||
|
||||
func toMergeStrategy(v interface{}) ParamsMergeStrategy {
|
||||
func toMergeStrategy(v any) ParamsMergeStrategy {
|
||||
s := ParamsMergeStrategy(cast.ToString(v))
|
||||
switch s {
|
||||
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
|
||||
@@ -260,13 +260,13 @@ func PrepareParams(m Params) {
|
||||
retyped = true
|
||||
} else {
|
||||
switch vv := v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
case map[any]any:
|
||||
var p Params = cast.ToStringMap(v)
|
||||
v = p
|
||||
PrepareParams(p)
|
||||
retyped = true
|
||||
case map[string]interface{}:
|
||||
var p Params = v.(map[string]interface{})
|
||||
case map[string]any:
|
||||
var p Params = v.(map[string]any)
|
||||
v = p
|
||||
PrepareParams(p)
|
||||
retyped = true
|
||||
|
||||
@@ -20,13 +20,13 @@ import (
|
||||
)
|
||||
|
||||
func TestGetNestedParam(t *testing.T) {
|
||||
m := map[string]interface{}{
|
||||
m := map[string]any{
|
||||
"string": "value",
|
||||
"first": 1,
|
||||
"with_underscore": 2,
|
||||
"nested": map[string]interface{}{
|
||||
"nested": map[string]any{
|
||||
"color": "blue",
|
||||
"nestednested": map[string]interface{}{
|
||||
"nestednested": map[string]any{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
@@ -34,7 +34,7 @@ func TestGetNestedParam(t *testing.T) {
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
must := func(keyStr, separator string, candidates ...Params) interface{} {
|
||||
must := func(keyStr, separator string, candidates ...Params) any {
|
||||
v, err := GetNestedParam(keyStr, separator, candidates...)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return v
|
||||
@@ -53,14 +53,14 @@ func TestGetNestedParam(t *testing.T) {
|
||||
func TestGetNestedParamFnNestedNewKey(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
nested := map[string]interface{}{
|
||||
nested := map[string]any{
|
||||
"color": "blue",
|
||||
}
|
||||
m := map[string]interface{}{
|
||||
m := map[string]any{
|
||||
"nested": nested,
|
||||
}
|
||||
|
||||
existing, nestedKey, owner, err := GetNestedParamFn("nested.new", ".", func(key string) interface{} {
|
||||
existing, nestedKey, owner, err := GetNestedParamFn("nested.new", ".", func(key string) any {
|
||||
return m[key]
|
||||
})
|
||||
|
||||
|
||||
+14
-14
@@ -24,7 +24,7 @@ import (
|
||||
|
||||
// Scratch is a writable context used for stateful operations in Page/Node rendering.
|
||||
type Scratch struct {
|
||||
values map[string]interface{}
|
||||
values map[string]any
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ func NewScratcher() Scratcher {
|
||||
// Supports numeric values and strings.
|
||||
//
|
||||
// If the first add for a key is an array or slice, then the next value(s) will be appended.
|
||||
func (c *Scratch) Add(key string, newAddend interface{}) (string, error) {
|
||||
var newVal interface{}
|
||||
func (c *Scratch) Add(key string, newAddend any) (string, error) {
|
||||
var newVal any
|
||||
c.mu.RLock()
|
||||
existingAddend, found := c.values[key]
|
||||
c.mu.RUnlock()
|
||||
@@ -82,7 +82,7 @@ func (c *Scratch) Add(key string, newAddend interface{}) (string, error) {
|
||||
|
||||
// Set stores a value with the given key in the Node context.
|
||||
// This value can later be retrieved with Get.
|
||||
func (c *Scratch) Set(key string, value interface{}) string {
|
||||
func (c *Scratch) Set(key string, value any) string {
|
||||
c.mu.Lock()
|
||||
c.values[key] = value
|
||||
c.mu.Unlock()
|
||||
@@ -98,7 +98,7 @@ func (c *Scratch) Delete(key string) string {
|
||||
}
|
||||
|
||||
// Get returns a value previously set by Add or Set.
|
||||
func (c *Scratch) Get(key string) interface{} {
|
||||
func (c *Scratch) Get(key string) any {
|
||||
c.mu.RLock()
|
||||
val := c.values[key]
|
||||
c.mu.RUnlock()
|
||||
@@ -109,7 +109,7 @@ func (c *Scratch) Get(key string) interface{} {
|
||||
// Values returns the raw backing map. Note that you should just use
|
||||
// this method on the locally scoped Scratch instances you obtain via newScratch, not
|
||||
// .Page.Scratch etc., as that will lead to concurrency issues.
|
||||
func (c *Scratch) Values() map[string]interface{} {
|
||||
func (c *Scratch) Values() map[string]any {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.values
|
||||
@@ -117,14 +117,14 @@ func (c *Scratch) Values() map[string]interface{} {
|
||||
|
||||
// SetInMap stores a value to a map with the given key in the Node context.
|
||||
// This map can later be retrieved with GetSortedMapValues.
|
||||
func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string {
|
||||
func (c *Scratch) SetInMap(key string, mapKey string, value any) string {
|
||||
c.mu.Lock()
|
||||
_, found := c.values[key]
|
||||
if !found {
|
||||
c.values[key] = make(map[string]interface{})
|
||||
c.values[key] = make(map[string]any)
|
||||
}
|
||||
|
||||
c.values[key].(map[string]interface{})[mapKey] = value
|
||||
c.values[key].(map[string]any)[mapKey] = value
|
||||
c.mu.Unlock()
|
||||
return ""
|
||||
}
|
||||
@@ -134,14 +134,14 @@ func (c *Scratch) DeleteInMap(key string, mapKey string) string {
|
||||
c.mu.Lock()
|
||||
_, found := c.values[key]
|
||||
if found {
|
||||
delete(c.values[key].(map[string]interface{}), mapKey)
|
||||
delete(c.values[key].(map[string]any), mapKey)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSortedMapValues returns a sorted map previously filled with SetInMap.
|
||||
func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
func (c *Scratch) GetSortedMapValues(key string) any {
|
||||
c.mu.RLock()
|
||||
|
||||
if c.values[key] == nil {
|
||||
@@ -149,7 +149,7 @@ func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
unsortedMap := c.values[key].(map[string]interface{})
|
||||
unsortedMap := c.values[key].(map[string]any)
|
||||
c.mu.RUnlock()
|
||||
var keys []string
|
||||
for mapKey := range unsortedMap {
|
||||
@@ -158,7 +158,7 @@ func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
sortedArray := make([]interface{}, len(unsortedMap))
|
||||
sortedArray := make([]any, len(unsortedMap))
|
||||
for i, mapKey := range keys {
|
||||
sortedArray[i] = unsortedMap[mapKey]
|
||||
}
|
||||
@@ -168,5 +168,5 @@ func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
|
||||
// NewScratch returns a new instance of Scratch.
|
||||
func NewScratch() *Scratch {
|
||||
return &Scratch{values: make(map[string]interface{})}
|
||||
return &Scratch{values: make(map[string]any)}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestScratchAddTypedSliceToInterfaceSlice(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Set("slice", []interface{}{})
|
||||
scratch.Set("slice", []any{})
|
||||
|
||||
_, err := scratch.Add("slice", []int{1, 2})
|
||||
c.Assert(err, qt.IsNil)
|
||||
@@ -107,7 +107,7 @@ func TestScratchAddDifferentTypedSliceToInterfaceSlice(t *testing.T) {
|
||||
|
||||
_, err := scratch.Add("slice", []int{1, 2})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(scratch.Get("slice"), qt.DeepEquals, []interface{}{"foo", 1, 2})
|
||||
c.Assert(scratch.Get("slice"), qt.DeepEquals, []any{"foo", 1, 2})
|
||||
}
|
||||
|
||||
func TestScratchSet(t *testing.T) {
|
||||
@@ -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, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"})
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []any{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "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, []interface{}{0: "Def", 1: "Lux", 2: "Zyx"})
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []any{0: "Def", 1: "Lux", 2: "Zyx"})
|
||||
}
|
||||
|
||||
func TestScratchGetSortedMapValues(t *testing.T) {
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to
|
||||
// determine the type of the two terms.
|
||||
func DoArithmetic(a, b interface{}, op rune) (interface{}, error) {
|
||||
func DoArithmetic(a, b any, op rune) (any, error) {
|
||||
av := reflect.ValueOf(a)
|
||||
bv := reflect.ValueOf(b)
|
||||
var ai, bi int64
|
||||
|
||||
@@ -24,10 +24,10 @@ func TestDoArithmetic(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
a interface{}
|
||||
b interface{}
|
||||
a any
|
||||
b any
|
||||
op rune
|
||||
expect interface{}
|
||||
expect any
|
||||
}{
|
||||
{3, 2, '+', int64(5)},
|
||||
{3, 2, '-', int64(1)},
|
||||
|
||||
+8
-55
@@ -16,7 +16,6 @@ package paths
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -36,8 +35,7 @@ type filepathPathBridge interface {
|
||||
Separator() string
|
||||
}
|
||||
|
||||
type filepathBridge struct {
|
||||
}
|
||||
type filepathBridge struct{}
|
||||
|
||||
func (filepathBridge) Base(in string) string {
|
||||
return filepath.Base(in)
|
||||
@@ -65,9 +63,13 @@ func (filepathBridge) Separator() string {
|
||||
|
||||
var fpb filepathBridge
|
||||
|
||||
// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer.
|
||||
func ToSlashTrimLeading(s string) string {
|
||||
return strings.TrimPrefix(filepath.ToSlash(s), "/")
|
||||
// AbsPathify creates an absolute path if given a working dir and a relative path.
|
||||
// If already absolute, the path is just cleaned.
|
||||
func AbsPathify(workingDir, inPath string) string {
|
||||
if filepath.IsAbs(inPath) {
|
||||
return filepath.Clean(inPath)
|
||||
}
|
||||
return filepath.Join(workingDir, inPath)
|
||||
}
|
||||
|
||||
// MakeTitle converts the path given to a suitable title, trimming whitespace
|
||||
@@ -233,23 +235,6 @@ func GetRelativePath(path, base string) (final string, err error) {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// PathPrep prepares the path using the uglify setting to create paths on
|
||||
// either the form /section/name/index.html or /section/name.html.
|
||||
func PathPrep(ugly bool, in string) string {
|
||||
if ugly {
|
||||
return Uglify(in)
|
||||
}
|
||||
return PrettifyPath(in)
|
||||
}
|
||||
|
||||
// PrettifyPath is the same as PrettifyURLPath but for file paths.
|
||||
// /section/name.html becomes /section/name/index.html
|
||||
// /section/name/ becomes /section/name/index.html
|
||||
// /section/name/index.html becomes /section/name/index.html
|
||||
func PrettifyPath(in string) string {
|
||||
return prettifyPath(in, fpb)
|
||||
}
|
||||
|
||||
func prettifyPath(in string, b filepathPathBridge) string {
|
||||
if filepath.Ext(in) == "" {
|
||||
// /section/name/ -> /section/name/index.html
|
||||
@@ -278,35 +263,3 @@ func (n NamedSlice) String() string {
|
||||
}
|
||||
return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ","))
|
||||
}
|
||||
|
||||
// FindCWD returns the current working directory from where the Hugo
|
||||
// executable is run.
|
||||
func FindCWD() (string, error) {
|
||||
serverFile, err := filepath.Abs(os.Args[0])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't get absolute path for executable: %v", err)
|
||||
}
|
||||
|
||||
path := filepath.Dir(serverFile)
|
||||
realFile, err := filepath.EvalSymlinks(serverFile)
|
||||
if err != nil {
|
||||
if _, err = os.Stat(serverFile + ".exe"); err == nil {
|
||||
realFile = filepath.Clean(serverFile + ".exe")
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && realFile != serverFile {
|
||||
path = filepath.Dir(realFile)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// AddTrailingSlash adds a trailing Unix styled slash (/) if not already
|
||||
// there.
|
||||
func AddTrailingSlash(path string) string {
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestGetRelativePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
base string
|
||||
expect interface{}
|
||||
expect any
|
||||
}{
|
||||
{filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")},
|
||||
{filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")},
|
||||
@@ -226,27 +226,3 @@ func TestFileAndExt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindCWD(t *testing.T) {
|
||||
type test struct {
|
||||
expectedDir string
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
// cwd, _ := os.Getwd()
|
||||
data := []test{
|
||||
//{cwd, nil},
|
||||
// Commenting this out. It doesn't work properly.
|
||||
// There's a good reason why we don't use os.Getwd(), it doesn't actually work the way we want it to.
|
||||
// I really don't know a better way to test this function. - SPF 2014.11.04
|
||||
}
|
||||
for i, d := range data {
|
||||
dir, err := FindCWD()
|
||||
if d.expectedDir != dir {
|
||||
t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedDir, dir)
|
||||
}
|
||||
if d.expectedErr != err {
|
||||
t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-59
@@ -18,12 +18,9 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/purell"
|
||||
)
|
||||
|
||||
type pathBridge struct {
|
||||
}
|
||||
type pathBridge struct{}
|
||||
|
||||
func (pathBridge) Base(in string) string {
|
||||
return path.Base(in)
|
||||
@@ -51,51 +48,6 @@ func (pathBridge) Separator() string {
|
||||
|
||||
var pb pathBridge
|
||||
|
||||
func sanitizeURLWithFlags(in string, f purell.NormalizationFlags) string {
|
||||
s, err := purell.NormalizeURLString(in, f)
|
||||
if err != nil {
|
||||
return in
|
||||
}
|
||||
|
||||
// Temporary workaround for the bug fix and resulting
|
||||
// behavioral change in purell.NormalizeURLString():
|
||||
// a leading '/' was inadvertently added to relative links,
|
||||
// but no longer, see #878.
|
||||
//
|
||||
// I think the real solution is to allow Hugo to
|
||||
// make relative URL with relative path,
|
||||
// e.g. "../../post/hello-again/", as wished by users
|
||||
// in issues #157, #622, etc., without forcing
|
||||
// relative URLs to begin with '/'.
|
||||
// Once the fixes are in, let's remove this kludge
|
||||
// and restore SanitizeURL() to the way it was.
|
||||
// -- @anthonyfok, 2015-02-16
|
||||
//
|
||||
// Begin temporary kludge
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(u.Path) > 0 && !strings.HasPrefix(u.Path, "/") {
|
||||
u.Path = "/" + u.Path
|
||||
}
|
||||
return u.String()
|
||||
// End temporary kludge
|
||||
|
||||
// return s
|
||||
|
||||
}
|
||||
|
||||
// SanitizeURL sanitizes the input URL string.
|
||||
func SanitizeURL(in string) string {
|
||||
return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveTrailingSlash|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator)
|
||||
}
|
||||
|
||||
// SanitizeURLKeepTrailingSlash is the same as SanitizeURL, but will keep any trailing slash.
|
||||
func SanitizeURLKeepTrailingSlash(in string) string {
|
||||
return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator)
|
||||
}
|
||||
|
||||
// MakePermalink combines base URL with content path to create full URL paths.
|
||||
// Example
|
||||
// base: http://spf13.com/
|
||||
@@ -127,16 +79,6 @@ func MakePermalink(host, plink string) *url.URL {
|
||||
return base
|
||||
}
|
||||
|
||||
// IsAbsURL determines whether the given path points to an absolute URL.
|
||||
func IsAbsURL(path string) bool {
|
||||
url, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return url.IsAbs() || strings.HasPrefix(path, "//")
|
||||
}
|
||||
|
||||
// AddContextRoot adds the context root to an URL if it's not already set.
|
||||
// For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite),
|
||||
// relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set.
|
||||
|
||||
@@ -14,41 +14,11 @@
|
||||
package paths
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestSanitizeURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"http://foo.bar/", "http://foo.bar"},
|
||||
{"http://foo.bar", "http://foo.bar"}, // issue #1105
|
||||
{"http://foo.bar/zoo/", "http://foo.bar/zoo"}, // issue #931
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
o1 := SanitizeURL(test.input)
|
||||
o2 := SanitizeURLKeepTrailingSlash(test.input)
|
||||
|
||||
expected2 := test.expected
|
||||
|
||||
if strings.HasSuffix(test.input, "/") && !strings.HasSuffix(expected2, "/") {
|
||||
expected2 += "/"
|
||||
}
|
||||
|
||||
if o1 != test.expected {
|
||||
t.Errorf("[%d] 1: Expected %#v, got %#v\n", i, test.expected, o1)
|
||||
}
|
||||
if o2 != expected2 {
|
||||
t.Errorf("[%d] 2: Expected %#v, got %#v\n", i, expected2, o2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePermalink(t *testing.T) {
|
||||
type test struct {
|
||||
host, link, output string
|
||||
|
||||
@@ -69,7 +69,7 @@ func createPositionStringFormatter(formatStr string) func(p Position) string {
|
||||
format := replacer.Replace(formatStr)
|
||||
|
||||
f := func(pos Position) string {
|
||||
args := make([]interface{}, len(identifiersFound))
|
||||
args := make([]any, len(identifiersFound))
|
||||
for i, id := range identifiersFound {
|
||||
switch id {
|
||||
case ":file":
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
var accentTransformerPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
},
|
||||
}
|
||||
@@ -45,3 +46,18 @@ func RemoveAccentsString(s string) string {
|
||||
accentTransformerPool.Put(t)
|
||||
return s
|
||||
}
|
||||
|
||||
// Chomp removes trailing newline characters from s.
|
||||
func Chomp(s string) string {
|
||||
return strings.TrimRightFunc(s, func(r rune) bool {
|
||||
return r == '\n' || r == '\r'
|
||||
})
|
||||
}
|
||||
|
||||
// Puts adds a trailing \n none found.
|
||||
func Puts(s string) string {
|
||||
if s == "" || s[len(s)-1] == '\n' {
|
||||
return s
|
||||
}
|
||||
return s + "\n"
|
||||
}
|
||||
|
||||
@@ -26,3 +26,18 @@ func TestRemoveAccents(t *testing.T) {
|
||||
c.Assert(string(RemoveAccents([]byte("Hugo Rocks!"))), qt.Equals, "Hugo Rocks!")
|
||||
c.Assert(string(RemoveAccentsString("Resumé")), qt.Equals, "Resume")
|
||||
}
|
||||
|
||||
func TestChomp(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(Chomp("\nA\n"), qt.Equals, "\nA")
|
||||
c.Assert(Chomp("A\r\n"), qt.Equals, "A")
|
||||
}
|
||||
|
||||
func TestPuts(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(Puts("A"), qt.Equals, "A\n")
|
||||
c.Assert(Puts("\nA\n"), qt.Equals, "\nA\n")
|
||||
c.Assert(Puts(""), qt.Equals, "")
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@ import (
|
||||
|
||||
// ToDuration converts v to time.Duration.
|
||||
// See ToDurationE if you need to handle errors.
|
||||
func ToDuration(v interface{}) time.Duration {
|
||||
func ToDuration(v any) time.Duration {
|
||||
d, _ := ToDurationE(v)
|
||||
return d
|
||||
}
|
||||
|
||||
// ToDurationE converts v to time.Duration.
|
||||
func ToDurationE(v interface{}) (time.Duration, error) {
|
||||
func ToDurationE(v any) (time.Duration, error) {
|
||||
if n := cast.ToInt(v); n > 0 {
|
||||
return time.Duration(n) * time.Millisecond, nil
|
||||
}
|
||||
@@ -44,14 +44,14 @@ func ToDurationE(v interface{}) (time.Duration, error) {
|
||||
|
||||
// ToStringSlicePreserveString is the same as ToStringSlicePreserveStringE,
|
||||
// but it never fails.
|
||||
func ToStringSlicePreserveString(v interface{}) []string {
|
||||
func ToStringSlicePreserveString(v any) []string {
|
||||
vv, _ := ToStringSlicePreserveStringE(v)
|
||||
return vv
|
||||
}
|
||||
|
||||
// ToStringSlicePreserveStringE converts v to a string slice.
|
||||
// If v is a string, it will be wrapped in a string slice.
|
||||
func ToStringSlicePreserveStringE(v interface{}) ([]string, error) {
|
||||
func ToStringSlicePreserveStringE(v any) ([]string, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func ToStringSlicePreserveStringE(v interface{}) ([]string, error) {
|
||||
// TypeToString converts v to a string if it's a valid string type.
|
||||
// Note that this will not try to convert numeric values etc.,
|
||||
// use ToString for that.
|
||||
func TypeToString(v interface{}) (string, bool) {
|
||||
func TypeToString(v any) (string, bool) {
|
||||
switch s := v.(type) {
|
||||
case string:
|
||||
return s, true
|
||||
@@ -110,13 +110,13 @@ func TypeToString(v interface{}) (string, bool) {
|
||||
}
|
||||
|
||||
// ToString converts v to a string.
|
||||
func ToString(v interface{}) string {
|
||||
func ToString(v any) string {
|
||||
s, _ := ToStringE(v)
|
||||
return s
|
||||
}
|
||||
|
||||
// ToStringE converts v to a string.
|
||||
func ToStringE(v interface{}) (string, error) {
|
||||
func ToStringE(v any) (string, error) {
|
||||
if s, ok := TypeToString(v); ok {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestToStringSlicePreserveString(t *testing.T) {
|
||||
|
||||
c.Assert(ToStringSlicePreserveString("Hugo"), qt.DeepEquals, []string{"Hugo"})
|
||||
c.Assert(ToStringSlicePreserveString(qt.Commentf("Hugo")), qt.DeepEquals, []string{"Hugo"})
|
||||
c.Assert(ToStringSlicePreserveString([]interface{}{"A", "B"}), qt.DeepEquals, []string{"A", "B"})
|
||||
c.Assert(ToStringSlicePreserveString([]any{"A", "B"}), qt.DeepEquals, []string{"A", "B"})
|
||||
c.Assert(ToStringSlicePreserveString([]int{1, 3}), qt.DeepEquals, []string{"1", "3"})
|
||||
c.Assert(ToStringSlicePreserveString(nil), qt.IsNil)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2022 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,27 +11,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
package hstring
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
type RenderedString string
|
||||
|
||||
var _ cmder = (*checkCmd)(nil)
|
||||
|
||||
type checkCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newCheckCmd() *checkCmd {
|
||||
cc := &checkCmd{baseCmd: &baseCmd{
|
||||
cmd: &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Contains some verification checks",
|
||||
},
|
||||
}}
|
||||
|
||||
cc.cmd.AddCommand(newLimitCmd().getCommand())
|
||||
|
||||
return cc
|
||||
func (s RenderedString) String() string {
|
||||
return string(s)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2022 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,21 +11,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hstring
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// GetString tries to get a value with key from map m and convert it to a string.
|
||||
// It will return an empty string if not found or if it cannot be convertd to a string.
|
||||
func GetString(m map[string]interface{}, key string) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
v, found := m[key]
|
||||
if !found {
|
||||
return ""
|
||||
}
|
||||
return cast.ToString(v)
|
||||
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"))
|
||||
}
|
||||
@@ -29,8 +29,8 @@ type RLocker interface {
|
||||
|
||||
// KeyValue is a interface{} tuple.
|
||||
type KeyValue struct {
|
||||
Key interface{}
|
||||
Value interface{}
|
||||
Key any
|
||||
Value any
|
||||
}
|
||||
|
||||
// KeyValueStr is a string tuple.
|
||||
@@ -41,8 +41,8 @@ type KeyValueStr struct {
|
||||
|
||||
// KeyValues holds an key and a slice of values.
|
||||
type KeyValues struct {
|
||||
Key interface{}
|
||||
Values []interface{}
|
||||
Key any
|
||||
Values []any
|
||||
}
|
||||
|
||||
// KeyString returns the key as a string, an empty string if conversion fails.
|
||||
@@ -57,7 +57,7 @@ func (k KeyValues) String() string {
|
||||
// NewKeyValuesStrings takes a given key and slice of values and returns a new
|
||||
// KeyValues struct.
|
||||
func NewKeyValuesStrings(key string, values ...string) KeyValues {
|
||||
iv := make([]interface{}, len(values))
|
||||
iv := make([]any, len(values))
|
||||
for i := 0; i < len(values); i++ {
|
||||
iv[i] = values[i]
|
||||
}
|
||||
@@ -71,7 +71,7 @@ type Zeroer interface {
|
||||
}
|
||||
|
||||
// IsNil reports whether v is nil.
|
||||
func IsNil(v interface{}) bool {
|
||||
func IsNil(v any) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@ func TestKeyValues(t *testing.T) {
|
||||
kv := NewKeyValuesStrings("key", "a1", "a2")
|
||||
|
||||
c.Assert(kv.KeyString(), qt.Equals, "key")
|
||||
c.Assert(kv.Values, qt.DeepEquals, []interface{}{"a1", "a2"})
|
||||
c.Assert(kv.Values, qt.DeepEquals, []any{"a1", "a2"})
|
||||
}
|
||||
|
||||
+2
-2
@@ -17,6 +17,6 @@ package urls
|
||||
// args must contain a path, but can also point to the target
|
||||
// language or output format.
|
||||
type RefLinker interface {
|
||||
Ref(args map[string]interface{}) (string, error)
|
||||
RelRef(args map[string]interface{}) (string, error)
|
||||
Ref(args map[string]any) (string, error)
|
||||
RelRef(args map[string]any) (string, error)
|
||||
}
|
||||
|
||||
+3
-3
@@ -17,13 +17,13 @@ package compare
|
||||
// The semantics of equals is that the two value are interchangeable
|
||||
// in the Hugo templates.
|
||||
type Eqer interface {
|
||||
Eq(other interface{}) bool
|
||||
Eq(other any) bool
|
||||
}
|
||||
|
||||
// ProbablyEqer is an equal check that may return false positives, but never
|
||||
// a false negative.
|
||||
type ProbablyEqer interface {
|
||||
ProbablyEq(other interface{}) bool
|
||||
ProbablyEq(other any) bool
|
||||
}
|
||||
|
||||
// Comparer can be used to compare two values.
|
||||
@@ -31,5 +31,5 @@ type ProbablyEqer interface {
|
||||
// Compare returns -1 if the given version is less than, 0 if equal and 1 if greater than
|
||||
// the running version.
|
||||
type Comparer interface {
|
||||
Compare(other interface{}) int
|
||||
Compare(other any) int
|
||||
}
|
||||
|
||||
@@ -62,3 +62,22 @@ func TestLexicographicSort(t *testing.T) {
|
||||
|
||||
c.Assert(s, qt.DeepEquals, []string{"A", "b", "Ba", "ba", "ba", "Bz"})
|
||||
}
|
||||
|
||||
func BenchmarkStringSort(b *testing.B) {
|
||||
prototype := []string{"b", "Bz", "zz", "ba", "αβδ αβδ αβδ", "A", "Ba", "ba", "nnnnasdfnnn", "AAgæåz", "αβδC"}
|
||||
b.Run("LessStrings", func(b *testing.B) {
|
||||
ss := make([][]string, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
ss[i] = make([]string, len(prototype))
|
||||
copy(ss[i], prototype)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sss := ss[i]
|
||||
sort.Slice(sss, func(i, j int) bool {
|
||||
return LessStrings(sss[i], sss[j])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ type Sitemap struct {
|
||||
Filename string
|
||||
}
|
||||
|
||||
func DecodeSitemap(prototype Sitemap, input map[string]interface{}) Sitemap {
|
||||
func DecodeSitemap(prototype Sitemap, input map[string]any) Sitemap {
|
||||
for key, value := range input {
|
||||
switch key {
|
||||
case "changefreq":
|
||||
@@ -177,7 +177,7 @@ func (s *Server) MatchRedirect(pattern string) Redirect {
|
||||
|
||||
type Headers struct {
|
||||
For string
|
||||
Values map[string]interface{}
|
||||
Values map[string]any
|
||||
}
|
||||
|
||||
type Redirect struct {
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestBuild(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
v := New()
|
||||
v.Set("build", map[string]interface{}{
|
||||
v.Set("build", map[string]any{
|
||||
"useResourceCacheWhen": "always",
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestBuild(t *testing.T) {
|
||||
|
||||
c.Assert(b.UseResourceCacheWhen, qt.Equals, "always")
|
||||
|
||||
v.Set("build", map[string]interface{}{
|
||||
v.Set("build", map[string]any{
|
||||
"useResourceCacheWhen": "foo",
|
||||
})
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ func (c *compositeConfig) GetInt(key string) int {
|
||||
return c.base.GetInt(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) Merge(key string, value interface{}) {
|
||||
func (c *compositeConfig) Merge(key string, value any) {
|
||||
c.layer.Merge(key, value)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (c *compositeConfig) GetParams(key string) maps.Params {
|
||||
return c.base.GetParams(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetStringMap(key string) map[string]interface{} {
|
||||
func (c *compositeConfig) GetStringMap(key string) map[string]any {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetStringMap(key)
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func (c *compositeConfig) GetStringSlice(key string) []string {
|
||||
return c.base.GetStringSlice(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) Get(key string) interface{} {
|
||||
func (c *compositeConfig) Get(key string) any {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.Get(key)
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func (c *compositeConfig) GetString(key string) string {
|
||||
return c.base.GetString(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) Set(key string, value interface{}) {
|
||||
func (c *compositeConfig) Set(key string, value any) {
|
||||
c.layer.Set(key, value)
|
||||
}
|
||||
|
||||
|
||||
@@ -67,11 +67,11 @@ func FromFile(fs afero.Fs, filename string) (Provider, error) {
|
||||
|
||||
// FromFileToMap is the same as FromFile, but it returns the config values
|
||||
// as a simple map.
|
||||
func FromFileToMap(fs afero.Fs, filename string) (map[string]interface{}, error) {
|
||||
func FromFileToMap(fs afero.Fs, filename string) (map[string]any, error) {
|
||||
return loadConfigFromFile(fs, filename)
|
||||
}
|
||||
|
||||
func readConfig(format metadecoders.Format, data []byte) (map[string]interface{}, error) {
|
||||
func readConfig(format metadecoders.Format, data []byte) (map[string]any, error) {
|
||||
m, err := metadecoders.Default.UnmarshalToMap(data, format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -82,7 +82,7 @@ func readConfig(format metadecoders.Format, data []byte) (map[string]interface{}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func loadConfigFromFile(fs afero.Fs, filename string) (map[string]interface{}, error) {
|
||||
func loadConfigFromFile(fs afero.Fs, filename string) (map[string]any, error) {
|
||||
m, err := metadecoders.Default.UnmarshalFileToMap(fs, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -156,13 +156,13 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid
|
||||
|
||||
root := item
|
||||
if len(keyPath) > 0 {
|
||||
root = make(map[string]interface{})
|
||||
root = make(map[string]any)
|
||||
m := root
|
||||
for i, key := range keyPath {
|
||||
if i >= len(keyPath)-1 {
|
||||
m[key] = item
|
||||
} else {
|
||||
nm := make(map[string]interface{})
|
||||
nm := make(map[string]any)
|
||||
m[key] = nm
|
||||
m = nm
|
||||
}
|
||||
@@ -203,6 +203,6 @@ func init() {
|
||||
|
||||
// RenameKeys renames config keys in m recursively according to a global Hugo
|
||||
// alias definition.
|
||||
func RenameKeys(m map[string]interface{}) {
|
||||
func RenameKeys(m map[string]any) {
|
||||
keyAliases.Rename(m)
|
||||
}
|
||||
|
||||
+23
-13
@@ -24,12 +24,12 @@ type Provider interface {
|
||||
GetInt(key string) int
|
||||
GetBool(key string) bool
|
||||
GetParams(key string) maps.Params
|
||||
GetStringMap(key string) map[string]interface{}
|
||||
GetStringMap(key string) map[string]any
|
||||
GetStringMapString(key string) map[string]string
|
||||
GetStringSlice(key string) []string
|
||||
Get(key string) interface{}
|
||||
Set(key string, value interface{})
|
||||
Merge(key string, value interface{})
|
||||
Get(key string) any
|
||||
Set(key string, value any)
|
||||
Merge(key string, value any)
|
||||
SetDefaults(params maps.Params)
|
||||
SetDefaultMergeStrategy()
|
||||
WalkParams(walkFn func(params ...KeyParams) bool)
|
||||
@@ -45,13 +45,23 @@ func GetStringSlicePreserveString(cfg Provider, key string) []string {
|
||||
}
|
||||
|
||||
// SetBaseTestDefaults provides some common config defaults used in tests.
|
||||
func SetBaseTestDefaults(cfg Provider) {
|
||||
cfg.Set("resourceDir", "resources")
|
||||
cfg.Set("contentDir", "content")
|
||||
cfg.Set("dataDir", "data")
|
||||
cfg.Set("i18nDir", "i18n")
|
||||
cfg.Set("layoutDir", "layouts")
|
||||
cfg.Set("assetDir", "assets")
|
||||
cfg.Set("archetypeDir", "archetypes")
|
||||
cfg.Set("publishDir", "public")
|
||||
func SetBaseTestDefaults(cfg Provider) Provider {
|
||||
setIfNotSet(cfg, "baseURL", "https://example.org")
|
||||
setIfNotSet(cfg, "resourceDir", "resources")
|
||||
setIfNotSet(cfg, "contentDir", "content")
|
||||
setIfNotSet(cfg, "dataDir", "data")
|
||||
setIfNotSet(cfg, "i18nDir", "i18n")
|
||||
setIfNotSet(cfg, "layoutDir", "layouts")
|
||||
setIfNotSet(cfg, "assetDir", "assets")
|
||||
setIfNotSet(cfg, "archetypeDir", "archetypes")
|
||||
setIfNotSet(cfg, "publishDir", "public")
|
||||
setIfNotSet(cfg, "workingDir", "")
|
||||
setIfNotSet(cfg, "defaultContentLanguage", "en")
|
||||
return cfg
|
||||
}
|
||||
|
||||
func setIfNotSet(cfg Provider, key string, value any) {
|
||||
if !cfg.IsSet(key) {
|
||||
cfg.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ var (
|
||||
"permalinks": true,
|
||||
"related": true,
|
||||
"sitemap": true,
|
||||
"privacy": true,
|
||||
"security": true,
|
||||
"taxonomies": true,
|
||||
}
|
||||
|
||||
@@ -73,6 +75,11 @@ func NewFrom(params maps.Params) Provider {
|
||||
}
|
||||
}
|
||||
|
||||
// NewWithTestDefaults is used in tests only.
|
||||
func NewWithTestDefaults() Provider {
|
||||
return SetBaseTestDefaults(New())
|
||||
}
|
||||
|
||||
// defaultConfigProvider is a Provider backed by a map where all keys are lower case.
|
||||
// All methods are thread safe.
|
||||
type defaultConfigProvider struct {
|
||||
@@ -82,7 +89,7 @@ type defaultConfigProvider struct {
|
||||
keyCache sync.Map
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) Get(k string) interface{} {
|
||||
func (c *defaultConfigProvider) Get(k string) any {
|
||||
if k == "" {
|
||||
return c.root
|
||||
}
|
||||
@@ -131,7 +138,7 @@ func (c *defaultConfigProvider) GetParams(k string) maps.Params {
|
||||
return v.(maps.Params)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringMap(k string) map[string]interface{} {
|
||||
func (c *defaultConfigProvider) GetStringMap(k string) map[string]any {
|
||||
v := c.Get(k)
|
||||
return maps.ToStringMap(v)
|
||||
}
|
||||
@@ -146,7 +153,7 @@ func (c *defaultConfigProvider) GetStringSlice(k string) []string {
|
||||
return cast.ToStringSlice(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) Set(k string, v interface{}) {
|
||||
func (c *defaultConfigProvider) Set(k string, v any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -164,7 +171,7 @@ func (c *defaultConfigProvider) Set(k string, v interface{}) {
|
||||
}
|
||||
|
||||
switch vv := v.(type) {
|
||||
case map[string]interface{}, map[interface{}]interface{}, map[string]string:
|
||||
case map[string]any, map[any]any, map[string]string:
|
||||
p := maps.MustToParamsAndPrepare(vv)
|
||||
v = p
|
||||
}
|
||||
@@ -196,7 +203,7 @@ func (c *defaultConfigProvider) SetDefaults(params maps.Params) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) Merge(k string, v interface{}) {
|
||||
func (c *defaultConfigProvider) Merge(k string, v any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
k = strings.ToLower(k)
|
||||
@@ -287,7 +294,7 @@ func (c *defaultConfigProvider) Merge(k string, v interface{}) {
|
||||
}
|
||||
|
||||
switch vv := v.(type) {
|
||||
case map[string]interface{}, map[interface{}]interface{}, map[string]string:
|
||||
case map[string]any, map[any]any, map[string]string:
|
||||
p := maps.MustToParamsAndPrepare(vv)
|
||||
v = p
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/gohugoio/hugo/common/para"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
@@ -36,7 +34,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
c.Run("Set and get", func(c *qt.C) {
|
||||
cfg := New()
|
||||
var k string
|
||||
var v interface{}
|
||||
var v any
|
||||
|
||||
k, v = "foo", "bar"
|
||||
cfg.Set(k, v)
|
||||
@@ -57,7 +55,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
c.Run("Set and get map", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("foo", map[string]interface{}{
|
||||
cfg.Set("foo", map[string]any{
|
||||
"bar": "baz",
|
||||
})
|
||||
|
||||
@@ -65,14 +63,14 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"bar": "baz",
|
||||
})
|
||||
|
||||
c.Assert(cfg.GetStringMap("foo"), qt.DeepEquals, map[string]interface{}{"bar": string("baz")})
|
||||
c.Assert(cfg.GetStringMap("foo"), qt.DeepEquals, map[string]any{"bar": string("baz")})
|
||||
c.Assert(cfg.GetStringMapString("foo"), qt.DeepEquals, map[string]string{"bar": string("baz")})
|
||||
})
|
||||
|
||||
c.Run("Set and get nested", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
cfg.Set("a", map[string]any{
|
||||
"B": "bv",
|
||||
})
|
||||
cfg.Set("a.c", "cv")
|
||||
@@ -88,7 +86,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"a": "av",
|
||||
})
|
||||
|
||||
cfg.Set("b", map[string]interface{}{
|
||||
cfg.Set("b", map[string]any{
|
||||
"b": "bv",
|
||||
})
|
||||
|
||||
@@ -101,7 +99,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
|
||||
cfg.Set("a", "av")
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
cfg.Set("", map[string]any{
|
||||
"a": "av2",
|
||||
"b": "bv2",
|
||||
})
|
||||
@@ -115,7 +113,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
|
||||
cfg.Set("a", "av")
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
cfg.Set("", map[string]any{
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
@@ -126,14 +124,14 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
|
||||
cfg = New()
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
cfg.Set("", map[string]any{
|
||||
"foo": map[string]any{
|
||||
"a": "av",
|
||||
},
|
||||
})
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
cfg.Set("", map[string]any{
|
||||
"foo": map[string]any{
|
||||
"b": "bv2",
|
||||
},
|
||||
})
|
||||
@@ -147,11 +145,11 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
c.Run("Merge default strategy", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
cfg.Set("a", map[string]any{
|
||||
"B": "bv",
|
||||
})
|
||||
|
||||
cfg.Merge("a", map[string]interface{}{
|
||||
cfg.Merge("a", map[string]any{
|
||||
"B": "bv2",
|
||||
"c": "cv2",
|
||||
})
|
||||
@@ -165,7 +163,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
|
||||
cfg.Set("a", "av")
|
||||
|
||||
cfg.Merge("", map[string]interface{}{
|
||||
cfg.Merge("", map[string]any{
|
||||
"a": "av2",
|
||||
"b": "bv2",
|
||||
})
|
||||
@@ -178,16 +176,16 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
c.Run("Merge shallow", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
cfg.Set("a", map[string]any{
|
||||
"_merge": "shallow",
|
||||
"B": "bv",
|
||||
"c": map[string]interface{}{
|
||||
"c": map[string]any{
|
||||
"b": "bv",
|
||||
},
|
||||
})
|
||||
|
||||
cfg.Merge("a", map[string]interface{}{
|
||||
"c": map[string]interface{}{
|
||||
cfg.Merge("a", map[string]any{
|
||||
"c": map[string]any{
|
||||
"d": "dv2",
|
||||
},
|
||||
"e": "ev2",
|
||||
@@ -205,21 +203,20 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
|
||||
// Issue #8679
|
||||
c.Run("Merge typed maps", func(c *qt.C) {
|
||||
|
||||
for _, left := range []interface{}{
|
||||
for _, left := range []any{
|
||||
map[string]string{
|
||||
"c": "cv1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"c": "cv1",
|
||||
},
|
||||
map[interface{}]interface{}{
|
||||
map[any]any{
|
||||
"c": "cv1",
|
||||
},
|
||||
} {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
cfg.Set("", map[string]any{
|
||||
"b": left,
|
||||
})
|
||||
|
||||
@@ -238,28 +235,27 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
for _, left := range []interface{}{
|
||||
for _, left := range []any{
|
||||
map[string]string{
|
||||
"b": "bv1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"b": "bv1",
|
||||
},
|
||||
map[interface{}]interface{}{
|
||||
map[any]any{
|
||||
"b": "bv1",
|
||||
},
|
||||
} {
|
||||
|
||||
for _, right := range []interface{}{
|
||||
for _, right := range []any{
|
||||
map[string]string{
|
||||
"b": "bv2",
|
||||
"c": "cv2",
|
||||
},
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"b": "bv2",
|
||||
"c": "cv2",
|
||||
},
|
||||
map[interface{}]interface{}{
|
||||
map[any]any{
|
||||
"b": "bv2",
|
||||
"c": "cv2",
|
||||
},
|
||||
@@ -277,21 +273,19 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// Issue #8701
|
||||
c.Run("Prevent _merge only maps", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
cfg.Set("", map[string]any{
|
||||
"B": "bv",
|
||||
})
|
||||
|
||||
cfg.Merge("", map[string]interface{}{
|
||||
"c": map[string]interface{}{
|
||||
cfg.Merge("", map[string]any{
|
||||
"c": map[string]any{
|
||||
"_merge": "shallow",
|
||||
"d": "dv2",
|
||||
},
|
||||
@@ -305,7 +299,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
c.Run("IsSet", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
cfg.Set("a", map[string]any{
|
||||
"B": "bv",
|
||||
})
|
||||
|
||||
@@ -363,15 +357,15 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
|
||||
func BenchmarkDefaultConfigProvider(b *testing.B) {
|
||||
type cfger interface {
|
||||
Get(key string) interface{}
|
||||
Set(key string, value interface{})
|
||||
Get(key string) any
|
||||
Set(key string, value any)
|
||||
IsSet(key string) bool
|
||||
}
|
||||
|
||||
newMap := func() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
"b": map[string]interface{}{
|
||||
newMap := func() map[string]any {
|
||||
return map[string]any{
|
||||
"a": map[string]any{
|
||||
"b": map[string]any{
|
||||
"c": 32,
|
||||
"d": 43,
|
||||
},
|
||||
@@ -397,13 +391,6 @@ func BenchmarkDefaultConfigProvider(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
b.Run("Viper", func(b *testing.B) {
|
||||
v := viper.New()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runMethods(b, v)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Custom", func(b *testing.B) {
|
||||
cfg := New()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -35,7 +35,7 @@ func init() {
|
||||
cfg.Set("languages", lang)
|
||||
cfg.SetDefaultMergeStrategy()
|
||||
|
||||
configHelpers := map[string]interface{}{
|
||||
configHelpers := map[string]any{
|
||||
"mergeStrategy": cfg.Get(""),
|
||||
}
|
||||
return docshelper.DocProvider{"config": configHelpers}
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@ func SetEnvVars(oldVars *[]string, keyValues ...string) {
|
||||
}
|
||||
|
||||
func SplitEnvVar(v string) (string, string) {
|
||||
parts := strings.SplitN(v, "=", 2)
|
||||
return parts[0], parts[1]
|
||||
name, value, _ := strings.Cut(v, "=")
|
||||
return name, value
|
||||
}
|
||||
|
||||
func setEnvVar(vars *[]string, key, value string) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2021 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,25 +11,16 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !darwin
|
||||
|
||||
package commands
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/gohugoio/hugo/docshelper"
|
||||
)
|
||||
|
||||
var _ cmder = (*checkCmd)(nil)
|
||||
func init() {
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
|
||||
type checkCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newCheckCmd() *checkCmd {
|
||||
return &checkCmd{baseCmd: &baseCmd{
|
||||
cmd: &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Contains some verification checks",
|
||||
},
|
||||
}}
|
||||
return docshelper.DocProvider{"config": DefaultConfig.ToSecurityMap()}
|
||||
}
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// 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 security
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const securityConfigKey = "security"
|
||||
|
||||
// DefaultConfig holds the default security policy.
|
||||
var DefaultConfig = Config{
|
||||
Exec: Exec{
|
||||
Allow: NewWhitelist(
|
||||
"^dart-sass-embedded$",
|
||||
"^go$", // for Go Modules
|
||||
"^npx$", // used by all Node tools (Babel, PostCSS).
|
||||
"^postcss$",
|
||||
),
|
||||
// These have been tested to work with Hugo's external programs
|
||||
// on Windows, Linux and MacOS.
|
||||
OsEnv: NewWhitelist("(?i)^(PATH|PATHEXT|APPDATA|TMP|TEMP|TERM)$"),
|
||||
},
|
||||
Funcs: Funcs{
|
||||
Getenv: NewWhitelist("^HUGO_"),
|
||||
},
|
||||
HTTP: HTTP{
|
||||
URLs: NewWhitelist(".*"),
|
||||
Methods: NewWhitelist("(?i)GET|POST"),
|
||||
},
|
||||
}
|
||||
|
||||
// Config is the top level security config.
|
||||
type Config struct {
|
||||
// Restricts access to os.Exec.
|
||||
Exec Exec `json:"exec"`
|
||||
|
||||
// Restricts access to certain template funcs.
|
||||
Funcs Funcs `json:"funcs"`
|
||||
|
||||
// Restricts access to resources.Get, getJSON, getCSV.
|
||||
HTTP HTTP `json:"http"`
|
||||
|
||||
// Allow inline shortcodes
|
||||
EnableInlineShortcodes bool `json:"enableInlineShortcodes"`
|
||||
}
|
||||
|
||||
// Exec holds os/exec policies.
|
||||
type Exec struct {
|
||||
Allow Whitelist `json:"allow"`
|
||||
OsEnv Whitelist `json:"osEnv"`
|
||||
}
|
||||
|
||||
// Funcs holds template funcs policies.
|
||||
type Funcs struct {
|
||||
// OS env keys allowed to query in os.Getenv.
|
||||
Getenv Whitelist `json:"getenv"`
|
||||
}
|
||||
|
||||
type HTTP struct {
|
||||
// URLs to allow in remote HTTP (resources.Get, getJSON, getCSV).
|
||||
URLs Whitelist `json:"urls"`
|
||||
|
||||
// HTTP methods to allow.
|
||||
Methods Whitelist `json:"methods"`
|
||||
}
|
||||
|
||||
// ToTOML converts c to TOML with [security] as the root.
|
||||
func (c Config) ToTOML() string {
|
||||
sec := c.ToSecurityMap()
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
if err := parser.InterfaceToConfig(sec, metadecoders.TOML, &b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
func (c Config) CheckAllowedExec(name string) error {
|
||||
if !c.Exec.Allow.Accept(name) {
|
||||
return &AccessDeniedError{
|
||||
name: name,
|
||||
path: "security.exec.allow",
|
||||
policies: c.ToTOML(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (c Config) CheckAllowedGetEnv(name string) error {
|
||||
if !c.Funcs.Getenv.Accept(name) {
|
||||
return &AccessDeniedError{
|
||||
name: name,
|
||||
path: "security.funcs.getenv",
|
||||
policies: c.ToTOML(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) CheckAllowedHTTPURL(url string) error {
|
||||
if !c.HTTP.URLs.Accept(url) {
|
||||
return &AccessDeniedError{
|
||||
name: url,
|
||||
path: "security.http.urls",
|
||||
policies: c.ToTOML(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) CheckAllowedHTTPMethod(method string) error {
|
||||
if !c.HTTP.Methods.Accept(method) {
|
||||
return &AccessDeniedError{
|
||||
name: method,
|
||||
path: "security.http.method",
|
||||
policies: c.ToTOML(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToSecurityMap converts c to a map with 'security' as the root key.
|
||||
func (c Config) ToSecurityMap() map[string]any {
|
||||
// Take it to JSON and back to get proper casing etc.
|
||||
asJson, err := json.Marshal(c)
|
||||
herrors.Must(err)
|
||||
m := make(map[string]any)
|
||||
herrors.Must(json.Unmarshal(asJson, &m))
|
||||
|
||||
// Add the root
|
||||
sec := map[string]any{
|
||||
"security": m,
|
||||
}
|
||||
return sec
|
||||
|
||||
}
|
||||
|
||||
// DecodeConfig creates a privacy Config from a given Hugo configuration.
|
||||
func DecodeConfig(cfg config.Provider) (Config, error) {
|
||||
sc := DefaultConfig
|
||||
if cfg.IsSet(securityConfigKey) {
|
||||
m := cfg.GetStringMap(securityConfigKey)
|
||||
dec, err := mapstructure.NewDecoder(
|
||||
&mapstructure.DecoderConfig{
|
||||
WeaklyTypedInput: true,
|
||||
Result: &sc,
|
||||
DecodeHook: stringSliceToWhitelistHook(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return sc, err
|
||||
}
|
||||
|
||||
if err = dec.Decode(m); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
}
|
||||
|
||||
if !sc.EnableInlineShortcodes {
|
||||
// Legacy
|
||||
sc.EnableInlineShortcodes = cfg.GetBool("enableInlineShortcodes")
|
||||
}
|
||||
|
||||
return sc, nil
|
||||
|
||||
}
|
||||
|
||||
func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data any) (any, error) {
|
||||
|
||||
if t != reflect.TypeOf(Whitelist{}) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
wl := types.ToStringSlicePreserveString(data)
|
||||
|
||||
return NewWhitelist(wl...), nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// AccessDeniedError represents a security policy conflict.
|
||||
type AccessDeniedError struct {
|
||||
path string
|
||||
name string
|
||||
policies string
|
||||
}
|
||||
|
||||
func (e *AccessDeniedError) Error() string {
|
||||
return fmt.Sprintf("access denied: %q is not whitelisted in policy %q; the current security configuration is:\n\n%s\n\n", e.name, e.path, e.policies)
|
||||
}
|
||||
|
||||
// IsAccessDenied reports whether err is an AccessDeniedError
|
||||
func IsAccessDenied(err error) bool {
|
||||
var notFoundErr *AccessDeniedError
|
||||
return errors.As(err, ¬FoundErr)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user