Compare commits

..

No commits in common. "master" and "v0.25" have entirely different histories.

3067 changed files with 74230 additions and 267627 deletions

View file

@ -1,115 +0,0 @@
parameters:
# v2: 11m.
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22400.20000
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
jobs:
prepare_release:
<<: *defaults
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
steps:
- setup_remote_docker
- checkout:
path: hugo
- &git-config
run:
command: |
git config --global user.email "bjorn.erik.pedersen+hugoreleaser@gmail.com"
git config --global user.name "hugoreleaser"
- run:
command: |
cd hugo
go mod download
go run -tags release main.go release --step 1
- save_cache:
key: git-sha-{{ .Revision }}
paths:
- hugo
- gomodcache
build_container1:
<<: [*defaults]
environment:
<<: [*buildenv]
steps:
- &restore-cache
restore_cache:
key: git-sha-{{ .Revision }}
- run:
no_output_timeout: 20m
command: |
mkdir -p /tmp/files/dist1
cd hugo
hugoreleaser build -paths "builds/container1/**" -workers 3 -dist /tmp/files/dist1 -chunks $CIRCLE_NODE_TOTAL -chunk-index $CIRCLE_NODE_INDEX
- &persist-workspace
persist_to_workspace:
root: /tmp/files
paths:
- dist1
- dist2
parallelism: 7
build_container2:
<<: [*defaults]
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000
steps:
- *restore-cache
- &attach-workspace
attach_workspace:
at: /tmp/workspace
- run:
command: |
mkdir -p /tmp/files/dist2
cd hugo
hugoreleaser build -paths "builds/container2/**" -workers 1 -dist /tmp/files/dist2
- *persist-workspace
archive_and_release:
<<: [*defaults]
environment:
<<: [*buildenv]
steps:
- *restore-cache
- *attach-workspace
- *git-config
- run:
name: Add github.com to known hosts
command: ssh-keyscan github.com >> ~/.ssh/known_hosts
- run:
command: |
cp -a /tmp/workspace/dist1/. ./hugo/dist
cp -a /tmp/workspace/dist2/. ./hugo/dist
- run:
command: |
cd hugo
hugoreleaser archive
hugoreleaser release
go run -tags release main.go release --step 2
workflows:
version: 2
release:
jobs:
- prepare_release:
filters:
branches:
only: /release-.*/
- build_container1:
requires:
- prepare_release
- build_container2:
requires:
- prepare_release
- archive_and_release:
context: org-global
requires:
- build_container1
- build_container2

View file

@ -1,9 +0,0 @@
*.md
*.log
*.txt
.git
.github
.circleci
docs
examples
Dockerfile

8
.gitattributes vendored
View file

@ -1,8 +0,0 @@
# Text files have auto line endings
* text=auto
# Go source files always have LF line endings
*.go text eol=lf
# SVG files should not be modified
*.svg -text

View file

@ -1,23 +0,0 @@
---
name: 'Bug report'
labels: 'Bug, NeedsTriage'
assignees: ''
about: Create a report to help us improve
---
<!--
Please do not use the issue queue for questions or troubleshooting. Unless you are certain that your issue is a software defect, use the forum:
https://discourse.gohugo.io
-->
<!-- Please answer these questions before submitting your issue. Thanks! -->
### What version of Hugo are you using (`hugo version`)?
<pre>
$ hugo version
</pre>
### Does this issue reproduce with the latest release?

View file

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: SUPPORT, ISSUES and TROUBLESHOOTING
url: https://discourse.gohugo.io/
about: Please DO NOT use Github for support requests. Please visit https://discourse.gohugo.io for support! You will be helped much faster there. If you ignore this request your issue might be closed with a discourse label.

View file

@ -1,11 +0,0 @@
---
name: Proposal
about: Propose a new feature for Hugo
title: ''
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. -->

3
.github/SUPPORT.md vendored
View file

@ -1,3 +0,0 @@
### Asking Support Questions
We have an active [discussion forum](https://discourse.gohugo.io) where users and developers can ask questions. Please don't use the GitHub issue tracker to ask questions.

View file

@ -1,7 +0,0 @@
# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#package-ecosystem
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"

View file

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

View file

@ -1,52 +0,0 @@
name: 'Close stale and lock closed issues and PRs'
on:
workflow_dispatch:
schedule:
- cron: '30 1 * * *'
permissions:
contents: read
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@7de207be1d3ce97a9abe6ff1306222982d1ca9f9 # v5.0.1
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@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
operations-per-run: 999
days-before-issue-stale: 365
days-before-pr-stale: 365
days-before-issue-close: 56
days-before-pr-close: 56
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'

View file

@ -1,132 +0,0 @@
on:
push:
branches: [master]
pull_request:
name: Test
env:
GOPROXY: https://proxy.golang.org
GO111MODULE: on
SASS_VERSION: 1.80.3
DART_SASS_SHA_LINUX: 7c933edbad0a7d389192c5b79393485c088bd2c4398e32f5754c32af006a9ffd
DART_SASS_SHA_MACOS: 79e060b0e131c3bb3c16926bafc371dc33feab122bfa8c01aa337a072097967b
DART_SASS_SHA_WINDOWS: 0bc4708b37cd1bac4740e83ac5e3176e66b774f77fd5dd364da5b5cfc9bfb469
permissions:
contents: read
jobs:
test:
strategy:
matrix:
go-version: [1.23.x, 1.24.x]
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
runs-on: ${{ matrix.os }}
steps:
- if: matrix.os == 'ubuntu-latest'
name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Install Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: ${{ matrix.go-version }}
check-latest: true
cache: true
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Install Ruby
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
with:
ruby-version: "2.7"
bundler-cache: true #
- name: Install Python
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with:
python-version: "3.x"
- name: Install Mage
run: go install github.com/magefile/mage@v1.15.0
- name: Install asciidoctor
uses: reitzig/actions-asciidoctor@c642db5eedd1d729bb8c92034770d0b2f769eda6 # v2.0.2
- name: Install docutils
run: |
pip install docutils
rst2html --version
- if: matrix.os == 'ubuntu-latest'
name: Install pandoc on Linux
run: |
sudo apt-get update -y
sudo apt-get install -y pandoc
- if: matrix.os == 'macos-latest'
run: |
brew install pandoc
- if: matrix.os == 'windows-latest'
run: |
choco install pandoc
- run: pandoc -v
- if: matrix.os == 'windows-latest'
run: |
choco install mingw
- if: matrix.os == 'ubuntu-latest'
name: Install dart-sass Linux
run: |
echo "Install Dart Sass version ${SASS_VERSION} ..."
curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-linux-x64.tar.gz";
echo "${DART_SASS_SHA_LINUX} dart-sass-${SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c;
tar -xvf "dart-sass-${SASS_VERSION}-linux-x64.tar.gz";
echo "$GOBIN"
echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH
- if: matrix.os == 'macos-latest'
name: Install dart-sass MacOS
run: |
echo "Install Dart Sass version ${SASS_VERSION} ..."
curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-macos-x64.tar.gz";
echo "${DART_SASS_SHA_MACOS} dart-sass-${SASS_VERSION}-macos-x64.tar.gz" | shasum -a 256 -c;
tar -xvf "dart-sass-${SASS_VERSION}-macos-x64.tar.gz";
echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH
- if: matrix.os == 'windows-latest'
name: Install dart-sass Windows
run: |
echo "Install Dart Sass version ${env:SASS_VERSION} ..."
curl -LJO "https://github.com/sass/dart-sass/releases/download/${env:SASS_VERSION}/dart-sass-${env:SASS_VERSION}-windows-x64.zip";
Expand-Archive -Path "dart-sass-${env:SASS_VERSION}-windows-x64.zip" -DestinationPath .;
echo "$env:GITHUB_WORKSPACE/dart-sass/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append
- if: matrix.os == 'ubuntu-latest'
name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- if: matrix.os == 'ubuntu-latest'
name: Run staticcheck
run: staticcheck ./...
- if: matrix.os != 'windows-latest'
name: Check
run: |
sass --version;
mage -v check;
env:
HUGO_BUILD_TAGS: extended,withdeploy
- if: matrix.os == 'windows-latest'
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
name: Test
run: |
mage -v test;
env:
HUGO_BUILD_TAGS: extended,withdeploy
- name: Build tags
run: |
go install -tags extended
- if: matrix.os == 'ubuntu-latest'
name: Build for dragonfly
run: |
go install
env:
GOARCH: amd64
GOOS: dragonfly

22
.gitignore vendored
View file

@ -1,6 +1,18 @@
hugo
docs/public*
/.idea
hugo.exe
*.test
*.prof
nohup.out
cover.out
*.swp
*.swo
.DS_Store
*~
vendor/*/
*.bench
coverage*.out
*.test
imports.*
dist/
public/
.DS_Store
GoBuilds
dist

6
.goxc.json Normal file
View file

@ -0,0 +1,6 @@
{
"ArtifactsDest": "GoBuilds/",
"OutPath": "{{.Dest}}{{.PS}}{{.AppName}}{{.PS}}{{.Version}}{{.PS}}{{.AppName}}_{{.Version}}_{{.Os}}_{{.Arch}}{{.Ext}}",
"BuildConstraints": "linux windows darwin freebsd netbsd openbsd dragonfly",
"ConfigVersion": "0.9"
}

23
.travis.yml Normal file
View file

@ -0,0 +1,23 @@
language: go
sudo: required
go:
- 1.7.6
- 1.8.3
- tip
os:
- linux
- osx
matrix:
allow_failures:
- go: tip
fast_finish: true
install:
- make vendor
script:
- make hugo-race check
- ./hugo -s docs/
- ./hugo --renderToMemory -s docs/
before_install:
# gem install must be run with sudo on OSX
- sudo gem install asciidoctor | gem install asciidoctor
- sudo pip install docutils

View file

@ -1,5 +1,3 @@
>**Note:** We would appreciate if you hold on with any big refactoring (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future.
# Contributing to Hugo
We welcome contributions to Hugo of any kind including documentation, themes,
@ -9,10 +7,6 @@ helping to manage issues, etc.
The Hugo community and maintainers are [very active](https://github.com/gohugoio/hugo/pulse/monthly) and helpful, and the project benefits greatly from this activity. We created a [step by step guide](https://gohugo.io/tutorials/how-to-contribute-to-hugo/) if you're unfamiliar with GitHub or contributing to open source projects in general.
*Note that this repository only contains the actual source code of Hugo. For **only** documentation-related pull requests / issues please refer to the [hugoDocs](https://github.com/gohugoio/hugoDocs) repository.*
*Changes to the codebase **and** related documentation, e.g. for a new feature, should still use a single pull request.*
## Table of Contents
* [Asking Support Questions](#asking-support-questions)
@ -20,8 +14,11 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
* [Submitting Patches](#submitting-patches)
* [Code Contribution Guidelines](#code-contribution-guidelines)
* [Git Commit Message Guidelines](#git-commit-message-guidelines)
* [Vendored Dependencies](#vendored-dependencies)
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
* [Building Hugo with Your Changes](#building-hugo-with-your-changes)
* [Using Git Remotes](#using-git-remotes)
* [Build Hugo with Your Changes](#build-hugo-with-your-changes)
* [Updating the Hugo Sources](#updating-the-hugo-sources)
## Asking Support Questions
@ -31,42 +28,22 @@ Please don't use the GitHub issue tracker to ask questions.
## Reporting Issues
If you believe you have found a defect in Hugo or its documentation, use
the GitHub issue tracker to report
the problem to the Hugo maintainers. If you're not sure if it's a bug or not,
start by asking in the [discussion forum](https://discourse.gohugo.io).
When reporting the issue, please provide the version of Hugo in use (`hugo
version`) and your operating system.
- [Hugo Issues · gohugoio/hugo](https://github.com/gohugoio/hugo/issues)
- [Hugo Documentation Issues · gohugoio/hugoDocs](https://github.com/gohugoio/hugoDocs/issues)
- [Hugo Website Theme Issues · gohugoio/hugoThemesSite](https://github.com/gohugoio/hugoThemesSite/issues)
## Code Contribution
Hugo has become a fully featured static site generator, so any new functionality must:
* be useful to many.
* fit naturally into _what Hugo does best._
* strive not to break existing sites.
* close or update an open [Hugo issue](https://github.com/gohugoio/hugo/issues)
If it is of some complexity, the contributor is expected to maintain and support the new feature in the future (answer questions on the forum, fix any bugs etc.).
Any non-trivial code change needs to update an open [issue](https://github.com/gohugoio/hugo/issues). A non-trivial code change without an issue reference with one of the labels `bug` or `enhancement` will not be merged.
Note that we do not accept new features that require [CGO](https://github.com/golang/go/wiki/cgo).
We have one exception to this rule which is LibSASS.
**Bug fixes are, of course, always welcome.**
the GitHub [issue tracker](https://github.com/gohugoio/hugo/issues) to report the problem to the Hugo maintainers.
If you're not sure if it's a bug or not, start by asking in the [discussion forum](https://discourse.gohugo.io).
When reporting the issue, please provide the version of Hugo in use (`hugo version`) and your operating system.
## Submitting Patches
The Hugo project welcomes all contributors and contributions regardless of skill or experience level. If you are interested in helping with the project, we will help you with your contribution.
The Hugo project welcomes all contributors and contributions regardless of skill or experience level.
If you are interested in helping with the project, we will help you with your contribution.
Hugo is a very active project with many contributions happening daily.
Because we want to create the best possible product for our users and the best contribution experience for our developers,
we have a set of guidelines which ensure that all contributions are acceptable.
The guidelines are not intended as a filter or barrier to participation.
If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines.
### Code Contribution Guidelines
Because we want to create the best possible product for our users and the best contribution experience for our developers, we have a set of guidelines which ensure that all contributions are acceptable. The guidelines are not intended as a filter or barrier to participation. If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines.
To make the contribution process as seamless as possible, we ask for the following:
* Go ahead and fork the project and make your changes. We encourage pull requests to allow for review and discussion of code changes.
@ -76,28 +53,24 @@ To make the contribution process as seamless as possible, we ask for the followi
* Run `go fmt`.
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
* Squash your commits into a single commit. `git rebase -i`. Its okay to force update your pull request with `git push -f`.
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
* Ensure that `make check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Linux and macOS) and [AppVeyor](https://ci.appveyor.com/project/gohugoio/hugo/branch/master) (Windows) will fail the build if `make check` fails.
* Follow the **Git Commit Message Guidelines** below.
### Git Commit Message Guidelines
This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages,
This [blog article](http://chris.beams.io/posts/git-commit/) is a good resource for learning how to write good commit messages,
the most important part being that each commit message should have a title/subject in imperative mood starting with a capital letter and no trailing period:
*"js: Return error when option x is not set"*, **NOT** *"returning some error."*
Most title/subjects should have a lower-cased prefix with a colon and one whitespace. The prefix can be:
* The name of the package where (most of) the changes are made (e.g. `media: Add text/calendar`)
* If the package name is deeply nested/long, try to shorten it from the left side, e.g. `markup/goldmark` is OK, `resources/resource_transformers/js` can be shortened to `js`.
* If this commit touches several packages with a common functional topic, use that as a prefix, e.g. `errors: Resolve correct line numbers`)
* If this commit touches many packages without a common functional topic, prefix with `all:` (e.g. `all: Reformat Go code`)
* If this is a documentation update, prefix with `docs:`.
* If nothing of the above applies, just leave the prefix out.
* Note that the above excludes nouns seen in other repositories, e.g. "chore:".
*"Return error on wrong use of the Paginator"*, **NOT** *"returning some error."*
Also, if your commit references one or more GitHub issues, always end your commit message body with *See #1234* or *Fixes #1234*.
Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*.
Sometimes it makes sense to prefix the commit message with the packagename (or docs folder) all lowercased ending with a colon.
That is fine, but the rest of the rules above apply.
So it is "tpl: Add emojify template func", not "tpl: add emojify template func.", and "docs: Document emoji", not "doc: document emoji."
Please consider to use a short and descriptive branch name, e.g. **NOT** "patch-1". It's very common but creates a naming conflict each time when a submission is pulled for a review.
An example:
```text
@ -111,35 +84,42 @@ new default function more useful for Hugo users.
Fixes #1949
```
### Fetching the Sources From GitHub
### Vendored Dependencies
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:
Hugo uses [govendor](https://github.com/kardianos/govendor) to vendor dependencies, but we don't commit the vendored packages themselves to the Hugo git repository.
Therefore, a simple `go get` is not supported since `go get` is not vendor-aware.
You **must use govendor** to fetch and manage Hugo's dependencies.
```bash
mkdir $HOME/src
cd $HOME/src
git clone https://github.com/gohugoio/hugo.git
cd hugo
go install
### Fetch the Sources From GitHub
```
go get github.com/kardianos/govendor
govendor get github.com/gohugoio/hugo
```
For some convenient build and test targets, you also will want to install Mage:
### Using Git Remotes
```bash
go install github.com/magefile/mage
```
Due to the way Go handles package imports, the best approach for working on a
Hugo fork is to use Git Remotes. Here's a simple walk-through for getting
started:
Now, to make a change to Hugo's source:
1. Fetch the Hugo sources as described above.
1. Change to the Hugo source directory:
```
cd $HOME/go/src/github.com/gohugoio/hugo
```
1. Create a new branch for your changes (the branch name is arbitrary):
```bash
```
git checkout -b iss1234
```
1. After making your changes, commit them to your new branch:
```bash
```
git commit -a -v
```
@ -147,53 +127,35 @@ Now, to make a change to Hugo's source:
1. Add your fork as a new remote (the remote name, "fork" in this example, is arbitrary):
```bash
git remote add fork git@github.com:USERNAME/hugo.git
```
git remote add fork git://github.com/USERNAME/hugo.git
```
1. Push the changes to your new remote:
```bash
```
git push --set-upstream fork iss1234
```
1. You're now ready to submit a PR based upon the new branch in your forked repository.
### Building Hugo with Your Changes
Hugo uses [mage](https://github.com/magefile/mage) to sync vendor dependencies, build Hugo, run the test suite and other things. You must run mage from the Hugo directory.
### Build Hugo with Your Changes
```bash
cd $HOME/go/src/github.com/gohugoio/hugo
make hugo
# or to install in $HOME/go/bin:
make install
```
To build Hugo:
### Updating the Hugo Sources
```bash
mage hugo
If you want to stay in sync with the Hugo repository, you can easily pull down
the source changes, but you'll need to keep the vendored packages up-to-date as
well.
```
git pull
make vendor
```
To install hugo in `$HOME/go/bin`:
```bash
mage install
```
To run the tests:
```bash
mage hugoRace
mage -v check
```
To list all available commands along with descriptions:
```bash
mage -l
```
**Note:** From Hugo 0.43 we have added a build tag, `extended` that adds **SCSS support**. This needs a C compiler installed to build. You can enable this when building by:
```bash
HUGO_BUILD_TAGS=extended mage install
````

110
Dockerfile Executable file → Normal file
View file

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

201
LICENSE
View file

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

194
LICENSE.md Normal file
View file

@ -0,0 +1,194 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

83
Makefile Normal file
View file

@ -0,0 +1,83 @@
# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
PACKAGE = github.com/gohugoio/hugo
COMMIT_HASH = `git rev-parse --short HEAD 2>/dev/null`
BUILD_DATE = `date +%FT%T%z`
LDFLAGS = -ldflags "-X ${PACKAGE}/hugolib.CommitHash=${COMMIT_HASH} -X ${PACKAGE}/hugolib.BuildDate=${BUILD_DATE}"
NOGI_LDFLAGS = -ldflags "-X ${PACKAGE}/hugolib.BuildDate=${BUILD_DATE}"
.PHONY: vendor docker check fmt lint test test-race vet test-cover-html help
.DEFAULT_GOAL := help
vendor: ## Install govendor and sync Hugo's vendored dependencies
go get github.com/kardianos/govendor
govendor sync ${PACKAGE}
hugo: vendor ## Build hugo binary
go build ${LDFLAGS} ${PACKAGE}
hugo-race: vendor ## Build hugo binary with race detector enabled
go build -race ${LDFLAGS} ${PACKAGE}
install: vendor ## Install hugo binary
go install ${LDFLAGS} ${PACKAGE}
hugo-no-gitinfo: LDFLAGS = ${NOGI_LDFLAGS}
hugo-no-gitinfo: vendor hugo ## Build hugo without git info
docker: ## Build hugo Docker container
docker build -t hugo .
docker rm -f hugo-build || true
docker run --name hugo-build hugo ls /go/bin
docker cp hugo-build:/go/bin/hugo .
docker rm hugo-build
govendor: vendor # Deprecated: use "vendor" target
get: vendor # Deprecated: use "vendor"
gitinfo: hugo # Deprecated: use "hugo" target
install-gitinfo: install # Deprecated: use "install" target
no-git-info: hugo-no-gitinfo # Deprecated: use "hugo-no-gitinfo" target
check: test-race test386 fmt vet ## Run tests and linters
test386: ## Run tests in 32-bit mode
GOARCH=386 govendor test +local
test: ## Run tests
govendor test +local
test-race: ## Run tests with race detector
govendor test -race +local
fmt: ## Run gofmt linter
@for d in `govendor list -no-status +local | sed 's/github.com.gohugoio.hugo/./'` ; do \
if [ "`gofmt -l $$d/*.go | tee /dev/stderr`" ]; then \
echo "^ improperly formatted go files" && echo && exit 1; \
fi \
done
lint: ## Run golint linter
@for d in `govendor list -no-status +local | sed 's/github.com.gohugoio.hugo/./'` ; do \
if [ "`golint $$d | tee /dev/stderr`" ]; then \
echo "^ golint errors!" && echo && exit 1; \
fi \
done
vet: ## Run go vet linter
@if [ "`govendor vet +local | tee /dev/stderr`" ]; then \
echo "^ go vet errors!" && echo && exit 1; \
fi
test-cover-html: PACKAGES = $(shell govendor list -no-status +local | sed 's/github.com.gohugoio.hugo/./')
test-cover-html: ## Generate test coverage report
echo "mode: count" > coverage-all.out
$(foreach pkg,$(PACKAGES),\
govendor test -coverprofile=coverage.out -covermode=count $(pkg);\
tail -n +2 coverage.out >> coverage-all.out;)
go tool cover -html=coverage-all.out
check-vendor: ## Verify that vendored packages match git HEAD
@git diff-index --quiet HEAD vendor/ || (echo "check-vendor target failed: vendored packages out of sync" && echo && git diff vendor/ && exit 1)
help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

320
README.md
View file

@ -1,282 +1,106 @@
[bep]: https://github.com/bep
[bugs]: https://github.com/gohugoio/hugo/issues?q=is%3Aopen+is%3Aissue+label%3ABug
[contributing]: CONTRIBUTING.md
[create a proposal]: https://github.com/gohugoio/hugo/issues/new?labels=Proposal%2C+NeedsTriage&template=feature_request.md
[documentation repository]: https://github.com/gohugoio/hugoDocs
[documentation]: https://gohugo.io/documentation
[dragonfly bsd, freebsd, netbsd, and openbsd]: https://gohugo.io/installation/bsd
[features]: https://gohugo.io/about/features/
[forum]: https://discourse.gohugo.io
[friends]: https://github.com/gohugoio/hugo/graphs/contributors
[go]: https://go.dev/
[hugo modules]: https://gohugo.io/hugo-modules/
[installation]: https://gohugo.io/installation
[issue queue]: https://github.com/gohugoio/hugo/issues
[linux]: https://gohugo.io/installation/linux
[macos]: https://gohugo.io/installation/macos
[prebuilt binary]: https://github.com/gohugoio/hugo/releases/latest
[requesting help]: https://discourse.gohugo.io/t/requesting-help/9132
[spf13]: https://github.com/spf13
[static site generator]: https://en.wikipedia.org/wiki/Static_site_generator
[support]: https://discourse.gohugo.io
[themes]: https://themes.gohugo.io/
[website]: https://gohugo.io
[windows]: https://gohugo.io/installation/windows
![Hugo](https://raw.githubusercontent.com/gohugoio/hugoDocs/master/static/img/hugo-logo.png)
<a href="https://gohugo.io/"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/static/images/hugo-logo-wide.svg?sanitize=true" alt="Hugo" width="565"></a>
A Fast and Flexible Static Site Generator built with love by [spf13](http://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go][].
A fast and flexible static site generator built with love by [bep], [spf13], and [friends] in [Go].
---
[Website](https://gohugo.io) |
[Forum](https://discourse.gohugo.io) |
[Developer Chat (no support)](https://gitter.im/gohugoio/hugo) |
[Documentation](https://gohugo.io/overview/introduction/) |
[Installation Guide](https://gohugo.io/overview/installing/) |
[Contribution Guide](CONTRIBUTING.md) |
[Twitter](http://twitter.com/gohugoio)
[![GoDoc](https://godoc.org/github.com/gohugoio/hugo?status.svg)](https://godoc.org/github.com/gohugoio/hugo)
[![Tests on Linux, MacOS and Windows](https://github.com/gohugoio/hugo/workflows/Test/badge.svg)](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
[![Linux and macOS Build Status](https://api.travis-ci.org/gohugoio/hugo.svg?branch=master&label=Linux+and+macOS+build "Linux and macOS Build Status")](https://travis-ci.org/gohugoio/hugo)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/a5mr220vsd091kua?svg=true&label=Windows+build "Windows Build Status")](https://ci.appveyor.com/project/bep/hugo/branch/master)
[![Dev chat at https://gitter.im/gohugoio/hugo](https://img.shields.io/badge/gitter-developer_chat-46bc99.svg)](https://gitter.im/spf13/hugo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](https://goreportcard.com/report/github.com/gohugoio/hugo)
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
## Overview
Hugo is a [static site generator] written in [Go], optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less.
Hugo is a static HTML and CSS website generator written in [Go][].
It is optimized for speed, easy use and configurability.
Hugo takes a directory with content and templates and renders them into a full HTML website.
Due to its flexible framework, multilingual support, and powerful taxonomy system, Hugo is widely used to create:
Hugo relies on Markdown files with front matter for meta data.
And you can run Hugo from any directory.
This works well for shared hosts and other systems where you dont have a privileged account.
- Corporate, government, nonprofit, education, news, event, and project sites
- Documentation sites
- Image portfolios
- Landing pages
- Business, professional, and personal blogs
- Resumes and CVs
Hugo renders a typical website of moderate size in a fraction of a second.
A good rule of thumb is that each piece of content renders in around 1 millisecond.
Use Hugo's embedded web server during development to instantly see changes to content, structure, behavior, and presentation. Then deploy the site to your host, or push changes to your Git provider for automated builds and deployment.
Hugo is designed to work well for any kind of website including blogs, tumbles and docs.
Hugo's fast asset pipelines include:
#### Supported Architectures
- Image processing &ndash; Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
- JavaScript bundling &ndash; Transpile TypeScript and JSX to JavaScript, bundle, tree shake, minify, create source maps, and perform SRI hashing.
- Sass processing &ndash; Transpile Sass to CSS, bundle, tree shake, minify, create source maps, perform SRI hashing, and integrate with PostCSS
- Tailwind CSS processing &ndash; Compile Tailwind CSS utility classes into standard CSS, bundle, tree shake, optimize, minify, perform SRI hashing, and integrate with PostCSS
Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD and macOS (Darwin) and [Android](https://gist.github.com/bep/a0d8a26cf6b4f8bc992729b8e50b480b) for x64, i386 and ARM architectures.
And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories.
Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including DragonFly BSD, OpenBSD, Plan&nbsp;9 and Solaris.
See the [features] section of the documentation for a comprehensive summary of Hugo's capabilities.
**Complete documentation is available at [Hugo Documentation][].**
## Sponsors
## Choose How to Install
<p>&nbsp;</p>
<p float="left">
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
&nbsp;&nbsp;&nbsp;
<a href="https://pinme.eth.limo/?s=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/logo-pinme.svg" width="200" alt="PinMe."></a>
</p>
If you want to use Hugo as your site generator, simply install the Hugo binaries.
The Hugo binaries have no external dependencies.
## Editions
To contribute to the Hugo source code or documentation, you should [fork the Hugo GitHub project](https://github.com/gohugoio/hugo#fork-destination-box) and clone it to your local machine.
Hugo is available in three editions: standard, extended, and extended/deploy. While the standard edition provides core functionality, the extended and extended/deploy editions offer advanced features.
Finally, you can install the Hugo source code with `go`, build the binaries yourself, and run Hugo that way.
Building the binaries is an easy task for an experienced `go` getter.
Feature|extended edition|extended/deploy edition
:--|:-:|:-:
Encode to the WebP format when [processing images]. You can decode WebP images with any edition.|:heavy_check_mark:|:heavy_check_mark:
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See&nbsp;[details].|:x:|:heavy_check_mark:
### Install Hugo as Your Site Generator (Binary Install)
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
[processing images]: https://gohugo.io/content-management/image-processing/
[transpile sass to css]: https://gohugo.io/functions/css/sass/
[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/
Use the [installation instructions in the Hugo documentation](https://gohugo.io/overview/installing/).
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
### Build and Install the Binaries from Source (Advanced Install)
## Installation
Add Hugo and its package dependencies to your go `src` directory.
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
go get -v github.com/gohugoio/hugo
- [macOS]
- [Linux]
- [Windows]
- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD]
Once the `get` completes, you should find your new `hugo` (or `hugo.exe`) executable sitting inside `$GOPATH/bin/`.
## Build from source
To update Hugos dependencies, use `go get` with the `-u` option.
Prerequisites to build Hugo from source:
go get -u -v github.com/gohugoio/hugo
- Standard edition: Go 1.23.0 or later
- Extended edition: Go 1.23.0 or later, and GCC
- Extended/deploy edition: Go 1.23.0 or later, and GCC
Build the standard edition:
```text
go install github.com/gohugoio/hugo@latest
```
Build the extended edition:
```text
CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest
```
Build the extended/deploy edition:
```text
CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest
```
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=gohugoio/hugo&type=Timeline)](https://star-history.com/#gohugoio/hugo&Timeline)
## Documentation
Hugo's [documentation] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
Please submit documentation issues and pull requests to the [documentation repository].
## Support
Please **do not use the issue queue** for questions or troubleshooting. Unless you are certain that your issue is a software defect, use the [forum].
Hugos [forum] is an active community of users and developers who answer questions, share knowledge, and provide examples. A quick search of over 20,000 topics will often answer your question. Please be sure to read about [requesting help] before asking your first question.
## Contributing
You can contribute to the Hugo project by:
- Answering questions on the [forum]
- Improving the [documentation]
- Monitoring the [issue queue]
- Creating or improving [themes]
- Squashing [bugs]
Please submit documentation issues and pull requests to the [documentation repository].
If you have an idea for an enhancement or new feature, create a new topic on the [forum] in the "Feature" category. This will help you to:
- Determine if the capability already exists
- Measure interest
- Refine the concept
If there is sufficient interest, [create a proposal]. Do not submit a pull request until the project lead accepts the proposal.
## Contributing to Hugo
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
## Dependencies
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,
helping to manage issues, etc.
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
The Hugo community and maintainers are [very active](https://github.com/gohugoio/hugo/pulse/monthly) and helpful, and the project benefits greatly from this activity.
<details>
<summary>See current dependencies</summary>
### Asking Support Questions
```text
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
github.com/PuerkitoBio/goquery="v1.10.1"
github.com/alecthomas/chroma/v2="v2.15.0"
github.com/andybalholm/cascadia="v1.3.3"
github.com/armon/go-radix="v1.0.1-0.20221118154546-54df44f2176c"
github.com/bep/clocks="v0.5.0"
github.com/bep/debounce="v1.2.0"
github.com/bep/gitmap="v1.6.0"
github.com/bep/goat="v0.5.0"
github.com/bep/godartsass/v2="v2.3.2"
github.com/bep/golibsass="v1.2.0"
github.com/bep/gowebp="v0.3.0"
github.com/bep/imagemeta="v0.8.4"
github.com/bep/lazycache="v0.7.0"
github.com/bep/logg="v0.4.0"
github.com/bep/mclib="v1.20400.20402"
github.com/bep/overlayfs="v0.9.2"
github.com/bep/simplecobra="v0.5.0"
github.com/bep/tmc="v0.5.1"
github.com/cespare/xxhash/v2="v2.3.0"
github.com/clbanning/mxj/v2="v2.7.0"
github.com/cpuguy83/go-md2man/v2="v2.0.4"
github.com/disintegration/gift="v1.2.1"
github.com/dlclark/regexp2="v1.11.5"
github.com/dop251/goja="v0.0.0-20250125213203-5ef83b82af17"
github.com/evanw/esbuild="v0.24.2"
github.com/fatih/color="v1.18.0"
github.com/frankban/quicktest="v1.14.6"
github.com/fsnotify/fsnotify="v1.8.0"
github.com/getkin/kin-openapi="v0.129.0"
github.com/ghodss/yaml="v1.0.0"
github.com/go-openapi/jsonpointer="v0.21.0"
github.com/go-openapi/swag="v0.23.0"
github.com/go-sourcemap/sourcemap="v2.1.4+incompatible"
github.com/gobuffalo/flect="v1.0.3"
github.com/gobwas/glob="v0.2.3"
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
github.com/gohugoio/hashstructure="v0.5.0"
github.com/gohugoio/httpcache="v0.7.0"
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0"
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0"
github.com/gohugoio/locales="v0.14.0"
github.com/gohugoio/localescompressed="v1.0.1"
github.com/golang/freetype="v0.0.0-20170609003504-e2365dfdc4a0"
github.com/google/go-cmp="v0.6.0"
github.com/google/pprof="v0.0.0-20250208200701-d0013a598941"
github.com/gorilla/websocket="v1.5.3"
github.com/hairyhenderson/go-codeowners="v0.7.0"
github.com/hashicorp/golang-lru/v2="v2.0.7"
github.com/jdkato/prose="v1.2.1"
github.com/josharian/intern="v1.0.0"
github.com/kr/pretty="v0.3.1"
github.com/kr/text="v0.2.0"
github.com/kyokomi/emoji/v2="v2.2.13"
github.com/lucasb-eyer/go-colorful="v1.2.0"
github.com/mailru/easyjson="v0.7.7"
github.com/makeworld-the-better-one/dither/v2="v2.4.0"
github.com/marekm4/color-extractor="v1.2.1"
github.com/mattn/go-colorable="v0.1.13"
github.com/mattn/go-isatty="v0.0.20"
github.com/mattn/go-runewidth="v0.0.9"
github.com/mazznoer/csscolorparser="v0.1.5"
github.com/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c"
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
github.com/muesli/smartcrop="v0.3.0"
github.com/niklasfasching/go-org="v1.7.0"
github.com/oasdiff/yaml3="v0.0.0-20241210130736-a94c01f36349"
github.com/oasdiff/yaml="v0.0.0-20241210131133-6b86fb107d80"
github.com/olekukonko/tablewriter="v0.0.5"
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
github.com/pelletier/go-toml/v2="v2.2.3"
github.com/perimeterx/marshmallow="v1.1.5"
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c"
github.com/pkg/errors="v0.9.1"
github.com/rivo/uniseg="v0.4.7"
github.com/rogpeppe/go-internal="v1.13.1"
github.com/russross/blackfriday/v2="v2.1.0"
github.com/sass/libsass="3.6.6"
github.com/spf13/afero="v1.11.0"
github.com/spf13/cast="v1.7.1"
github.com/spf13/cobra="v1.8.1"
github.com/spf13/fsync="v0.10.1"
github.com/spf13/pflag="v1.0.6"
github.com/tdewolff/minify/v2="v2.20.37"
github.com/tdewolff/parse/v2="v2.7.15"
github.com/tetratelabs/wazero="v1.8.2"
github.com/webmproject/libwebp="v1.3.2"
github.com/yuin/goldmark-emoji="v1.0.4"
github.com/yuin/goldmark="v1.7.8"
go.uber.org/automaxprocs="v1.5.3"
golang.org/x/crypto="v0.33.0"
golang.org/x/exp="v0.0.0-20250210185358-939b2ce775ac"
golang.org/x/image="v0.24.0"
golang.org/x/mod="v0.23.0"
golang.org/x/net="v0.35.0"
golang.org/x/sync="v0.11.0"
golang.org/x/sys="v0.30.0"
golang.org/x/text="v0.22.0"
golang.org/x/tools="v0.30.0"
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da"
gonum.org/v1/plot="v0.15.0"
google.golang.org/protobuf="v1.36.5"
gopkg.in/yaml.v2="v2.4.0"
gopkg.in/yaml.v3="v3.0.1"
oss.terrastruct.com/d2="v0.6.9"
oss.terrastruct.com/util-go="v0.0.0-20241005222610-44c011a04896"
rsc.io/qr="v0.2.0"
software.sslmate.com/src/go-pkcs12="v0.2.0"
```
</details>
We have an active [discussion forum](https://discourse.gohugo.io) where users and developers can ask questions.
Please don't use the GitHub issue tracker to ask questions.
### Reporting Issues
If you believe you have found a defect in Hugo or its documentation, use
the GitHub issue tracker to report the problem to the Hugo maintainers.
If you're not sure if it's a bug or not, start by asking in the [discussion forum](https://discourse.gohugo.io).
When reporting the issue, please provide the version of Hugo in use (`hugo version`).
### Submitting Patches
The Hugo project welcomes all contributors and contributions regardless of skill or experience level.
If you are interested in helping with the project, we will help you with your contribution.
Hugo is a very active project with many contributions happening daily.
Because we want to create the best possible product for our users and the best contribution experience for our developers,
we have a set of guidelines which ensure that all contributions are acceptable.
The guidelines are not intended as a filter or barrier to participation.
If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines.
For a complete guide to contributing code to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
[![Analytics](https://ga-beacon.appspot.com/UA-7131036-6/hugo/readme)](https://github.com/igrigorik/ga-beacon)
[Go]: https://golang.org/
[Hugo Documentation]: https://gohugo.io/overview/introduction/

View file

@ -1,7 +0,0 @@
## Security Policy
### Reporting a Vulnerability
Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days.
Also see [Hugo's Security Model](https://gohugo.io/about/security/).

17
appveyor.yml Normal file
View file

@ -0,0 +1,17 @@
init:
- copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe
- set PATH=%PATH%;C:\MinGW\bin;%GOPATH%\bin
- go version
- go env
# clones and cd's to path
clone_folder: C:\GOPATH\src\github.com\gohugoio\hugo
install:
- gem install asciidoctor
- pip install docutils
build_script:
- make hugo-race check
- hugo -s docs/
- hugo --renderToMemory -s docs/

35
bench.sh Executable file
View file

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

9
benchSite.sh Executable file
View file

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

View file

@ -20,7 +20,7 @@ import (
)
var bufferPool = &sync.Pool{
New: func() any {
New: func() interface{} {
return &bytes.Buffer{}
},
}

View file

@ -14,18 +14,14 @@
package bufferpool
import (
"github.com/stretchr/testify/assert"
"testing"
qt "github.com/frankban/quicktest"
)
func TestBufferPool(t *testing.T) {
c := qt.New(t)
buff := GetBuffer()
buff.WriteString("do be do be do")
c.Assert(buff.String(), qt.Equals, "do be do be do")
assert.Equal(t, "do be do be do", buff.String())
PutBuffer(buff)
c.Assert(buff.Len(), qt.Equals, 0)
assert.Equal(t, 0, buff.Len())
}

2
cache/docs.go vendored
View file

@ -1,2 +0,0 @@
// Package cache contains the different cache implementations.
package cache

View file

@ -1,647 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dynacache
import (
"context"
"fmt"
"math"
"path"
"regexp"
"runtime"
"sync"
"time"
"github.com/bep/lazycache"
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/rungroup"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/resource"
)
const minMaxSize = 10
type KeyIdentity struct {
Key any
Identity identity.Identity
}
// New creates a new cache.
func New(opts Options) *Cache {
if opts.CheckInterval == 0 {
opts.CheckInterval = time.Second * 2
}
if opts.MaxSize == 0 {
opts.MaxSize = 100000
}
if opts.Log == nil {
panic("nil Log")
}
if opts.MinMaxSize == 0 {
opts.MinMaxSize = 30
}
stats := &stats{
opts: opts,
adjustmentFactor: 1.0,
currentMaxSize: opts.MaxSize,
availableMemory: config.GetMemoryLimit(),
}
infol := opts.Log.InfoCommand("dynacache")
evictedIdentities := collections.NewStack[KeyIdentity]()
onEvict := func(k, v any) {
if !opts.Watching {
return
}
identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool {
evictedIdentities.Push(KeyIdentity{Key: k, Identity: id})
return false
})
resource.MarkStale(v)
}
c := &Cache{
partitions: make(map[string]PartitionManager),
onEvict: onEvict,
evictedIdentities: evictedIdentities,
opts: opts,
stats: stats,
infol: infol,
}
c.stop = c.start()
return c
}
// Options for the cache.
type Options struct {
Log loggers.Logger
CheckInterval time.Duration
MaxSize int
MinMaxSize int
Watching bool
}
// Options for a partition.
type OptionsPartition struct {
// When to clear the this partition.
ClearWhen ClearWhen
// Weight is a number between 1 and 100 that indicates how, in general, how big this partition may get.
Weight int
}
func (o OptionsPartition) WeightFraction() float64 {
return float64(o.Weight) / 100
}
func (o OptionsPartition) CalculateMaxSize(maxSizePerPartition int) int {
return int(math.Floor(float64(maxSizePerPartition) * o.WeightFraction()))
}
// A dynamic partitioned cache.
type Cache struct {
mu sync.RWMutex
partitions map[string]PartitionManager
onEvict func(k, v any)
evictedIdentities *collections.Stack[KeyIdentity]
opts Options
infol logg.LevelLogger
stats *stats
stopOnce sync.Once
stop func()
}
// DrainEvictedIdentities drains the evicted identities from the cache.
func (c *Cache) DrainEvictedIdentities() []KeyIdentity {
return c.evictedIdentities.Drain()
}
// DrainEvictedIdentitiesMatching drains the evicted identities from the cache that match the given predicate.
func (c *Cache) DrainEvictedIdentitiesMatching(predicate func(KeyIdentity) bool) []KeyIdentity {
return c.evictedIdentities.DrainMatching(predicate)
}
// ClearMatching clears all partition for which the predicate returns true.
func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManager) bool, predicateValue func(k, v any) bool) {
if predicatePartition == nil {
predicatePartition = func(k string, p PartitionManager) bool { return true }
}
if predicateValue == nil {
panic("nil predicateValue")
}
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
NumWorkers: len(c.partitions),
Handle: func(ctx context.Context, partition PartitionManager) error {
partition.clearMatching(predicateValue)
return nil
},
})
for k, p := range c.partitions {
if !predicatePartition(k, p) {
continue
}
g.Enqueue(p)
}
g.Wait()
}
// ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account.
// predicate is optional and will clear any entry for which it returns true.
func (c *Cache) ClearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) {
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
NumWorkers: len(c.partitions),
Handle: func(ctx context.Context, partition PartitionManager) error {
partition.clearOnRebuild(predicate, changeset...)
return nil
},
})
for _, p := range c.partitions {
g.Enqueue(p)
}
g.Wait()
// Clear any entries marked as stale above.
g = rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
NumWorkers: len(c.partitions),
Handle: func(ctx context.Context, partition PartitionManager) error {
partition.clearStale()
return nil
},
})
for _, p := range c.partitions {
g.Enqueue(p)
}
g.Wait()
}
type keysProvider interface {
Keys() []string
}
// Keys returns a list of keys in all partitions.
func (c *Cache) Keys(predicate func(s string) bool) []string {
if predicate == nil {
predicate = func(s string) bool { return true }
}
var keys []string
for pn, g := range c.partitions {
pkeys := g.(keysProvider).Keys()
for _, k := range pkeys {
p := path.Join(pn, k)
if predicate(p) {
keys = append(keys, p)
}
}
}
return keys
}
func calculateMaxSizePerPartition(maxItemsTotal, totalWeightQuantity, numPartitions int) int {
if numPartitions == 0 {
panic("numPartitions must be > 0")
}
if totalWeightQuantity == 0 {
panic("totalWeightQuantity must be > 0")
}
avgWeight := float64(totalWeightQuantity) / float64(numPartitions)
return int(math.Floor(float64(maxItemsTotal) / float64(numPartitions) * (100.0 / avgWeight)))
}
// Stop stops the cache.
func (c *Cache) Stop() {
c.stopOnce.Do(func() {
c.stop()
})
}
func (c *Cache) adjustCurrentMaxSize() {
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.partitions) == 0 {
return
}
var m runtime.MemStats
runtime.ReadMemStats(&m)
s := c.stats
s.memstatsCurrent = m
// fmt.Printf("\n\nAvailable = %v\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\nMaxSize = %d\nAdjustmentFactor=%f\n\n", helpers.FormatByteCount(s.availableMemory), helpers.FormatByteCount(m.Alloc), helpers.FormatByteCount(m.TotalAlloc), helpers.FormatByteCount(m.Sys), m.NumGC, c.stats.currentMaxSize, s.adjustmentFactor)
if s.availableMemory >= s.memstatsCurrent.Alloc {
if s.adjustmentFactor <= 1.0 {
s.adjustmentFactor += 0.2
}
} else {
// We're low on memory.
s.adjustmentFactor -= 0.4
}
if s.adjustmentFactor <= 0 {
s.adjustmentFactor = 0.05
}
if !s.adjustCurrentMaxSize() {
return
}
totalWeight := 0
for _, pm := range c.partitions {
totalWeight += pm.getOptions().Weight
}
maxSizePerPartition := calculateMaxSizePerPartition(c.stats.currentMaxSize, totalWeight, len(c.partitions))
evicted := 0
for _, p := range c.partitions {
evicted += p.adjustMaxSize(p.getOptions().CalculateMaxSize(maxSizePerPartition))
}
if evicted > 0 {
c.infol.
WithFields(
logg.Fields{
{Name: "evicted", Value: evicted},
{Name: "numGC", Value: m.NumGC},
{Name: "limit", Value: helpers.FormatByteCount(c.stats.availableMemory)},
{Name: "alloc", Value: helpers.FormatByteCount(m.Alloc)},
{Name: "totalAlloc", Value: helpers.FormatByteCount(m.TotalAlloc)},
},
).Logf("adjusted partitions' max size")
}
}
func (c *Cache) start() func() {
ticker := time.NewTicker(c.opts.CheckInterval)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
c.adjustCurrentMaxSize()
// Reset the ticker to avoid drift.
ticker.Reset(c.opts.CheckInterval)
case <-quit:
ticker.Stop()
return
}
}
}()
return func() {
close(quit)
}
}
var partitionNameRe = regexp.MustCompile(`^\/[a-zA-Z0-9]{4}(\/[a-zA-Z0-9]+)?(\/[a-zA-Z0-9]+)?`)
// GetOrCreatePartition gets or creates a partition with the given name.
func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts OptionsPartition) *Partition[K, V] {
if c == nil {
panic("nil Cache")
}
if opts.Weight < 1 || opts.Weight > 100 {
panic("invalid Weight, must be between 1 and 100")
}
if partitionNameRe.FindString(name) != name {
panic(fmt.Sprintf("invalid partition name %q", name))
}
c.mu.RLock()
p, found := c.partitions[name]
c.mu.RUnlock()
if found {
return p.(*Partition[K, V])
}
c.mu.Lock()
defer c.mu.Unlock()
// Double check.
p, found = c.partitions[name]
if found {
return p.(*Partition[K, V])
}
// At this point, we don't know the number of partitions or their configuration, but
// this will be re-adjusted later.
const numberOfPartitionsEstimate = 10
maxSize := opts.CalculateMaxSize(c.opts.MaxSize / numberOfPartitionsEstimate)
onEvict := func(k K, v V) {
c.onEvict(k, v)
}
// Create a new partition and cache it.
partition := &Partition[K, V]{
c: lazycache.New(lazycache.Options[K, V]{MaxEntries: maxSize, OnEvict: onEvict}),
maxSize: maxSize,
trace: c.opts.Log.Logger().WithLevel(logg.LevelTrace).WithField("partition", name),
opts: opts,
}
c.partitions[name] = partition
return partition
}
// Partition is a partition in the cache.
type Partition[K comparable, V any] struct {
c *lazycache.Cache[K, V]
zero V
trace logg.LevelLogger
opts OptionsPartition
maxSize int
}
// GetOrCreate gets or creates a value for the given key.
func (p *Partition[K, V]) GetOrCreate(key K, create func(key K) (V, error)) (V, error) {
v, err := p.doGetOrCreate(key, create)
if err != nil {
return p.zero, err
}
if resource.StaleVersion(v) > 0 {
p.c.Delete(key)
return p.doGetOrCreate(key, create)
}
return v, err
}
func (p *Partition[K, V]) doGetOrCreate(key K, create func(key K) (V, error)) (V, error) {
v, _, err := p.c.GetOrCreate(key, create)
return v, err
}
func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
v, err := p.doGetOrCreateWitTimeout(key, duration, create)
if err != nil {
return p.zero, err
}
if resource.StaleVersion(v) > 0 {
p.c.Delete(key)
return p.doGetOrCreateWitTimeout(key, duration, create)
}
return v, err
}
// GetOrCreateWitTimeout gets or creates a value for the given key and times out if the create function
// takes too long.
func (p *Partition[K, V]) doGetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
resultch := make(chan V, 1)
errch := make(chan error, 1)
go func() {
var (
v V
err error
)
defer func() {
if r := recover(); r != nil {
if rerr, ok := r.(error); ok {
err = rerr
} else {
err = fmt.Errorf("panic: %v", r)
}
}
if err != nil {
errch <- err
} else {
resultch <- v
}
}()
v, _, err = p.c.GetOrCreate(key, create)
}()
select {
case v := <-resultch:
return v, nil
case err := <-errch:
return p.zero, err
case <-time.After(duration):
return p.zero, &herrors.TimeoutError{
Duration: duration,
}
}
}
func (p *Partition[K, V]) clearMatching(predicate func(k, v any) bool) {
p.c.DeleteFunc(func(key K, v V) bool {
if predicate(key, v) {
p.trace.Log(
logg.StringFunc(
func() string {
return fmt.Sprintf("clearing cache key %v", key)
},
),
)
return true
}
return false
})
}
func (p *Partition[K, V]) clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) {
if predicate == nil {
predicate = func(k, v any) bool {
return false
}
}
opts := p.getOptions()
if opts.ClearWhen == ClearNever {
return
}
if opts.ClearWhen == ClearOnRebuild {
// Clear all.
p.Clear()
return
}
depsFinder := identity.NewFinder(identity.FinderConfig{})
shouldDelete := func(key K, v V) bool {
// We always clear elements marked as stale.
if resource.StaleVersion(v) > 0 {
return true
}
// Now check if this entry has changed based on the changeset
// based on filesystem events.
if len(changeset) == 0 {
// Nothing changed.
return false
}
var probablyDependent bool
identity.WalkIdentitiesShallow(v, func(level int, id2 identity.Identity) bool {
for _, id := range changeset {
if r := depsFinder.Contains(id, id2, -1); r > 0 {
// It's probably dependent, evict from cache.
probablyDependent = true
return true
}
}
return false
})
return probablyDependent
}
// First pass.
// Second pass needs to be done in a separate loop to catch any
// elements marked as stale in the other partitions.
p.c.DeleteFunc(func(key K, v V) bool {
if predicate(key, v) || shouldDelete(key, v) {
p.trace.Log(
logg.StringFunc(
func() string {
return fmt.Sprintf("first pass: clearing cache key %v", key)
},
),
)
return true
}
return false
})
}
func (p *Partition[K, V]) Keys() []K {
var keys []K
p.c.DeleteFunc(func(key K, v V) bool {
keys = append(keys, key)
return false
})
return keys
}
func (p *Partition[K, V]) clearStale() {
p.c.DeleteFunc(func(key K, v V) bool {
staleVersion := resource.StaleVersion(v)
if staleVersion > 0 {
p.trace.Log(
logg.StringFunc(
func() string {
return fmt.Sprintf("second pass: clearing cache key %v", key)
},
),
)
}
return staleVersion > 0
})
}
// adjustMaxSize adjusts the max size of the and returns the number of items evicted.
func (p *Partition[K, V]) adjustMaxSize(newMaxSize int) int {
if newMaxSize < minMaxSize {
newMaxSize = minMaxSize
}
oldMaxSize := p.maxSize
if newMaxSize == oldMaxSize {
return 0
}
p.maxSize = newMaxSize
// fmt.Println("Adjusting max size of partition from", oldMaxSize, "to", newMaxSize)
return p.c.Resize(newMaxSize)
}
func (p *Partition[K, V]) getMaxSize() int {
return p.maxSize
}
func (p *Partition[K, V]) getOptions() OptionsPartition {
return p.opts
}
func (p *Partition[K, V]) Clear() {
p.c.DeleteFunc(func(key K, v V) bool {
return true
})
}
func (p *Partition[K, V]) Get(ctx context.Context, key K) (V, bool) {
return p.c.Get(key)
}
type PartitionManager interface {
adjustMaxSize(addend int) int
getMaxSize() int
getOptions() OptionsPartition
clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity)
clearMatching(predicate func(k, v any) bool)
clearStale()
}
const (
ClearOnRebuild ClearWhen = iota + 1
ClearOnChange
ClearNever
)
type ClearWhen int
type stats struct {
opts Options
memstatsCurrent runtime.MemStats
currentMaxSize int
availableMemory uint64
adjustmentFactor float64
}
func (s *stats) adjustCurrentMaxSize() bool {
newCurrentMaxSize := int(math.Floor(float64(s.opts.MaxSize) * s.adjustmentFactor))
if newCurrentMaxSize < s.opts.MinMaxSize {
newCurrentMaxSize = int(s.opts.MinMaxSize)
}
changed := newCurrentMaxSize != s.currentMaxSize
s.currentMaxSize = newCurrentMaxSize
return changed
}
// CleanKey turns s into a format suitable for a cache key for this package.
// The key will be a Unix-styled path with a leading slash but no trailing slash.
func CleanKey(s string) string {
return path.Clean(paths.ToSlashPreserveLeading(s))
}

View file

@ -1,230 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dynacache
import (
"errors"
"fmt"
"path/filepath"
"testing"
"time"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/resource"
)
var (
_ resource.StaleInfo = (*testItem)(nil)
_ identity.Identity = (*testItem)(nil)
)
type testItem struct {
name string
staleVersion uint32
}
func (t testItem) StaleVersion() uint32 {
return t.staleVersion
}
func (t testItem) IdentifierBase() string {
return t.name
}
func TestCache(t *testing.T) {
t.Parallel()
c := qt.New(t)
cache := New(Options{
Log: loggers.NewDefault(),
})
c.Cleanup(func() {
cache.Stop()
})
opts := OptionsPartition{Weight: 30}
c.Assert(cache, qt.Not(qt.IsNil))
p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", opts)
c.Assert(p1, qt.Not(qt.IsNil))
p2 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", opts)
c.Assert(func() { GetOrCreatePartition[string, testItem](cache, "foo bar", opts) }, qt.PanicMatches, ".*invalid partition name.*")
c.Assert(func() { GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", OptionsPartition{Weight: 1234}) }, qt.PanicMatches, ".*invalid Weight.*")
c.Assert(p2, qt.Equals, p1)
p3 := GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", opts)
c.Assert(p3, qt.Not(qt.IsNil))
c.Assert(p3, qt.Not(qt.Equals), p1)
c.Assert(func() { New(Options{}) }, qt.PanicMatches, ".*nil Log.*")
}
func TestCalculateMaxSizePerPartition(t *testing.T) {
t.Parallel()
c := qt.New(t)
c.Assert(calculateMaxSizePerPartition(1000, 500, 5), qt.Equals, 200)
c.Assert(calculateMaxSizePerPartition(1000, 250, 5), qt.Equals, 400)
c.Assert(func() { calculateMaxSizePerPartition(1000, 250, 0) }, qt.PanicMatches, ".*must be > 0.*")
c.Assert(func() { calculateMaxSizePerPartition(1000, 0, 1) }, qt.PanicMatches, ".*must be > 0.*")
}
func TestCleanKey(t *testing.T) {
c := qt.New(t)
c.Assert(CleanKey("a/b/c"), qt.Equals, "/a/b/c")
c.Assert(CleanKey("/a/b/c"), qt.Equals, "/a/b/c")
c.Assert(CleanKey("a/b/c/"), qt.Equals, "/a/b/c")
c.Assert(CleanKey(filepath.FromSlash("/a/b/c/")), qt.Equals, "/a/b/c")
}
func newTestCache(t *testing.T) *Cache {
cache := New(
Options{
Log: loggers.NewDefault(),
},
)
p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", OptionsPartition{Weight: 30, ClearWhen: ClearOnRebuild})
p2 := GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", OptionsPartition{Weight: 30, ClearWhen: ClearOnChange})
p1.GetOrCreate("clearOnRebuild", func(string) (testItem, error) {
return testItem{}, nil
})
p2.GetOrCreate("clearBecauseStale", func(string) (testItem, error) {
return testItem{
staleVersion: 32,
}, nil
})
p2.GetOrCreate("clearBecauseIdentityChanged", func(string) (testItem, error) {
return testItem{
name: "changed",
}, nil
})
p2.GetOrCreate("clearNever", func(string) (testItem, error) {
return testItem{
staleVersion: 0,
}, nil
})
t.Cleanup(func() {
cache.Stop()
})
return cache
}
func TestClear(t *testing.T) {
t.Parallel()
c := qt.New(t)
predicateAll := func(string) bool {
return true
}
cache := newTestCache(t)
c.Assert(cache.Keys(predicateAll), qt.HasLen, 4)
cache.ClearOnRebuild(nil)
// Stale items are always cleared.
c.Assert(cache.Keys(predicateAll), qt.HasLen, 2)
cache = newTestCache(t)
cache.ClearOnRebuild(nil, identity.StringIdentity("changed"))
c.Assert(cache.Keys(nil), qt.HasLen, 1)
cache = newTestCache(t)
cache.ClearMatching(nil, func(k, v any) bool {
return k.(string) == "clearOnRebuild"
})
c.Assert(cache.Keys(predicateAll), qt.HasLen, 3)
cache.adjustCurrentMaxSize()
}
func TestPanicInCreate(t *testing.T) {
t.Parallel()
c := qt.New(t)
cache := newTestCache(t)
p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", OptionsPartition{Weight: 30, ClearWhen: ClearOnRebuild})
willPanic := func(i int) func() {
return func() {
p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) {
panic(errors.New(key))
})
}
}
// GetOrCreateWitTimeout needs to recover from panics in the create func.
willErr := func(i int) error {
_, err := p1.GetOrCreateWitTimeout(fmt.Sprintf("error-%d", i), 10*time.Second, func(key string) (testItem, error) {
return testItem{}, errors.New(key)
})
return err
}
for i := range 3 {
for range 3 {
c.Assert(willPanic(i), qt.PanicMatches, fmt.Sprintf("panic-%d", i))
c.Assert(willErr(i), qt.ErrorMatches, fmt.Sprintf("error-%d", i))
}
}
// Test the same keys again without the panic.
for i := range 3 {
for range 3 {
v, err := p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) {
return testItem{
name: key,
}, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v.name, qt.Equals, fmt.Sprintf("panic-%d", i))
v, err = p1.GetOrCreateWitTimeout(fmt.Sprintf("error-%d", i), 10*time.Second, func(key string) (testItem, error) {
return testItem{
name: key,
}, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v.name, qt.Equals, fmt.Sprintf("error-%d", i))
}
}
}
func TestAdjustCurrentMaxSize(t *testing.T) {
t.Parallel()
c := qt.New(t)
cache := newTestCache(t)
alloc := cache.stats.memstatsCurrent.Alloc
cache.adjustCurrentMaxSize()
c.Assert(cache.stats.memstatsCurrent.Alloc, qt.Not(qt.Equals), alloc)
}

View file

@ -1,496 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package filecache
import (
"bytes"
"errors"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/gohugoio/httpcache"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/helpers"
"github.com/BurntSushi/locker"
"github.com/spf13/afero"
)
// ErrFatal can be used to signal an unrecoverable error.
var ErrFatal = errors.New("fatal filecache error")
const (
FilecacheRootDirname = "filecache"
)
// Cache caches a set of files in a directory. This is usually a file on
// disk, but since this is backed by an Afero file system, it can be anything.
type Cache struct {
Fs afero.Fs
// Max age for items in this cache. Negative duration means forever,
// 0 is effectively turning this cache off.
maxAge time.Duration
// When set, we just remove this entire root directory on expiration.
pruneAllRootDir string
nlocker *lockTracker
initOnce sync.Once
initErr error
}
type lockTracker struct {
seenMu sync.RWMutex
seen map[string]struct{}
*locker.Locker
}
// Lock tracks the ids in use. We use this information to do garbage collection
// after a Hugo build.
func (l *lockTracker) Lock(id string) {
l.seenMu.RLock()
if _, seen := l.seen[id]; !seen {
l.seenMu.RUnlock()
l.seenMu.Lock()
l.seen[id] = struct{}{}
l.seenMu.Unlock()
} else {
l.seenMu.RUnlock()
}
l.Locker.Lock(id)
}
// ItemInfo contains info about a cached file.
type ItemInfo struct {
// This is the file's name relative to the cache's filesystem.
Name string
}
// NewCache creates a new file cache with the given filesystem and max age.
func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
return &Cache{
Fs: fs,
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
maxAge: maxAge,
pruneAllRootDir: pruneAllRootDir,
}
}
// lockedFile is a file with a lock that is released on Close.
type lockedFile struct {
afero.File
unlock func()
}
func (l *lockedFile) Close() error {
defer l.unlock()
return l.File.Close()
}
func (c *Cache) init() error {
c.initOnce.Do(func() {
// Create the base dir if it does not exist.
if err := c.Fs.MkdirAll("", 0o777); err != nil && !os.IsExist(err) {
c.initErr = err
}
})
return c.initErr
}
// WriteCloser returns a transactional writer into the cache.
// It's important that it's closed when done.
func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
if err := c.init(); err != nil {
return ItemInfo{}, nil, err
}
id = cleanID(id)
c.nlocker.Lock(id)
info := ItemInfo{Name: id}
f, err := helpers.OpenFileForWriting(c.Fs, id)
if err != nil {
c.nlocker.Unlock(id)
return info, nil, err
}
return info, &lockedFile{
File: f,
unlock: func() { c.nlocker.Unlock(id) },
}, nil
}
// ReadOrCreate tries to lookup the file in cache.
// If found, it is passed to read and then closed.
// If not found a new file is created and passed to create, which should close
// it when done.
func (c *Cache) ReadOrCreate(id string,
read func(info ItemInfo, r io.ReadSeeker) error,
create func(info ItemInfo, w io.WriteCloser) error,
) (info ItemInfo, err error) {
if err := c.init(); err != nil {
return ItemInfo{}, err
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info = ItemInfo{Name: id}
if r := c.getOrRemove(id); r != nil {
err = read(info, r)
defer r.Close()
if err == nil || err == ErrFatal {
// See https://github.com/gohugoio/hugo/issues/6401
// To recover from file corruption we handle read errors
// as the cache item was not found.
// Any file permission issue will also fail in the next step.
return
}
}
f, err := helpers.OpenFileForWriting(c.Fs, id)
if err != nil {
return
}
err = create(info, f)
return
}
// NamedLock locks the given id. The lock is released when the returned function is called.
func (c *Cache) NamedLock(id string) func() {
id = cleanID(id)
c.nlocker.Lock(id)
return func() {
c.nlocker.Unlock(id)
}
}
// GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will
// be invoked and the result cached.
// This method is protected by a named lock using the given id as identifier.
func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (ItemInfo, io.ReadCloser, error) {
if err := c.init(); err != nil {
return ItemInfo{}, nil, err
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
if r := c.getOrRemove(id); r != nil {
return info, r, nil
}
var (
r io.ReadCloser
err error
)
r, err = create()
if err != nil {
return info, nil, err
}
if c.maxAge == 0 {
// No caching.
return info, hugio.ToReadCloser(r), nil
}
var buff bytes.Buffer
return info,
hugio.ToReadCloser(&buff),
c.writeReader(id, io.TeeReader(r, &buff))
}
func (c *Cache) writeReader(id string, r io.Reader) error {
dir := filepath.Dir(id)
if dir != "" {
_ = c.Fs.MkdirAll(dir, 0o777)
}
f, err := c.Fs.Create(id)
if err != nil {
return err
}
defer f.Close()
_, _ = io.Copy(f, r)
return nil
}
// GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice.
func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) {
if err := c.init(); err != nil {
return ItemInfo{}, nil, err
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
if r := c.getOrRemove(id); r != nil {
defer r.Close()
b, err := io.ReadAll(r)
return info, b, err
}
var (
b []byte
err error
)
b, err = create()
if err != nil {
return info, nil, err
}
if c.maxAge == 0 {
return info, b, nil
}
if err := c.writeReader(id, bytes.NewReader(b)); err != nil {
return info, nil, err
}
return info, b, nil
}
// GetBytes gets the file content with the given id from the cache, nil if none found.
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
if err := c.init(); err != nil {
return ItemInfo{}, nil, err
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
if r := c.getOrRemove(id); r != nil {
defer r.Close()
b, err := io.ReadAll(r)
return info, b, err
}
return info, nil, nil
}
// Get gets the file with the given id from the cache, nil if none found.
func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
if err := c.init(); err != nil {
return ItemInfo{}, nil, err
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
r := c.getOrRemove(id)
return info, r, nil
}
// getOrRemove gets the file with the given id. If it's expired, it will
// be removed.
func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
if c.maxAge == 0 {
// No caching.
return nil
}
if removed, err := c.removeIfExpired(id); err != nil || removed {
return nil
}
f, err := c.Fs.Open(id)
if err != nil {
return nil
}
return f
}
func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
if c.maxAge == 0 {
// No caching.
return nil, false
}
f, err := c.Fs.Open(id)
if err != nil {
return nil, false
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil, false
}
removed, err := c.removeIfExpired(id)
if err != nil {
return nil, false
}
return b, removed
}
func (c *Cache) removeIfExpired(id string) (bool, error) {
if c.maxAge <= 0 {
return false, nil
}
fi, err := c.Fs.Stat(id)
if err != nil {
return false, err
}
if c.isExpired(fi.ModTime()) {
c.Fs.Remove(id)
return true, nil
}
return false, nil
}
func (c *Cache) isExpired(modTime time.Time) bool {
if c.maxAge < 0 {
return false
}
// Note the use of time.Since here.
// We cannot use Hugo's global Clock for this.
return c.maxAge == 0 || time.Since(modTime) > c.maxAge
}
// For testing
func (c *Cache) GetString(id string) string {
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
f, err := c.Fs.Open(id)
if err != nil {
return ""
}
defer f.Close()
b, _ := io.ReadAll(f)
return string(b)
}
// Caches is a named set of caches.
type Caches map[string]*Cache
// Get gets a named cache, nil if none found.
func (f Caches) Get(name string) *Cache {
return f[strings.ToLower(name)]
}
// NewCaches creates a new set of file caches from the given
// configuration.
func NewCaches(p *helpers.PathSpec) (Caches, error) {
dcfg := p.Cfg.GetConfigSection("caches").(Configs)
fs := p.Fs.Source
m := make(Caches)
for k, v := range dcfg {
var cfs afero.Fs
if v.IsResourceDir {
cfs = p.BaseFs.ResourcesCache
} else {
cfs = fs
}
if cfs == nil {
panic("nil fs")
}
baseDir := v.DirCompiled
bfs := hugofs.NewBasePathFs(cfs, baseDir)
var pruneAllRootDir string
if k == CacheKeyModules {
pruneAllRootDir = "pkg"
}
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
}
return m, nil
}
func cleanID(name string) string {
return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator)
}
// AsHTTPCache returns an httpcache.Cache implementation for this file cache.
// Note that none of the methods are protected by named locks, so you need to make sure
// to do that in your own code.
func (c *Cache) AsHTTPCache() httpcache.Cache {
return &httpCache{c: c}
}
type httpCache struct {
c *Cache
}
func (h *httpCache) Get(id string) (resp []byte, ok bool) {
id = cleanID(id)
b, removed := h.c.getBytesAndRemoveIfExpired(id)
return b, !removed
}
func (h *httpCache) Set(id string, resp []byte) {
if h.c.maxAge == 0 {
return
}
id = cleanID(id)
if err := h.c.writeReader(id, bytes.NewReader(resp)); err != nil {
panic(err)
}
}
func (h *httpCache) Delete(key string) {
h.c.Fs.Remove(key)
}

View file

@ -1,247 +0,0 @@
// 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 filecache provides a file based cache for Hugo.
package filecache
import (
"errors"
"fmt"
"path"
"path/filepath"
"strings"
"time"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/mitchellh/mapstructure"
"github.com/spf13/afero"
)
const (
resourcesGenDir = ":resourceDir/_gen"
cacheDirProject = ":cacheDir/:project"
)
var defaultCacheConfig = FileCacheConfig{
MaxAge: -1, // Never expire
Dir: cacheDirProject,
}
const (
CacheKeyGetJSON = "getjson"
CacheKeyGetCSV = "getcsv"
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
CacheKeyGetResource = "getresource"
CacheKeyMisc = "misc"
)
type Configs map[string]FileCacheConfig
// For internal use.
func (c Configs) CacheDirModules() string {
return c[CacheKeyModules].DirCompiled
}
var defaultCacheConfigs = Configs{
CacheKeyModules: {
MaxAge: -1,
Dir: ":cacheDir/modules",
},
CacheKeyGetJSON: defaultCacheConfig,
CacheKeyGetCSV: defaultCacheConfig,
CacheKeyImages: {
MaxAge: -1,
Dir: resourcesGenDir,
},
CacheKeyAssets: {
MaxAge: -1,
Dir: resourcesGenDir,
},
CacheKeyGetResource: {
MaxAge: -1, // Never expire
Dir: cacheDirProject,
},
CacheKeyMisc: {
MaxAge: -1,
Dir: cacheDirProject,
},
}
type FileCacheConfig struct {
// Max age of cache entries in this cache. Any items older than this will
// be removed and not returned from the cache.
// A negative value means forever, 0 means cache is disabled.
// Hugo is lenient with what types it accepts here, but we recommend using
// a duration string, a sequence of decimal numbers, each with optional fraction and a unit suffix,
// such as "300ms", "1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
MaxAge time.Duration
// The directory where files are stored.
Dir string
DirCompiled string `json:"-"`
// Will resources/_gen will get its own composite filesystem that
// also checks any theme.
IsResourceDir bool `json:"-"`
}
// GetJSONCache gets the file cache for getJSON.
func (f Caches) GetJSONCache() *Cache {
return f[CacheKeyGetJSON]
}
// GetCSVCache gets the file cache for getCSV.
func (f Caches) GetCSVCache() *Cache {
return f[CacheKeyGetCSV]
}
// ImageCache gets the file cache for processed images.
func (f Caches) ImageCache() *Cache {
return f[CacheKeyImages]
}
// ModulesCache gets the file cache for Hugo Modules.
func (f Caches) ModulesCache() *Cache {
return f[CacheKeyModules]
}
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
func (f Caches) AssetsCache() *Cache {
return f[CacheKeyAssets]
}
// MiscCache gets the file cache for miscellaneous stuff.
func (f Caches) MiscCache() *Cache {
return f[CacheKeyMisc]
}
// GetResourceCache gets the file cache for remote resources.
func (f Caches) GetResourceCache() *Cache {
return f[CacheKeyGetResource]
}
func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Configs, error) {
c := make(Configs)
valid := make(map[string]bool)
// Add defaults
for k, v := range defaultCacheConfigs {
c[k] = v
valid[k] = true
}
_, isOsFs := fs.(*afero.OsFs)
for k, v := range m {
if _, ok := v.(maps.Params); !ok {
continue
}
cc := defaultCacheConfig
dc := &mapstructure.DecoderConfig{
Result: &cc,
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
WeaklyTypedInput: true,
}
decoder, err := mapstructure.NewDecoder(dc)
if err != nil {
return c, err
}
if err := decoder.Decode(v); err != nil {
return nil, fmt.Errorf("failed to decode filecache config: %w", err)
}
if cc.Dir == "" {
return c, errors.New("must provide cache Dir")
}
name := strings.ToLower(k)
if !valid[name] {
return nil, fmt.Errorf("%q is not a valid cache name", name)
}
c[name] = cc
}
for k, v := range c {
dir := filepath.ToSlash(filepath.Clean(v.Dir))
hadSlash := strings.HasPrefix(dir, "/")
parts := strings.Split(dir, "/")
for i, part := range parts {
if strings.HasPrefix(part, ":") {
resolved, isResource, err := resolveDirPlaceholder(fs, bcfg, part)
if err != nil {
return c, err
}
if isResource {
v.IsResourceDir = true
}
parts[i] = resolved
}
}
dir = path.Join(parts...)
if hadSlash {
dir = "/" + dir
}
v.DirCompiled = filepath.Clean(filepath.FromSlash(dir))
if !v.IsResourceDir {
if isOsFs && !filepath.IsAbs(v.DirCompiled) {
return c, fmt.Errorf("%q must resolve to an absolute directory", v.DirCompiled)
}
// Avoid cache in root, e.g. / (Unix) or c:\ (Windows)
if len(strings.TrimPrefix(v.DirCompiled, filepath.VolumeName(v.DirCompiled))) == 1 {
return c, fmt.Errorf("%q is a root folder and not allowed as cache dir", v.DirCompiled)
}
}
if !strings.HasPrefix(v.DirCompiled, "_gen") {
// We do cache eviction (file removes) and since the user can set
// his/hers own cache directory, we really want to make sure
// we do not delete any files that do not belong to this cache.
// We do add the cache name as the root, but this is an extra safe
// guard. We skip the files inside /resources/_gen/ because
// that would be breaking.
v.DirCompiled = filepath.Join(v.DirCompiled, FilecacheRootDirname, k)
} else {
v.DirCompiled = filepath.Join(v.DirCompiled, k)
}
c[k] = v
}
return c, nil
}
// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ...
func resolveDirPlaceholder(fs afero.Fs, bcfg config.BaseConfig, placeholder string) (cacheDir string, isResource bool, err error) {
switch strings.ToLower(placeholder) {
case ":resourcedir":
return "", true, nil
case ":cachedir":
return bcfg.CacheDir, false, nil
case ":project":
return filepath.Base(bcfg.WorkingDir), false, nil
}
return "", false, fmt.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder)
}

View file

@ -1,146 +0,0 @@
// 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 filecache_test
import (
"path/filepath"
"runtime"
"testing"
"time"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/testconfig"
qt "github.com/frankban/quicktest"
)
func TestDecodeConfig(t *testing.T) {
t.Parallel()
c := qt.New(t)
configStr := `
resourceDir = "myresources"
contentDir = "content"
dataDir = "data"
i18nDir = "i18n"
layoutDir = "layouts"
assetDir = "assets"
archetypeDir = "archetypes"
[caches]
[caches.getJSON]
maxAge = "10m"
dir = "/path/to/c1"
[caches.getCSV]
maxAge = "11h"
dir = "/path/to/c2"
[caches.images]
dir = "/path/to/c3"
[caches.getResource]
dir = "/path/to/c4"
`
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7)
c2 := decoded["getcsv"]
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv"))
c3 := decoded["images"]
c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1))
c.Assert(c3.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images"))
c4 := decoded["getresource"]
c.Assert(c4.MaxAge, qt.Equals, time.Duration(-1))
c.Assert(c4.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource"))
}
func TestDecodeConfigIgnoreCache(t *testing.T) {
t.Parallel()
c := qt.New(t)
configStr := `
resourceDir = "myresources"
contentDir = "content"
dataDir = "data"
i18nDir = "i18n"
layoutDir = "layouts"
assetDir = "assets"
archeTypedir = "archetypes"
ignoreCache = true
[caches]
[caches.getJSON]
maxAge = 1234
dir = "/path/to/c1"
[caches.getCSV]
maxAge = 3456
dir = "/path/to/c2"
[caches.images]
dir = "/path/to/c3"
[caches.getResource]
dir = "/path/to/c4"
`
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7)
for _, v := range decoded {
c.Assert(v.MaxAge, qt.Equals, time.Duration(0))
}
}
func TestDecodeConfigDefault(t *testing.T) {
c := qt.New(t)
cfg := config.New()
if runtime.GOOS == "windows" {
cfg.Set("resourceDir", "c:\\cache\\resources")
cfg.Set("cacheDir", "c:\\cache\\thecache")
} else {
cfg.Set("resourceDir", "/cache/resources")
cfg.Set("cacheDir", "/cache/thecache")
}
cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7)
imgConfig := decoded[filecache.CacheKeyImages]
jsonConfig := decoded[filecache.CacheKeyGetJSON]
if runtime.GOOS == "windows" {
c.Assert(imgConfig.DirCompiled, qt.Equals, filepath.FromSlash("_gen/images"))
} else {
c.Assert(imgConfig.DirCompiled, qt.Equals, "_gen/images")
c.Assert(jsonConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson")
}
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
}

View file

@ -1,106 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package filecache_test
import (
"path/filepath"
"testing"
"time"
"github.com/bep/logg"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
)
// See issue #10781. That issue wouldn't have been triggered if we kept
// the empty root directories (e.g. _resources/gen/images).
// It's still an upstream Go issue that we also need to handle, but
// this is a test for the first part.
func TestPruneShouldPreserveEmptyCacheRoots(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
-- content/_index.md --
---
title: "Home"
---
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t, TxtarString: files, RunGC: true, NeedsOsFS: true},
).Build()
_, err := b.H.BaseFs.ResourcesCache.Stat(filepath.Join("_gen", "images"))
b.Assert(err, qt.IsNil)
}
func TestPruneImages(t *testing.T) {
if htesting.IsCI() {
// TODO(bep)
t.Skip("skip flaky test on CI server")
}
t.Skip("skip flaky test")
files := `
-- hugo.toml --
baseURL = "https://example.com"
[caches]
[caches.images]
maxAge = "200ms"
dir = ":resourceDir/_gen"
-- content/_index.md --
---
title: "Home"
---
-- assets/a/pixel.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- layouts/index.html --
{{ warnf "HOME!" }}
{{ $img := resources.GetMatch "**.png" }}
{{ $img = $img.Resize "3x3" }}
{{ $img.RelPermalink }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: logg.LevelInfo},
).Build()
b.Assert(b.GCCount, qt.Equals, 0)
b.Assert(b.H, qt.IsNotNil)
imagesCacheDir := filepath.Join("_gen", "images")
_, err := b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir)
b.Assert(err, qt.IsNil)
// TODO(bep) we need a way to test full rebuilds.
// For now, just sleep a little so the cache elements expires.
time.Sleep(500 * time.Millisecond)
b.RenameFile("assets/a/pixel.png", "assets/b/pixel2.png").Build()
b.Assert(b.GCCount, qt.Equals, 1)
// Build it again to GC the empty a dir.
b.Build()
_, err = b.H.BaseFs.ResourcesCache.Stat(filepath.Join(imagesCacheDir, "a"))
b.Assert(err, qt.Not(qt.IsNil))
_, err = b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir)
b.Assert(err, qt.IsNil)
}

View file

@ -1,137 +0,0 @@
// 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 filecache
import (
"fmt"
"io"
"os"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
)
// Prune removes expired and unused items from this cache.
// The last one requires a full build so the cache usage can be tracked.
// Note that we operate directly on the filesystem here, so this is not
// thread safe.
func (c Caches) Prune() (int, error) {
counter := 0
for k, cache := range c {
count, err := cache.Prune(false)
counter += count
if err != nil {
if herrors.IsNotExist(err) {
continue
}
return counter, fmt.Errorf("failed to prune cache %q: %w", k, err)
}
}
return counter, nil
}
// Prune removes expired and unused items from this cache.
// If force is set, everything will be removed not considering expiry time.
func (c *Cache) Prune(force bool) (int, error) {
if c.pruneAllRootDir != "" {
return c.pruneRootDir(force)
}
if err := c.init(); err != nil {
return 0, err
}
counter := 0
err := afero.Walk(c.Fs, "", func(name string, info os.FileInfo, err error) error {
if info == nil {
return nil
}
name = cleanID(name)
if info.IsDir() {
f, err := c.Fs.Open(name)
if err != nil {
// This cache dir may not exist.
return nil
}
_, err = f.Readdirnames(1)
f.Close()
if err == io.EOF {
// Empty dir.
if name == "." {
// e.g. /_gen/images -- keep it even if empty.
err = nil
} else {
err = c.Fs.Remove(name)
}
}
if err != nil && !herrors.IsNotExist(err) {
return err
}
return nil
}
shouldRemove := force || c.isExpired(info.ModTime())
if !shouldRemove && len(c.nlocker.seen) > 0 {
// Remove it if it's not been touched/used in the last build.
_, seen := c.nlocker.seen[name]
shouldRemove = !seen
}
if shouldRemove {
err := c.Fs.Remove(name)
if err == nil {
counter++
}
if err != nil && !herrors.IsNotExist(err) {
return err
}
}
return nil
})
return counter, err
}
func (c *Cache) pruneRootDir(force bool) (int, error) {
if err := c.init(); err != nil {
return 0, err
}
info, err := c.Fs.Stat(c.pruneAllRootDir)
if err != nil {
if herrors.IsNotExist(err) {
return 0, nil
}
return 0, err
}
if !force && !c.isExpired(info.ModTime()) {
return 0, nil
}
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, c.pruneAllRootDir)
}

View file

@ -1,111 +0,0 @@
// 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 filecache_test
import (
"fmt"
"testing"
"time"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/spf13/afero"
qt "github.com/frankban/quicktest"
)
func TestPrune(t *testing.T) {
t.Parallel()
c := qt.New(t)
configStr := `
resourceDir = "myresources"
contentDir = "content"
dataDir = "data"
i18nDir = "i18n"
layoutDir = "layouts"
assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.getjson]
maxAge = "200ms"
dir = "/cache/c"
[caches.getcsv]
maxAge = "200ms"
dir = "/cache/d"
[caches.assets]
maxAge = "200ms"
dir = ":resourceDir/_gen"
[caches.images]
maxAge = "200ms"
dir = ":resourceDir/_gen"
`
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
msg := qt.Commentf("cache: %s", name)
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
cache := caches[name]
for i := range 10 {
id := fmt.Sprintf("i%d", i)
cache.GetOrCreateBytes(id, func() ([]byte, error) {
return []byte("abc"), nil
})
if i == 4 {
// This will expire the first 5
time.Sleep(201 * time.Millisecond)
}
}
count, err := caches.Prune()
c.Assert(err, qt.IsNil)
c.Assert(count, qt.Equals, 5, msg)
for i := range 10 {
id := fmt.Sprintf("i%d", i)
v := cache.GetString(id)
if i < 5 {
c.Assert(v, qt.Equals, "")
} else {
c.Assert(v, qt.Equals, "abc")
}
}
caches, err = filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
cache = caches[name]
// Touch one and then prune.
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
return []byte("abc"), nil
})
count, err = caches.Prune()
c.Assert(err, qt.IsNil)
c.Assert(count, qt.Equals, 4)
// Now only the i5 should be left.
for i := range 10 {
id := fmt.Sprintf("i%d", i)
v := cache.GetString(id)
if i != 5 {
c.Assert(v, qt.Equals, "")
} else {
c.Assert(v, qt.Equals, "abc")
}
}
}
}

View file

@ -1,276 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package filecache_test
import (
"errors"
"fmt"
"io"
"strings"
"sync"
"testing"
"time"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
qt "github.com/frankban/quicktest"
)
func TestFileCache(t *testing.T) {
t.Parallel()
c := qt.New(t)
tempWorkingDir := t.TempDir()
tempCacheDir := t.TempDir()
osfs := afero.NewOsFs()
for _, test := range []struct {
cacheDir string
workingDir string
}{
// Run with same dirs twice to make sure that works.
{tempCacheDir, tempWorkingDir},
{tempCacheDir, tempWorkingDir},
} {
configStr := `
workingDir = "WORKING_DIR"
resourceDir = "resources"
cacheDir = "CACHEDIR"
contentDir = "content"
dataDir = "data"
i18nDir = "i18n"
layoutDir = "layouts"
assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.getJSON]
maxAge = "10h"
dir = ":cacheDir/c"
`
winPathSep := "\\\\"
replacer := strings.NewReplacer("CACHEDIR", test.cacheDir, "WORKING_DIR", test.workingDir)
configStr = replacer.Replace(configStr)
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
p := newPathsSpec(t, osfs, configStr)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
cache := caches.Get("GetJSON")
c.Assert(cache, qt.Not(qt.IsNil))
cache = caches.Get("Images")
c.Assert(cache, qt.Not(qt.IsNil))
rf := func(s string) func() (io.ReadCloser, error) {
return func() (io.ReadCloser, error) {
return struct {
io.ReadSeeker
io.Closer
}{
strings.NewReader(s),
io.NopCloser(nil),
}, nil
}
}
bf := func() ([]byte, error) {
return []byte("bcd"), nil
}
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
for range 2 {
info, r, err := ca.GetOrCreate("a", rf("abc"))
c.Assert(err, qt.IsNil)
c.Assert(r, qt.Not(qt.IsNil))
c.Assert(info.Name, qt.Equals, "a")
b, _ := io.ReadAll(r)
r.Close()
c.Assert(string(b), qt.Equals, "abc")
info, b, err = ca.GetOrCreateBytes("b", bf)
c.Assert(err, qt.IsNil)
c.Assert(r, qt.Not(qt.IsNil))
c.Assert(info.Name, qt.Equals, "b")
c.Assert(string(b), qt.Equals, "bcd")
_, b, err = ca.GetOrCreateBytes("a", bf)
c.Assert(err, qt.IsNil)
c.Assert(string(b), qt.Equals, "abc")
_, r, err = ca.GetOrCreate("a", rf("bcd"))
c.Assert(err, qt.IsNil)
b, _ = io.ReadAll(r)
r.Close()
c.Assert(string(b), qt.Equals, "abc")
}
}
c.Assert(caches.Get("getJSON"), qt.Not(qt.IsNil))
info, w, err := caches.ImageCache().WriteCloser("mykey")
c.Assert(err, qt.IsNil)
c.Assert(info.Name, qt.Equals, "mykey")
io.WriteString(w, "Hugo is great!")
w.Close()
c.Assert(caches.ImageCache().GetString("mykey"), qt.Equals, "Hugo is great!")
info, r, err := caches.ImageCache().Get("mykey")
c.Assert(err, qt.IsNil)
c.Assert(r, qt.Not(qt.IsNil))
c.Assert(info.Name, qt.Equals, "mykey")
b, _ := io.ReadAll(r)
r.Close()
c.Assert(string(b), qt.Equals, "Hugo is great!")
info, b, err = caches.ImageCache().GetBytes("mykey")
c.Assert(err, qt.IsNil)
c.Assert(info.Name, qt.Equals, "mykey")
c.Assert(string(b), qt.Equals, "Hugo is great!")
}
}
func TestFileCacheConcurrent(t *testing.T) {
t.Parallel()
c := qt.New(t)
configStr := `
resourceDir = "myresources"
contentDir = "content"
dataDir = "data"
i18nDir = "i18n"
layoutDir = "layouts"
assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.getjson]
maxAge = "1s"
dir = "/cache/c"
`
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
const cacheName = "getjson"
filenameData := func(i int) (string, string) {
data := fmt.Sprintf("data: %d", i)
filename := fmt.Sprintf("file%d", i)
return filename, data
}
var wg sync.WaitGroup
for i := range 50 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for range 20 {
ca := caches.Get(cacheName)
c.Assert(ca, qt.Not(qt.IsNil))
filename, data := filenameData(i)
_, r, err := ca.GetOrCreate(filename, func() (io.ReadCloser, error) {
return hugio.ToReadCloser(strings.NewReader(data)), nil
})
c.Assert(err, qt.IsNil)
b, _ := io.ReadAll(r)
r.Close()
c.Assert(string(b), qt.Equals, data)
// Trigger some expiration.
time.Sleep(50 * time.Millisecond)
}
}(i)
}
wg.Wait()
}
func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
t.Parallel()
c := qt.New(t)
var result string
rf := func(failLevel int) func(info filecache.ItemInfo, r io.ReadSeeker) error {
return func(info filecache.ItemInfo, r io.ReadSeeker) error {
if failLevel > 0 {
if failLevel > 1 {
return filecache.ErrFatal
}
return errors.New("fail")
}
b, _ := io.ReadAll(r)
result = string(b)
return nil
}
}
bf := func(s string) func(info filecache.ItemInfo, w io.WriteCloser) error {
return func(info filecache.ItemInfo, w io.WriteCloser) error {
defer w.Close()
result = s
_, err := w.Write([]byte(s))
return err
}
}
cache := filecache.NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
const id = "a32"
_, err := cache.ReadOrCreate(id, rf(0), bf("v1"))
c.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, "v1")
_, err = cache.ReadOrCreate(id, rf(0), bf("v2"))
c.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, "v1")
_, err = cache.ReadOrCreate(id, rf(1), bf("v3"))
c.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, "v3")
_, err = cache.ReadOrCreate(id, rf(2), bf("v3"))
c.Assert(err, qt.Equals, filecache.ErrFatal)
}
func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec {
c := qt.New(t)
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
acfg := testconfig.GetTestConfig(fs, cfg)
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil)
c.Assert(err, qt.IsNil)
return p
}

View file

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

View file

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

View file

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

80
cache/partitioned_lazy_cache.go vendored Normal file
View file

@ -0,0 +1,80 @@
// Copyright 2017-present 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 cache
import (
"sync"
)
// Partition represents a cache partition where Load is the callback
// for when the partition is needed.
type Partition struct {
Key string
Load func() (map[string]interface{}, error)
}
type lazyPartition struct {
initSync sync.Once
cache map[string]interface{}
load func() (map[string]interface{}, error)
}
func (l *lazyPartition) init() error {
var err error
l.initSync.Do(func() {
var c map[string]interface{}
c, err = l.load()
l.cache = c
})
return err
}
// PartitionedLazyCache is a lazily loaded cache paritioned by a supplied string key.
type PartitionedLazyCache struct {
partitions map[string]*lazyPartition
}
// NewPartitionedLazyCache creates a new NewPartitionedLazyCache with the supplied
// partitions.
func NewPartitionedLazyCache(partitions ...Partition) *PartitionedLazyCache {
lazyPartitions := make(map[string]*lazyPartition, len(partitions))
for _, partition := range partitions {
lazyPartitions[partition.Key] = &lazyPartition{load: partition.Load}
}
cache := &PartitionedLazyCache{partitions: lazyPartitions}
return cache
}
// Get initializes the partition if not already done so, then looks up the given
// key in the given partition, returns nil if no value found.
func (c *PartitionedLazyCache) Get(partition, key string) (interface{}, error) {
p, found := c.partitions[partition]
if !found {
return nil, nil
}
if err := p.init(); err != nil {
return nil, err
}
if v, found := p.cache[key]; found {
return v, nil
}
return nil, nil
}

138
cache/partitioned_lazy_cache_test.go vendored Normal file
View file

@ -0,0 +1,138 @@
// Copyright 2017-present 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 cache
import (
"errors"
"sync"
"testing"
"github.com/stretchr/testify/require"
)
func TestNewPartitionedLazyCache(t *testing.T) {
t.Parallel()
assert := require.New(t)
p1 := Partition{
Key: "p1",
Load: func() (map[string]interface{}, error) {
return map[string]interface{}{
"p1_1": "p1v1",
"p1_2": "p1v2",
"p1_nil": nil,
}, nil
},
}
p2 := Partition{
Key: "p2",
Load: func() (map[string]interface{}, error) {
return map[string]interface{}{
"p2_1": "p2v1",
"p2_2": "p2v2",
"p2_3": "p2v3",
}, nil
},
}
cache := NewPartitionedLazyCache(p1, p2)
v, err := cache.Get("p1", "p1_1")
assert.NoError(err)
assert.Equal("p1v1", v)
v, err = cache.Get("p1", "p2_1")
assert.NoError(err)
assert.Nil(v)
v, err = cache.Get("p1", "p1_nil")
assert.NoError(err)
assert.Nil(v)
v, err = cache.Get("p2", "p2_3")
assert.NoError(err)
assert.Equal("p2v3", v)
v, err = cache.Get("doesnotexist", "p1_1")
assert.NoError(err)
assert.Nil(v)
v, err = cache.Get("p1", "doesnotexist")
assert.NoError(err)
assert.Nil(v)
errorP := Partition{
Key: "p3",
Load: func() (map[string]interface{}, error) {
return nil, errors.New("Failed")
},
}
cache = NewPartitionedLazyCache(errorP)
v, err = cache.Get("p1", "doesnotexist")
assert.NoError(err)
assert.Nil(v)
_, err = cache.Get("p3", "doesnotexist")
assert.Error(err)
}
func TestConcurrentPartitionedLazyCache(t *testing.T) {
t.Parallel()
assert := require.New(t)
var wg sync.WaitGroup
p1 := Partition{
Key: "p1",
Load: func() (map[string]interface{}, error) {
return map[string]interface{}{
"p1_1": "p1v1",
"p1_2": "p1v2",
"p1_nil": nil,
}, nil
},
}
p2 := Partition{
Key: "p2",
Load: func() (map[string]interface{}, error) {
return map[string]interface{}{
"p2_1": "p2v1",
"p2_2": "p2v2",
"p2_3": "p2v3",
}, nil
},
}
cache := NewPartitionedLazyCache(p1, p2)
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
v, err := cache.Get("p1", "p1_1")
assert.NoError(err)
assert.Equal("p1v1", v)
}
}()
}
wg.Wait()
}

View file

@ -1,2 +0,0 @@
#!/usr/bin/env bash
diff <(gofmt -d .) <(printf '')

View file

@ -1,540 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Some functions in this file (see comments) is based on the Go source code,
// copyright The Go Authors and governed by a BSD-style license.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package codegen contains helpers for code generation.
package codegen
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"slices"
"sort"
"strings"
"sync"
)
// Make room for insertions
const weightWidth = 1000
// NewInspector creates a new Inspector given a source root.
func NewInspector(root string) *Inspector {
return &Inspector{ProjectRootDir: root}
}
// Inspector provides methods to help code generation. It uses a combination
// of reflection and source code AST to do the heavy lifting.
type Inspector struct {
ProjectRootDir string
init sync.Once
// Determines method order. Go's reflect sorts lexicographically, so
// we must parse the source to preserve this order.
methodWeight map[string]map[string]int
}
// MethodsFromTypes create a method set from the include slice, excluding any
// method in exclude.
func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.Type) Methods {
c.parseSource()
var methods Methods
excludes := make(map[string]bool)
if len(exclude) > 0 {
for _, m := range c.MethodsFromTypes(exclude, nil) {
excludes[m.Name] = true
}
}
// There may be overlapping interfaces in types. Do a simple check for now.
seen := make(map[string]bool)
nameAndPackage := func(t reflect.Type) (string, string) {
var name, pkg string
isPointer := t.Kind() == reflect.Ptr
if isPointer {
t = t.Elem()
}
pkgPrefix := ""
if pkgPath := t.PkgPath(); pkgPath != "" {
pkgPath = strings.TrimSuffix(pkgPath, "/")
_, shortPath := path.Split(pkgPath)
pkgPrefix = shortPath + "."
pkg = pkgPath
}
name = t.Name()
if name == "" {
// interface{}
name = t.String()
}
if isPointer {
pkgPrefix = "*" + pkgPrefix
}
name = pkgPrefix + name
return name, pkg
}
for _, t := range include {
for i := range t.NumMethod() {
m := t.Method(i)
if excludes[m.Name] || seen[m.Name] {
continue
}
seen[m.Name] = true
if m.PkgPath != "" {
// Not exported
continue
}
numIn := m.Type.NumIn()
ownerName, _ := nameAndPackage(t)
method := Method{Owner: t, OwnerName: ownerName, Name: m.Name}
for i := range numIn {
in := m.Type.In(i)
name, pkg := nameAndPackage(in)
if pkg != "" {
method.Imports = append(method.Imports, pkg)
}
method.In = append(method.In, name)
}
numOut := m.Type.NumOut()
if numOut > 0 {
for i := range numOut {
out := m.Type.Out(i)
name, pkg := nameAndPackage(out)
if pkg != "" {
method.Imports = append(method.Imports, pkg)
}
method.Out = append(method.Out, name)
}
}
methods = append(methods, method)
}
}
sort.SliceStable(methods, func(i, j int) bool {
mi, mj := methods[i], methods[j]
wi := c.methodWeight[mi.OwnerName][mi.Name]
wj := c.methodWeight[mj.OwnerName][mj.Name]
if wi == wj {
return mi.Name < mj.Name
}
return wi < wj
})
return methods
}
func (c *Inspector) parseSource() {
c.init.Do(func() {
if !strings.Contains(c.ProjectRootDir, "hugo") {
panic("dir must be set to the Hugo root")
}
c.methodWeight = make(map[string]map[string]int)
dirExcludes := regexp.MustCompile("docs|examples")
fileExcludes := regexp.MustCompile("autogen")
var filenames []string
filepath.Walk(c.ProjectRootDir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
if dirExcludes.MatchString(info.Name()) {
return filepath.SkipDir
}
}
if !strings.HasSuffix(path, ".go") || fileExcludes.MatchString(path) {
return nil
}
filenames = append(filenames, path)
return nil
})
for _, filename := range filenames {
pkg := c.packageFromPath(filename)
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
panic(err)
}
ast.Inspect(node, func(n ast.Node) bool {
switch t := n.(type) {
case *ast.TypeSpec:
if t.Name.IsExported() {
switch it := t.Type.(type) {
case *ast.InterfaceType:
iface := pkg + "." + t.Name.Name
methodNames := collectMethodsRecursive(pkg, it.Methods.List)
weights := make(map[string]int)
weight := weightWidth
for _, name := range methodNames {
weights[name] = weight
weight += weightWidth
}
c.methodWeight[iface] = weights
}
}
}
return true
})
}
// Complement
for _, v1 := range c.methodWeight {
for k2, w := range v1 {
if v, found := c.methodWeight[k2]; found {
for k3, v3 := range v {
v1[k3] = (v3 / weightWidth) + w
}
}
}
}
})
}
func (c *Inspector) packageFromPath(p string) string {
p = filepath.ToSlash(p)
base := path.Base(p)
if !strings.Contains(base, ".") {
return base
}
return path.Base(strings.TrimSuffix(p, base))
}
// Method holds enough information about it to recreate it.
type Method struct {
// The interface we extracted this method from.
Owner reflect.Type
// String version of the above, on the form PACKAGE.NAME, e.g.
// page.Page
OwnerName string
// Method name.
Name string
// Imports needed to satisfy the method signature.
Imports []string
// Argument types, including any package prefix, e.g. string, int, interface{},
// net.Url
In []string
// Return types.
Out []string
}
// Declaration creates a method declaration (without any body) for the given receiver.
func (m Method) Declaration(receiver string) string {
return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStr())
}
// DeclarationNamed creates a method declaration (without any body) for the given receiver
// with named return values.
func (m Method) DeclarationNamed(receiver string) string {
return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStrNamed())
}
// Delegate creates a delegate call string.
func (m Method) Delegate(receiver, delegate string) string {
ret := ""
if len(m.Out) > 0 {
ret = "return "
}
return fmt.Sprintf("%s%s.%s.%s%s", ret, receiverShort(receiver), delegate, m.Name, m.inOutStr())
}
func (m Method) String() string {
return m.Name + m.inStr() + " " + m.outStr() + "\n"
}
func (m Method) inOutStr() string {
if len(m.In) == 0 {
return "()"
}
args := make([]string, len(m.In))
for i := range args {
args[i] = fmt.Sprintf("arg%d", i)
}
return "(" + strings.Join(args, ", ") + ")"
}
func (m Method) inStr() string {
if len(m.In) == 0 {
return "()"
}
args := make([]string, len(m.In))
for i := range args {
args[i] = fmt.Sprintf("arg%d %s", i, m.In[i])
}
return "(" + strings.Join(args, ", ") + ")"
}
func (m Method) outStr() string {
if len(m.Out) == 0 {
return ""
}
if len(m.Out) == 1 {
return m.Out[0]
}
return "(" + strings.Join(m.Out, ", ") + ")"
}
func (m Method) outStrNamed() string {
if len(m.Out) == 0 {
return ""
}
outs := make([]string, len(m.Out))
for i := range outs {
outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i])
}
return "(" + strings.Join(outs, ", ") + ")"
}
// Methods represents a list of methods for one or more interfaces.
// The order matches the defined order in their source file(s).
type Methods []Method
// Imports returns a sorted list of package imports needed to satisfy the
// signatures of all methods.
func (m Methods) Imports() []string {
var pkgImports []string
for _, method := range m {
pkgImports = append(pkgImports, method.Imports...)
}
if len(pkgImports) > 0 {
pkgImports = uniqueNonEmptyStrings(pkgImports)
sort.Strings(pkgImports)
}
return pkgImports
}
// ToMarshalJSON creates a MarshalJSON method for these methods. Any method name
// matching any of the regexps in excludes will be ignored.
func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (string, []string) {
var sb strings.Builder
r := receiverShort(receiver)
what := firstToUpper(trimAsterisk(receiver))
pgkName := path.Base(pkgPath)
fmt.Fprintf(&sb, "func Marshal%sToJSON(%s %s) ([]byte, error) {\n", what, r, receiver)
var methods Methods
excludeRes := make([]*regexp.Regexp, len(excludes))
for i, exclude := range excludes {
excludeRes[i] = regexp.MustCompile(exclude)
}
for _, method := range m {
// Exclude methods with arguments and incompatible return values
if len(method.In) > 0 || len(method.Out) == 0 || len(method.Out) > 2 {
continue
}
if len(method.Out) == 2 {
if method.Out[1] != "error" {
continue
}
}
for _, re := range excludeRes {
if re.MatchString(method.Name) {
continue
}
}
methods = append(methods, method)
}
for _, method := range methods {
varn := varName(method.Name)
if len(method.Out) == 1 {
fmt.Fprintf(&sb, "\t%s := %s.%s()\n", varn, r, method.Name)
} else {
fmt.Fprintf(&sb, "\t%s, err := %s.%s()\n", varn, r, method.Name)
fmt.Fprint(&sb, "\tif err != nil {\n\t\treturn nil, err\n\t}\n")
}
}
fmt.Fprint(&sb, "\n\ts := struct {\n")
for _, method := range methods {
fmt.Fprintf(&sb, "\t\t%s %s\n", method.Name, typeName(method.Out[0], pgkName))
}
fmt.Fprint(&sb, "\n\t}{\n")
for _, method := range methods {
varn := varName(method.Name)
fmt.Fprintf(&sb, "\t\t%s: %s,\n", method.Name, varn)
}
fmt.Fprint(&sb, "\n\t}\n\n")
fmt.Fprint(&sb, "\treturn json.Marshal(&s)\n}")
pkgImports := append(methods.Imports(), "encoding/json")
if pkgPath != "" {
// Exclude self
for i, pkgImp := range pkgImports {
if pkgImp == pkgPath {
pkgImports = slices.Delete(pkgImports, i, i+1)
}
}
}
return sb.String(), pkgImports
}
func collectMethodsRecursive(pkg string, f []*ast.Field) []string {
var methodNames []string
for _, m := range f {
if m.Names != nil {
methodNames = append(methodNames, m.Names[0].Name)
continue
}
if ident, ok := m.Type.(*ast.Ident); ok && ident.Obj != nil {
switch tt := ident.Obj.Decl.(*ast.TypeSpec).Type.(type) {
case *ast.InterfaceType:
// Embedded interface
methodNames = append(
methodNames,
collectMethodsRecursive(
pkg,
tt.Methods.List)...)
}
} else {
// Embedded, but in a different file/package. Return the
// package.Name and deal with that later.
name := packageName(m.Type)
if !strings.Contains(name, ".") {
// Assume current package
name = pkg + "." + name
}
methodNames = append(methodNames, name)
}
}
return methodNames
}
func firstToLower(name string) string {
return strings.ToLower(name[:1]) + name[1:]
}
func firstToUpper(name string) string {
return strings.ToUpper(name[:1]) + name[1:]
}
func packageName(e ast.Expr) string {
switch tp := e.(type) {
case *ast.Ident:
return tp.Name
case *ast.SelectorExpr:
return fmt.Sprintf("%s.%s", packageName(tp.X), packageName(tp.Sel))
}
return ""
}
func receiverShort(receiver string) string {
return strings.ToLower(trimAsterisk(receiver))[:1]
}
func trimAsterisk(name string) string {
return strings.TrimPrefix(name, "*")
}
func typeName(name, pkg string) string {
return strings.TrimPrefix(name, pkg+".")
}
func uniqueNonEmptyStrings(s []string) []string {
var unique []string
set := map[string]any{}
for _, val := range s {
if val == "" {
continue
}
if _, ok := set[val]; !ok {
unique = append(unique, val)
set[val] = val
}
}
return unique
}
func varName(name string) string {
name = firstToLower(name)
// Adjust some reserved keywords, see https://golang.org/ref/spec#Keywords
switch name {
case "type":
name = "typ"
case "package":
name = "pkg"
// Not reserved, but syntax highlighters has it as a keyword.
case "len":
name = "length"
}
return name
}

View file

@ -1,20 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// 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 codegen
type IEmbed interface {
MethodEmbed3(s string) string
MethodEmbed1() string
MethodEmbed2()
}

View file

@ -1,96 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// 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 codegen
import (
"fmt"
"net"
"os"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/herrors"
)
func TestMethods(t *testing.T) {
var (
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
zeroI = reflect.TypeOf((*I)(nil)).Elem()
)
dir, _ := os.Getwd()
insp := NewInspector(dir)
t.Run("MethodsFromTypes", func(t *testing.T) {
c := qt.New(t)
methods := insp.MethodsFromTypes([]reflect.Type{zeroI}, nil)
methodsStr := fmt.Sprint(methods)
c.Assert(methodsStr, qt.Contains, "Method1(arg0 herrors.ErrorContext)")
c.Assert(methodsStr, qt.Contains, "Method7() interface {}")
c.Assert(methodsStr, qt.Contains, "Method0() string\n Method4() string")
c.Assert(methodsStr, qt.Contains, "MethodEmbed3(arg0 string) string\n MethodEmbed1() string")
c.Assert(methods.Imports(), qt.Contains, "github.com/gohugoio/hugo/common/herrors")
})
t.Run("EmbedOnly", func(t *testing.T) {
c := qt.New(t)
methods := insp.MethodsFromTypes([]reflect.Type{zeroIEOnly}, nil)
methodsStr := fmt.Sprint(methods)
c.Assert(methodsStr, qt.Contains, "MethodEmbed3(arg0 string) string")
})
t.Run("ToMarshalJSON", func(t *testing.T) {
c := qt.New(t)
m, pkg := insp.MethodsFromTypes(
[]reflect.Type{zeroI},
[]reflect.Type{zeroIE}).ToMarshalJSON("*page", "page")
c.Assert(m, qt.Contains, "method6 := p.Method6()")
c.Assert(m, qt.Contains, "Method0: method0,")
c.Assert(m, qt.Contains, "return json.Marshal(&s)")
c.Assert(pkg, qt.Contains, "github.com/gohugoio/hugo/common/herrors")
c.Assert(pkg, qt.Contains, "encoding/json")
fmt.Println(pkg)
})
}
type I interface {
IEmbed
Method0() string
Method4() string
Method1(myerr herrors.ErrorContext)
Method3(myint int, mystring string)
Method5() (string, error)
Method6() *net.IP
Method7() any
Method8() herrors.ErrorContext
method2()
method9() os.FileInfo
}
type IEOnly interface {
IEmbed
}

112
commands/benchmark.go Normal file
View file

@ -0,0 +1,112 @@
// 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 (
"os"
"runtime"
"runtime/pprof"
"time"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var (
benchmarkTimes int
cpuProfileFile string
memProfileFile string
)
var benchmarkCmd = &cobra.Command{
Use: "benchmark",
Short: "Benchmark Hugo by building a site a number of times.",
Long: `Hugo can build a site many times over and analyze the running process
creating a benchmark.`,
}
func init() {
initHugoBuilderFlags(benchmarkCmd)
initBenchmarkBuildingFlags(benchmarkCmd)
benchmarkCmd.Flags().StringVar(&cpuProfileFile, "cpuprofile", "", "path/filename for the CPU profile file")
benchmarkCmd.Flags().StringVar(&memProfileFile, "memprofile", "", "path/filename for the memory profile file")
benchmarkCmd.Flags().IntVarP(&benchmarkTimes, "count", "n", 13, "number of times to build the site")
benchmarkCmd.RunE = benchmark
}
func benchmark(cmd *cobra.Command, args []string) error {
cfg, err := InitializeConfig(benchmarkCmd)
if err != nil {
return err
}
c, err := newCommandeer(cfg)
if err != nil {
return err
}
var memProf *os.File
if memProfileFile != "" {
memProf, err = os.Create(memProfileFile)
if err != nil {
return err
}
}
var cpuProf *os.File
if cpuProfileFile != "" {
cpuProf, err = os.Create(cpuProfileFile)
if err != nil {
return err
}
}
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
memAllocated := memStats.TotalAlloc
mallocs := memStats.Mallocs
if cpuProf != nil {
pprof.StartCPUProfile(cpuProf)
}
t := time.Now()
for i := 0; i < benchmarkTimes; i++ {
if err = c.resetAndBuildSites(false); err != nil {
return err
}
}
totalTime := time.Since(t)
if memProf != nil {
pprof.WriteHeapProfile(memProf)
memProf.Close()
}
if cpuProf != nil {
pprof.StopCPUProfile()
cpuProf.Close()
}
runtime.ReadMemStats(&memStats)
totalMemAllocated := memStats.TotalAlloc - memAllocated
totalMallocs := memStats.Mallocs - mallocs
jww.FEEDBACK.Println()
jww.FEEDBACK.Printf("Average time per operation: %vms\n", int(1000*totalTime.Seconds()/float64(benchmarkTimes)))
jww.FEEDBACK.Printf("Average memory allocated per operation: %vkB\n", totalMemAllocated/uint64(benchmarkTimes)/1024)
jww.FEEDBACK.Printf("Average allocations per operation: %v\n", totalMallocs/uint64(benchmarkTimes))
return nil
}

23
commands/check.go Normal file
View file

@ -0,0 +1,23 @@
// 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 (
"github.com/spf13/cobra"
)
var checkCmd = &cobra.Command{
Use: "check",
Short: "Contains some verification checks",
}

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2017 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,666 +14,49 @@
package commands
import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"go.uber.org/automaxprocs/maxprocs"
"github.com/bep/clocks"
"github.com/bep/lazycache"
"github.com/bep/logg"
"github.com/bep/overlayfs"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
var errHelp = errors.New("help requested")
type commandeer struct {
*deps.DepsCfg
pathSpec *helpers.PathSpec
configured bool
}
// Execute executes a command.
func Execute(args []string) error {
// Default GOMAXPROCS to be CPU limit aware, still respecting GOMAXPROCS env.
maxprocs.Set()
x, err := newExec()
func (c *commandeer) Set(key string, value interface{}) {
if c.configured {
panic("commandeer cannot be changed")
}
c.Cfg.Set(key, value)
}
// PathSpec lazily creates a new PathSpec, as all the paths must
// be configured before it is created.
func (c *commandeer) PathSpec() *helpers.PathSpec {
c.configured = true
return c.pathSpec
}
func (c *commandeer) initFs(fs *hugofs.Fs) error {
c.DepsCfg.Fs = fs
ps, err := helpers.NewPathSpec(fs, c.Cfg)
if err != nil {
return err
}
args = mapLegacyArgs(args)
cd, err := x.Execute(context.Background(), args)
if cd != nil {
if closer, ok := cd.Root.Command.(types.Closer); ok {
closer.Close()
}
}
if err != nil {
if err == errHelp {
cd.CobraCommand.Help()
fmt.Println()
return nil
}
if simplecobra.IsCommandError(err) {
// Print the help, but also return the error to fail the command.
cd.CobraCommand.Help()
fmt.Println()
}
}
return err
}
type commonConfig struct {
mu *sync.Mutex
configs *allconfig.Configs
cfg config.Provider
fs *hugofs.Fs
}
type configKey struct {
counter int32
ignoreModulesDoesNotExists bool
}
// This is the root command.
type rootCommand struct {
Printf func(format string, v ...any)
Println func(a ...any)
StdOut io.Writer
StdErr io.Writer
logger loggers.Logger
// The main cache busting key for the caches below.
configVersionID atomic.Int32
// Some, but not all commands need access to these.
// Some needs more than one, so keep them in a small cache.
commonConfigs *lazycache.Cache[configKey, *commonConfig]
hugoSites *lazycache.Cache[configKey, *hugolib.HugoSites]
// changesFromBuild received from Hugo in watch mode.
changesFromBuild chan []identity.Identity
commands []simplecobra.Commander
// Flags
source string
buildWatch bool
environment string
// Common build flags.
baseURL string
gc bool
poll string
forceSyncStatic bool
// Profile flags (for debugging of performance problems)
cpuprofile string
memprofile string
mutexprofile string
traceprofile string
printm bool
logLevel string
quiet bool
devMode bool // Hidden flag.
renderToMemory bool
cfgFile string
cfgDir string
}
func (r *rootCommand) isVerbose() bool {
return r.logger.Level() <= logg.LevelInfo
}
func (r *rootCommand) Close() error {
if r.hugoSites != nil {
r.hugoSites.DeleteFunc(func(key configKey, value *hugolib.HugoSites) bool {
if value != nil {
value.Close()
}
return false
})
}
c.pathSpec = ps
return nil
}
func (r *rootCommand) Build(cd *simplecobra.Commandeer, bcfg hugolib.BuildCfg, cfg config.Provider) (*hugolib.HugoSites, error) {
h, err := r.Hugo(cfg)
func newCommandeer(cfg *deps.DepsCfg) (*commandeer, error) {
l := cfg.Language
if l == nil {
l = helpers.NewDefaultLanguage(cfg.Cfg)
}
ps, err := helpers.NewPathSpec(cfg.Fs, l)
if err != nil {
return nil, err
}
if err := h.Build(bcfg); err != nil {
return nil, err
}
return h, nil
}
func (r *rootCommand) Commands() []simplecobra.Commander {
return r.commands
}
func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*commonConfig, error) {
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) {
fs := oldConf.fs
configs, err := allconfig.LoadConfig(
allconfig.ConfigSourceDescriptor{
Flags: oldConf.cfg,
Fs: fs.Source,
Filename: r.cfgFile,
ConfigDir: r.cfgDir,
Logger: r.logger,
Environment: r.environment,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
},
)
if err != nil {
return nil, err
}
if !configs.Base.C.Clock.IsZero() {
// TODO(bep) find a better place for this.
htime.Clock = clocks.Start(configs.Base.C.Clock)
}
return &commonConfig{
mu: oldConf.mu,
configs: configs,
cfg: oldConf.cfg,
fs: fs,
}, nil
})
return cc, err
}
func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*commonConfig, error) {
if cfg == nil {
panic("cfg must be set")
}
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) {
var dir string
if r.source != "" {
dir, _ = filepath.Abs(r.source)
} else {
dir, _ = os.Getwd()
}
if cfg == nil {
cfg = config.New()
}
if !cfg.IsSet("workingDir") {
cfg.Set("workingDir", dir)
} else {
if err := os.MkdirAll(cfg.GetString("workingDir"), 0o777); err != nil {
return nil, fmt.Errorf("failed to create workingDir: %w", err)
}
}
// Load the config first to allow publishDir to be configured in config file.
configs, err := allconfig.LoadConfig(
allconfig.ConfigSourceDescriptor{
Flags: cfg,
Fs: hugofs.Os,
Filename: r.cfgFile,
ConfigDir: r.cfgDir,
Environment: r.environment,
Logger: r.logger,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
},
)
if err != nil {
return nil, err
}
base := configs.Base
cfg.Set("publishDir", base.PublishDir)
cfg.Set("publishDirStatic", base.PublishDir)
cfg.Set("publishDirDynamic", base.PublishDir)
renderStaticToDisk := cfg.GetBool("renderStaticToDisk")
sourceFs := hugofs.Os
var destinationFs afero.Fs
if cfg.GetBool("renderToMemory") {
destinationFs = afero.NewMemMapFs()
if renderStaticToDisk {
// Hybrid, render dynamic content to Root.
cfg.Set("publishDirDynamic", "/")
} else {
// Rendering to memoryFS, publish to Root regardless of publishDir.
cfg.Set("publishDirDynamic", "/")
cfg.Set("publishDirStatic", "/")
}
} else {
destinationFs = hugofs.Os
}
fs := hugofs.NewFromSourceAndDestination(sourceFs, destinationFs, cfg)
if renderStaticToDisk {
dynamicFs := fs.PublishDir
publishDirStatic := cfg.GetString("publishDirStatic")
workingDir := cfg.GetString("workingDir")
absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
staticFs := hugofs.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
}
if !base.C.Clock.IsZero() {
// TODO(bep) find a better place for this.
htime.Clock = clocks.Start(configs.Base.C.Clock)
}
if base.PrintPathWarnings {
// Note that we only care about the "dynamic creates" here,
// so skip the static fs.
fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir)
}
commonConfig := &commonConfig{
mu: &sync.Mutex{},
configs: configs,
cfg: cfg,
fs: fs,
}
return commonConfig, nil
})
return cc, err
}
func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
k := configKey{counter: r.configVersionID.Load()}
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
depsCfg := r.newDepsConfig(conf)
return hugolib.NewHugoSites(depsCfg)
})
return h, err
}
func (r *rootCommand) Hugo(cfg config.Provider) (*hugolib.HugoSites, error) {
return r.getOrCreateHugo(cfg, false)
}
func (r *rootCommand) getOrCreateHugo(cfg config.Provider, ignoreModuleDoesNotExist bool) (*hugolib.HugoSites, error) {
k := configKey{counter: r.configVersionID.Load(), ignoreModulesDoesNotExists: ignoreModuleDoesNotExist}
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
conf, err := r.ConfigFromProvider(key, cfg)
if err != nil {
return nil, err
}
depsCfg := r.newDepsConfig(conf)
return hugolib.NewHugoSites(depsCfg)
})
return h, err
}
func (r *rootCommand) newDepsConfig(conf *commonConfig) deps.DepsCfg {
return deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, StdOut: r.logger.StdOut(), StdErr: r.logger.StdErr(), LogLevel: r.logger.Level(), ChangesFromBuild: r.changesFromBuild}
}
func (r *rootCommand) Name() string {
return "hugo"
}
func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
b := newHugoBuilder(r, nil)
if !r.buildWatch {
defer b.postBuild("Total", time.Now())
}
if err := b.loadConfig(cd, false); err != nil {
return err
}
err := func() error {
if r.buildWatch {
defer r.timeTrack(time.Now(), "Built")
}
err := b.build()
if err != nil {
return err
}
return nil
}()
if err != nil {
return err
}
if !r.buildWatch {
// Done.
return nil
}
watchDirs, err := b.getDirList()
if err != nil {
return err
}
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
for _, group := range watchGroups {
r.Printf("Watching for changes in %s\n", group)
}
watcher, err := b.newWatcher(r.poll, watchDirs...)
if err != nil {
return err
}
defer watcher.Close()
r.Println("Press Ctrl+C to stop")
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
return nil
}
func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
r.StdOut = os.Stdout
r.StdErr = os.Stderr
if r.quiet {
r.StdOut = io.Discard
r.StdErr = io.Discard
}
// Used by mkcert (server).
log.SetOutput(r.StdOut)
r.Printf = func(format string, v ...any) {
if !r.quiet {
fmt.Fprintf(r.StdOut, format, v...)
}
}
r.Println = func(a ...any) {
if !r.quiet {
fmt.Fprintln(r.StdOut, a...)
}
}
_, running := runner.Command.(*serverCommand)
var err error
r.logger, err = r.createLogger(running)
if err != nil {
return err
}
// Set up the global logger early to allow info deprecations during config load.
loggers.SetGlobalLogger(r.logger)
r.changesFromBuild = make(chan []identity.Identity, 10)
r.commonConfigs = lazycache.New(lazycache.Options[configKey, *commonConfig]{MaxEntries: 5})
// We don't want to keep stale HugoSites in memory longer than needed.
r.hugoSites = lazycache.New(lazycache.Options[configKey, *hugolib.HugoSites]{
MaxEntries: 1,
OnEvict: func(key configKey, value *hugolib.HugoSites) {
value.Close()
runtime.GC()
},
})
return nil
}
func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
level := logg.LevelWarn
if r.devMode {
level = logg.LevelTrace
} else {
if r.logLevel != "" {
switch strings.ToLower(r.logLevel) {
case "debug":
level = logg.LevelDebug
case "info":
level = logg.LevelInfo
case "warn", "warning":
level = logg.LevelWarn
case "error":
level = logg.LevelError
default:
return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel)
}
}
}
optsLogger := loggers.Options{
DistinctLevel: logg.LevelWarn,
Level: level,
StdOut: r.StdOut,
StdErr: r.StdErr,
StoreErrors: running,
}
return loggers.New(optsLogger), nil
}
func (r *rootCommand) resetLogs() {
r.logger.Reset()
loggers.Log().Reset()
}
// IsTestRun reports whether the command is running as a test.
func (r *rootCommand) IsTestRun() bool {
return os.Getenv("HUGO_TESTRUN") != ""
}
func (r *rootCommand) Init(cd *simplecobra.Commandeer) error {
return r.initRootCommand("", cd)
}
func (r *rootCommand) initRootCommand(subCommandName string, cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
commandName := "hugo"
if subCommandName != "" {
commandName = subCommandName
}
cmd.Use = fmt.Sprintf("%s [flags]", commandName)
cmd.Short = "Build your site"
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo site.
Hugo is a Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go.
Complete documentation is available at https://gohugo.io/.`
cmd.Long = strings.ReplaceAll(cmd.Long, "COMMAND_NAME", commandName)
// Configure persistent flags
cmd.PersistentFlags().StringVarP(&r.source, "source", "s", "", "filesystem path to read files relative from")
_ = cmd.MarkFlagDirname("source")
cmd.PersistentFlags().StringP("destination", "d", "", "filesystem path to write files to")
_ = cmd.MarkFlagDirname("destination")
cmd.PersistentFlags().StringVarP(&r.environment, "environment", "e", "", "build environment")
_ = cmd.RegisterFlagCompletionFunc("environment", cobra.NoFileCompletions)
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
_ = cmd.MarkFlagDirname("themesDir")
cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
cmd.PersistentFlags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
_ = cmd.RegisterFlagCompletionFunc("ignoreVendorPaths", cobra.NoFileCompletions)
cmd.PersistentFlags().String("clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00")
_ = cmd.RegisterFlagCompletionFunc("clock", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&r.cfgFile, "config", "", "config file (default is hugo.yaml|json|toml)")
_ = cmd.MarkFlagFilename("config", config.ValidConfigFileExtensions...)
cmd.PersistentFlags().StringVar(&r.cfgDir, "configDir", "config", "config dir")
_ = cmd.MarkFlagDirname("configDir")
cmd.PersistentFlags().BoolVar(&r.quiet, "quiet", false, "build in quiet mode")
cmd.PersistentFlags().BoolVarP(&r.renderToMemory, "renderToMemory", "M", false, "render to memory (mostly useful when running the server)")
cmd.PersistentFlags().BoolVarP(&r.devMode, "devMode", "", false, "only used for internal testing, flag hidden.")
cmd.PersistentFlags().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)")
_ = cmd.RegisterFlagCompletionFunc("logLevel", cobra.FixedCompletions([]string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().BoolVarP(&r.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
cmd.PersistentFlags().MarkHidden("devMode")
// Configure local flags
applyLocalFlagsBuild(cmd, r)
return nil
}
// A sub set of the complete build flags. These flags are used by new and mod.
func applyLocalFlagsBuildConfig(cmd *cobra.Command, r *rootCommand) {
cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
_ = cmd.MarkFlagDirname("theme")
cmd.Flags().StringVarP(&r.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. https://spf13.com/")
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory")
_ = cmd.MarkFlagDirname("cacheDir")
cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory")
cmd.Flags().StringSliceP("renderSegments", "", []string{}, "named segments to render (configured in the segments config)")
}
// Flags needed to do a build (used by hugo and hugo server commands)
func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
applyLocalFlagsBuildConfig(cmd, r)
cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories")
cmd.Flags().BoolP("buildDrafts", "D", false, "include content marked as draft")
cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future")
cmd.Flags().BoolP("buildExpired", "E", false, "include expired content")
cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date, author, and CODEOWNERS info to the pages")
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
_ = cmd.MarkFlagDirname("layoutDir")
cmd.Flags().BoolVar(&r.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
cmd.Flags().StringVar(&r.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
_ = cmd.RegisterFlagCompletionFunc("poll", cobra.NoFileCompletions)
cmd.Flags().Bool("panicOnWarning", false, "panic on first WARNING log")
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.")
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
cmd.Flags().BoolP("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(&r.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
cmd.Flags().StringVarP(&r.memprofile, "profile-mem", "", "", "write memory profile to `file`")
cmd.Flags().BoolVarP(&r.printm, "printMemoryUsage", "", false, "print memory usage to screen at intervals")
cmd.Flags().StringVarP(&r.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`")
cmd.Flags().StringVarP(&r.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)")
// Hide these for now.
cmd.Flags().MarkHidden("profile-cpu")
cmd.Flags().MarkHidden("profile-mem")
cmd.Flags().MarkHidden("profile-mutex")
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")
_ = cmd.RegisterFlagCompletionFunc("disableKinds", cobra.FixedCompletions(kinds.AllKinds, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)")
}
func (r *rootCommand) timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
r.Printf("%s in %v ms\n", name, int(1000*elapsed.Seconds()))
}
type simpleCommand struct {
use string
name string
short string
long string
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
withc func(cmd *cobra.Command, r *rootCommand)
initc func(cd *simplecobra.Commandeer) error
commands []simplecobra.Commander
rootCmd *rootCommand
}
func (c *simpleCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *simpleCommand) Name() string {
return c.name
}
func (c *simpleCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
if c.run == nil {
return nil
}
return c.run(ctx, cd, c.rootCmd, args)
}
func (c *simpleCommand) Init(cd *simplecobra.Commandeer) error {
c.rootCmd = cd.Root.Command.(*rootCommand)
cmd := cd.CobraCommand
cmd.Short = c.short
cmd.Long = c.long
if c.use != "" {
cmd.Use = c.use
}
if c.withc != nil {
c.withc(cmd, c.rootCmd)
}
return nil
}
func (c *simpleCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
if c.initc != nil {
return c.initc(cd)
}
return nil
}
func mapLegacyArgs(args []string) []string {
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "site", "theme", "content") {
// Insert "content" as the second argument
args = append(args[:1], append([]string{"content"}, args[1:]...)...)
}
return args
return &commandeer{DepsCfg: cfg, pathSpec: ps}, nil
}

View file

@ -1,73 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"github.com/bep/simplecobra"
)
// newExec wires up all of Hugo's CLI.
func newExec() (*simplecobra.Exec, error) {
rootCmd := &rootCommand{
commands: []simplecobra.Commander{
newHugoBuildCmd(),
newVersionCmd(),
newEnvCommand(),
newServerCommand(),
newDeployCommand(),
newConfigCommand(),
newNewCommand(),
newConvertCommand(),
newImportCommand(),
newListCommand(),
newModCommands(),
newGenCommand(),
newReleaseCommand(),
},
}
return simplecobra.New(rootCmd)
}
func newHugoBuildCmd() simplecobra.Commander {
return &hugoBuildCommand{}
}
// hugoBuildCommand just delegates to the rootCommand.
type hugoBuildCommand struct {
rootCmd *rootCommand
}
func (c *hugoBuildCommand) Commands() []simplecobra.Commander {
return nil
}
func (c *hugoBuildCommand) Name() string {
return "build"
}
func (c *hugoBuildCommand) Init(cd *simplecobra.Commandeer) error {
c.rootCmd = cd.Root.Command.(*rootCommand)
return c.rootCmd.initRootCommand("build", cd)
}
func (c *hugoBuildCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
return c.rootCmd.PreRun(cd, runner)
}
func (c *hugoBuildCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return c.rootCmd.Run(ctx, cd, args)
}

View file

@ -1,239 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/cobra"
)
// newConfigCommand creates a new config command and its subcommands.
func newConfigCommand() *configCommand {
return &configCommand{
commands: []simplecobra.Commander{
&configMountsCommand{},
},
}
}
type configCommand struct {
r *rootCommand
format string
lang string
printZero bool
commands []simplecobra.Commander
}
func (c *configCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *configCommand) Name() string {
return "config"
}
func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil))
if err != nil {
return err
}
var config *allconfig.Config
if c.lang != "" {
var found bool
config, found = conf.configs.LanguageConfigMap[c.lang]
if !found {
return fmt.Errorf("language %q not found", c.lang)
}
} else {
config = conf.configs.LanguageConfigSlice[0]
}
var buf bytes.Buffer
dec := json.NewEncoder(&buf)
dec.SetIndent("", " ")
dec.SetEscapeHTML(false)
if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: !c.printZero}); err != nil {
return err
}
format := strings.ToLower(c.format)
switch format {
case "json":
os.Stdout.Write(buf.Bytes())
default:
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
var m map[string]any
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err
}
maps.ConvertFloat64WithNoDecimalsToInt(m)
switch format {
case "yaml":
return parser.InterfaceToConfig(m, metadecoders.YAML, os.Stdout)
case "toml":
return parser.InterfaceToConfig(m, metadecoders.TOML, os.Stdout)
default:
return fmt.Errorf("unsupported format: %q", format)
}
}
return nil
}
func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
cmd := cd.CobraCommand
cmd.Short = "Display site configuration"
cmd.Long = `Display site configuration, both default and custom settings.`
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
cmd.Flags().BoolVar(&c.printZero, "printZero", false, `include config options with zero values (e.g. false, 0, "") in the output`)
_ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions)
applyLocalFlagsBuildConfig(cmd, c.r)
return nil
}
func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
return nil
}
type configModMount struct {
Source string `json:"source"`
Target string `json:"target"`
Lang string `json:"lang,omitempty"`
}
type configModMounts struct {
verbose bool
m modules.Module
}
// MarshalJSON is for internal use only.
func (m *configModMounts) MarshalJSON() ([]byte, error) {
var mounts []configModMount
for _, mount := range m.m.Mounts() {
mounts = append(mounts, configModMount{
Source: mount.Source,
Target: mount.Target,
Lang: mount.Lang,
})
}
var ownerPath string
if m.m.Owner() != nil {
ownerPath = m.m.Owner().Path()
}
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]any `json:"meta"`
HugoVersion modules.HugoVersion `json:"hugoVersion"`
Mounts []configModMount `json:"mounts"`
}{
Path: m.m.Path(),
Version: m.m.Version(),
Time: m.m.Time(),
Owner: ownerPath,
Dir: m.m.Dir(),
Meta: config.Params,
HugoVersion: config.HugoVersion,
Mounts: mounts,
})
}
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"`
Mounts []configModMount `json:"mounts"`
}{
Path: m.m.Path(),
Version: m.m.Version(),
Time: m.m.Time(),
Owner: ownerPath,
Dir: m.m.Dir(),
Mounts: mounts,
})
}
type configMountsCommand struct {
r *rootCommand
configCmd *configCommand
}
func (c *configMountsCommand) Commands() []simplecobra.Commander {
return nil
}
func (c *configMountsCommand) Name() string {
return "mounts"
}
func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
r := c.configCmd.r
conf, err := r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil))
if err != nil {
return err
}
for _, m := range conf.configs.Modules {
if err := parser.InterfaceToConfig(&configModMounts{m: m, verbose: r.isVerbose()}, metadecoders.JSON, os.Stdout); err != nil {
return err
}
}
return nil
}
func (c *configMountsCommand) Init(cd *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
cmd := cd.CobraCommand
cmd.Short = "Print the configured file mounts"
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, c.r)
return nil
}
func (c *configMountsCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
c.configCmd = cd.Parent.Command.(*configCommand)
return nil
}

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,216 +14,145 @@
package commands
import (
"bytes"
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
"github.com/spf13/cast"
"github.com/spf13/cobra"
)
func newConvertCommand() *convertCommand {
var c *convertCommand
c = &convertCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "toJSON",
short: "Convert front matter to JSON",
long: `toJSON converts all front matter in the content directory
var outputDir string
var unsafe bool
var convertCmd = &cobra.Command{
Use: "convert",
Short: "Convert your content to different formats",
Long: `Convert your content (e.g. front matter) to different formats.
See convert's subcommands toJSON, toTOML and toYAML for more information.`,
RunE: nil,
}
var toJSONCmd = &cobra.Command{
Use: "toJSON",
Short: "Convert front matter to JSON",
Long: `toJSON converts all front matter in the content directory
to use JSON for the front matter.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return c.convertContents(metadecoders.JSON)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
},
&simpleCommand{
name: "toTOML",
short: "Convert front matter to TOML",
long: `toTOML converts all front matter in the content directory
RunE: func(cmd *cobra.Command, args []string) error {
return convertContents(rune([]byte(parser.JSONLead)[0]))
},
}
var toTOMLCmd = &cobra.Command{
Use: "toTOML",
Short: "Convert front matter to TOML",
Long: `toTOML converts all front matter in the content directory
to use TOML for the front matter.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return c.convertContents(metadecoders.TOML)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
},
&simpleCommand{
name: "toYAML",
short: "Convert front matter to YAML",
long: `toYAML converts all front matter in the content directory
RunE: func(cmd *cobra.Command, args []string) error {
return convertContents(rune([]byte(parser.TOMLLead)[0]))
},
}
var toYAMLCmd = &cobra.Command{
Use: "toYAML",
Short: "Convert front matter to YAML",
Long: `toYAML converts all front matter in the content directory
to use YAML for the front matter.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return c.convertContents(metadecoders.YAML)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
},
},
}
return c
RunE: func(cmd *cobra.Command, args []string) error {
return convertContents(rune([]byte(parser.YAMLLead)[0]))
},
}
type convertCommand struct {
// Flags.
outputDir string
unsafe bool
// Deps.
r *rootCommand
h *hugolib.HugoSites
// Commands.
commands []simplecobra.Commander
func init() {
convertCmd.AddCommand(toJSONCmd)
convertCmd.AddCommand(toTOMLCmd)
convertCmd.AddCommand(toYAMLCmd)
convertCmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "filesystem path to write files to")
convertCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from")
convertCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "enable less safe operations, please backup first")
convertCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
}
func (c *convertCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *convertCommand) Name() string {
return "convert"
}
func (c *convertCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *convertCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "Convert front matter to another format"
cmd.Long = `Convert front matter to another format.
See convert's subcommands toJSON, toTOML and toYAML for more information.`
cmd.PersistentFlags().StringVarP(&c.outputDir, "output", "o", "", "filesystem path to write files to")
_ = cmd.MarkFlagDirname("output")
cmd.PersistentFlags().BoolVar(&c.unsafe, "unsafe", false, "enable less safe operations, please backup first")
cmd.RunE = nil
return nil
}
func (c *convertCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
cfg := config.New()
cfg.Set("buildDrafts", true)
h, err := c.r.Hugo(flagsToCfg(cd, cfg))
func convertContents(mark rune) error {
cfg, err := InitializeConfig()
if err != nil {
return err
}
c.h = h
return nil
}
func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
// The resources are not in .Site.AllPages.
for _, r := range p.Resources().ByType("page") {
if err := c.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
h, err := hugolib.NewHugoSites(*cfg)
if err != nil {
return err
}
site := h.Sites[0]
if err = site.Initialise(); err != nil {
return err
}
if site.Source == nil {
panic("site.Source not set")
}
if len(site.Source.Files()) < 1 {
return errors.New("No source files found")
}
contentDir := site.PathSpec.AbsPathify(site.Cfg.GetString("contentDir"))
site.Log.FEEDBACK.Println("processing", len(site.Source.Files()), "content files")
for _, file := range site.Source.Files() {
site.Log.INFO.Println("Attempting to convert", file.LogicalName())
page, err := site.NewPage(file.LogicalName())
if err != nil {
return err
}
}
if p.File() == nil {
// No content file.
return nil
}
psr, err := parser.ReadFrom(file.Contents)
if err != nil {
site.Log.ERROR.Println("Error processing file:", file.Path())
return err
}
metadata, err := psr.Metadata()
if err != nil {
site.Log.ERROR.Println("Error processing file:", file.Path())
return err
}
errMsg := fmt.Errorf("error processing file %q", p.File().Path())
// better handling of dates in formats that don't have support for them
if mark == parser.FormatToLeadRune("json") || mark == parser.FormatToLeadRune("yaml") || mark == parser.FormatToLeadRune("toml") {
newMetadata := cast.ToStringMap(metadata)
for k, v := range newMetadata {
switch vv := v.(type) {
case time.Time:
newMetadata[k] = vv.Format(time.RFC3339)
}
}
metadata = newMetadata
}
site.Log.Infoln("attempting to convert", p.File().Filename())
page.SetDir(filepath.Join(contentDir, file.Dir()))
page.SetSourceContent(psr.Content())
if err = page.SetSourceMetaData(metadata, mark); err != nil {
site.Log.ERROR.Printf("Failed to set source metadata for file %q: %s. For more info see For more info see https://github.com/gohugoio/hugo/issues/2458", page.FullFilePath(), err)
continue
}
f := p.File()
file, err := f.FileInfo().Meta().Open()
if err != nil {
site.Log.Errorln(errMsg)
file.Close()
return nil
}
pf, err := pageparser.ParseFrontMatterAndContent(file)
if err != nil {
site.Log.Errorln(errMsg)
file.Close()
return err
}
file.Close()
// better handling of dates in formats that don't have support for them
if pf.FrontMatterFormat == metadecoders.JSON || pf.FrontMatterFormat == metadecoders.YAML || pf.FrontMatterFormat == metadecoders.TOML {
for k, v := range pf.FrontMatter {
switch vv := v.(type) {
case time.Time:
pf.FrontMatter[k] = vv.Format(time.RFC3339)
if outputDir != "" {
if err = page.SaveSourceAs(filepath.Join(outputDir, page.FullFilePath())); err != nil {
return fmt.Errorf("Failed to save file %q: %s", page.FullFilePath(), err)
}
} else {
if unsafe {
if err = page.SaveSource(); err != nil {
return fmt.Errorf("Failed to save file %q: %s", page.FullFilePath(), err)
}
} else {
site.Log.FEEDBACK.Println("Unsafe operation not allowed, use --unsafe or set a different output path")
}
}
}
var newContent bytes.Buffer
err = parser.InterfaceToFrontMatter(pf.FrontMatter, targetFormat, &newContent)
if err != nil {
site.Log.Errorln(errMsg)
return err
}
newContent.Write(pf.Content)
newFilename := p.File().Filename()
if c.outputDir != "" {
contentDir := strings.TrimSuffix(newFilename, p.File().Path())
contentDir = filepath.Base(contentDir)
newFilename = filepath.Join(c.outputDir, contentDir, p.File().Path())
}
fs := hugofs.Os
if err := helpers.WriteToDisk(newFilename, &newContent, fs); err != nil {
return fmt.Errorf("failed to save file %q:: %w", newFilename, err)
}
return nil
}
func (c *convertCommand) convertContents(format metadecoders.Format) error {
if c.outputDir == "" && !c.unsafe {
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
}
if err := c.h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return err
}
site := c.h.Sites[0]
var pagesBackedByFile page.Pages
for _, p := range site.AllPages() {
if p.File() == nil {
continue
}
pagesBackedByFile = append(pagesBackedByFile, p)
}
site.Log.Println("processing", len(pagesBackedByFile), "content files")
for _, p := range site.AllPages() {
if err := c.convertAndSavePage(p, site, format); err != nil {
return err
}
}
return nil
}

View file

@ -1,51 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build withdeploy
package commands
import (
"context"
"github.com/gohugoio/hugo/deploy"
"github.com/bep/simplecobra"
"github.com/spf13/cobra"
)
func newDeployCommand() simplecobra.Commander {
return &simpleCommand{
name: "deploy",
short: "Deploy your site to a cloud provider",
long: `Deploy your site to a cloud provider
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
documentation.
`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfgWithAdditionalConfigBase(cd, nil, "deployment"))
if err != nil {
return err
}
deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.Log, h.PathSpec.PublishFs)
if err != nil {
return err
}
return deployer.Deploy(ctx)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
applyDeployFlags(cmd, r)
},
}
}

View file

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

View file

@ -1,50 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !withdeploy
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"errors"
"github.com/bep/simplecobra"
"github.com/spf13/cobra"
)
func newDeployCommand() simplecobra.Commander {
return &simpleCommand{
name: "deploy",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return errors.New("deploy not supported in this version of Hugo; install a release with 'withdeploy' in the archive filename or build yourself with the 'withdeploy' build tag. Also see https://github.com/gohugoio/hugo/pull/12995")
},
withc: func(cmd *cobra.Command, r *rootCommand) {
applyDeployFlags(cmd, r)
cmd.Hidden = true
},
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2016 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,57 +14,22 @@
package commands
import (
"context"
"runtime"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hugo"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
func newEnvCommand() simplecobra.Commander {
return &simpleCommand{
name: "env",
short: "Display version and environment info",
long: "Display version and environment info. This is useful in Hugo bug reports",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Printf("%s\n", hugo.BuildVersionString())
r.Printf("GOOS=%q\n", runtime.GOOS)
r.Printf("GOARCH=%q\n", runtime.GOARCH)
r.Printf("GOVERSION=%q\n", runtime.Version())
var envCmd = &cobra.Command{
Use: "env",
Short: "Print Hugo version and environment info",
Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.`,
RunE: func(cmd *cobra.Command, args []string) error {
printHugoVersion()
jww.FEEDBACK.Printf("GOOS=%q\n", runtime.GOOS)
jww.FEEDBACK.Printf("GOARCH=%q\n", runtime.GOARCH)
jww.FEEDBACK.Printf("GOVERSION=%q\n", runtime.Version())
if r.isVerbose() {
deps := hugo.GetDependencyList()
for _, dep := range deps {
r.Printf("%s\n", dep)
}
} else {
// These are also included in the GetDependencyList above;
// always print these as these are most likely the most useful to know about.
deps := hugo.GetDependencyListNonGo()
for _, dep := range deps {
r.Printf("%s\n", dep)
}
}
return nil
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
}
}
func newVersionCmd() simplecobra.Commander {
return &simpleCommand{
name: "version",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Println(hugo.BuildVersionString())
return nil
},
short: "Display version",
long: "Display version and environment info. This is useful in Hugo bug reports.",
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
}
return nil
},
}

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,290 +14,10 @@
package commands
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"slices"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/docshelper"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"gopkg.in/yaml.v2"
)
func newGenCommand() *genCommand {
var (
// Flags.
gendocdir string
genmandir string
// Chroma flags.
style string
highlightStyle string
lineNumbersInlineStyle string
lineNumbersTableStyle string
omitEmpty bool
)
newChromaStyles := func() simplecobra.Commander {
return &simpleCommand{
name: "chromastyles",
short: "Generate CSS stylesheet for the Chroma code highlighter",
long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
style = strings.ToLower(style)
if !slices.Contains(styles.Names(), style) {
return fmt.Errorf("invalid style: %s", style)
}
builder := styles.Get(style).Builder()
if highlightStyle != "" {
builder.Add(chroma.LineHighlight, highlightStyle)
}
if lineNumbersInlineStyle != "" {
builder.Add(chroma.LineNumbers, lineNumbersInlineStyle)
}
if lineNumbersTableStyle != "" {
builder.Add(chroma.LineNumbersTable, lineNumbersTableStyle)
}
style, err := builder.Build()
if err != nil {
return err
}
var formatter *html.Formatter
if omitEmpty {
formatter = html.New(html.WithClasses(true))
} else {
formatter = html.New(html.WithAllClasses(true))
}
w := os.Stdout
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
formatter.WriteCSS(w, style)
return nil
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersInlineStyle, "lineNumbersInlineStyle", "", `foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules`)
_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
},
}
}
newMan := func() simplecobra.Commander {
return &simpleCommand{
name: "man",
short: "Generate man pages for the Hugo CLI",
long: `This command automatically generates up-to-date man pages of Hugo's
command-line interface. By default, it creates the man page files
in the "man" directory under the current directory.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
header := &doc.GenManHeader{
Section: "1",
Manual: "Hugo Manual",
Source: fmt.Sprintf("Hugo %s", hugo.CurrentVersion),
}
if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) {
genmandir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(genmandir, hugofs.Os); !found {
r.Println("Directory", genmandir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(genmandir, 0o777); err != nil {
return err
}
}
cd.CobraCommand.Root().DisableAutoGenTag = true
r.Println("Generating Hugo man pages in", genmandir, "...")
doc.GenManTree(cd.CobraCommand.Root(), header, genmandir)
r.Println("Done.")
return nil
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.")
_ = cmd.MarkFlagDirname("dir")
},
}
}
newGen := func() simplecobra.Commander {
const gendocFrontmatterTemplate = `---
title: "%s"
slug: %s
url: %s
---
`
return &simpleCommand{
name: "doc",
short: "Generate Markdown documentation for the Hugo CLI",
long: `Generate Markdown documentation for the Hugo CLI.
This command is, mostly, used to create up-to-date documentation
of Hugo's command-line interface for https://gohugo.io/.
It creates one Markdown file per command with front matter suitable
for rendering in Hugo.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
cd.CobraCommand.VisitParents(func(c *cobra.Command) {
// Disable the "Auto generated by spf13/cobra on DATE"
// as it creates a lot of diffs.
c.DisableAutoGenTag = true
})
if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) {
gendocdir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found {
r.Println("Directory", gendocdir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(gendocdir, 0o777); err != nil {
return err
}
}
prepender := func(filename string) string {
name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name))
url := "/docs/reference/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
}
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/docs/reference/commands/" + strings.ToLower(base) + "/"
}
r.Println("Generating Hugo command-line documentation in", gendocdir, "...")
doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler)
r.Println("Done.")
return nil
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
_ = cmd.MarkFlagDirname("dir")
},
}
}
var docsHelperTarget string
newDocsHelper := func() simplecobra.Commander {
return &simpleCommand{
name: "docshelper",
short: "Generate some data files for the Hugo docs",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Println("Generate docs data to", docsHelperTarget)
var buf bytes.Buffer
jsonEnc := json.NewEncoder(&buf)
configProvider := func() docshelper.DocProvider {
conf := hugolib.DefaultConfig()
conf.CacheDir = "" // The default value does not make sense in the docs.
defaultConfig := parser.NullBoolJSONMarshaller{Wrapped: parser.LowerCaseCamelJSONMarshaller{Value: conf}}
return docshelper.DocProvider{"config": defaultConfig}
}
docshelper.AddDocProviderFunc(configProvider)
if err := jsonEnc.Encode(docshelper.GetDocProvider()); err != nil {
return err
}
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
var m map[string]any
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err
}
targetFile := filepath.Join(docsHelperTarget, "docs.yaml")
f, err := os.Create(targetFile)
if err != nil {
return err
}
defer f.Close()
yamlEnc := yaml.NewEncoder(f)
if err := yamlEnc.Encode(m); err != nil {
return err
}
r.Println("Done!")
return nil
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.Hidden = true
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVarP(&docsHelperTarget, "dir", "", "docs/data", "data dir")
},
}
}
return &genCommand{
commands: []simplecobra.Commander{
newChromaStyles(),
newGen(),
newMan(),
newDocsHelper(),
},
}
}
type genCommand struct {
rootCmd *rootCommand
commands []simplecobra.Commander
}
func (c *genCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *genCommand) Name() string {
return "gen"
}
func (c *genCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *genCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "Generate documentation and syntax highlighting styles"
cmd.Long = "Generate documentation for your project using Hugo's documentation engine, including syntax highlighting for various programming languages."
cmd.RunE = nil
return nil
}
func (c *genCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
c.rootCmd = cd.Root.Command.(*rootCommand)
return nil
var genCmd = &cobra.Command{
Use: "gen",
Short: "A collection of several useful generators.",
}

View file

@ -0,0 +1,70 @@
// 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 (
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var autocompleteTarget string
// bash for now (zsh and others will come)
var autocompleteType string
var genautocompleteCmd = &cobra.Command{
Use: "autocomplete",
Short: "Generate shell autocompletion script for Hugo",
Long: `Generates a shell autocompletion script for Hugo.
NOTE: The current version supports Bash only.
This should work for *nix systems with Bash installed.
By default, the file is written directly to /etc/bash_completion.d
for convenience, and the command may need superuser rights, e.g.:
$ sudo hugo gen autocomplete
Add ` + "`--completionfile=/path/to/file`" + ` flag to set alternative
file-path and name.
Logout and in again to reload the completion scripts,
or just source them in directly:
$ . /etc/bash_completion`,
RunE: func(cmd *cobra.Command, args []string) error {
if autocompleteType != "bash" {
return newUserError("Only Bash is supported for now")
}
err := cmd.Root().GenBashCompletionFile(autocompleteTarget)
if err != nil {
return err
}
jww.FEEDBACK.Println("Bash completion file for Hugo saved to", autocompleteTarget)
return nil
},
}
func init() {
genautocompleteCmd.PersistentFlags().StringVarP(&autocompleteTarget, "completionfile", "", "/etc/bash_completion.d/hugo.sh", "autocompletion file")
genautocompleteCmd.PersistentFlags().StringVarP(&autocompleteType, "type", "", "bash", "autocompletion type (currently only bash supported)")
// For bash-completion
genautocompleteCmd.PersistentFlags().SetAnnotation("completionfile", cobra.BashCompFilenameExt, []string{})
}

86
commands/gendoc.go Normal file
View file

@ -0,0 +1,86 @@
// Copyright 2016 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 (
"fmt"
"path"
"path/filepath"
"strings"
"time"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
jww "github.com/spf13/jwalterweatherman"
)
const gendocFrontmatterTemplate = `---
date: %s
title: "%s"
slug: %s
url: %s
---
`
var gendocdir string
var gendocCmd = &cobra.Command{
Use: "doc",
Short: "Generate Markdown documentation for the Hugo CLI.",
Long: `Generate Markdown documentation for the Hugo CLI.
This command is, mostly, used to create up-to-date documentation
of Hugo's command-line interface for http://gohugo.io/.
It creates one Markdown file per command with front matter suitable
for rendering in Hugo.`,
RunE: func(cmd *cobra.Command, args []string) error {
if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) {
gendocdir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found {
jww.FEEDBACK.Println("Directory", gendocdir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(gendocdir, 0777); err != nil {
return err
}
}
now := time.Now().Format(time.RFC3339)
prepender := func(filename string) string {
name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name))
url := "/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(gendocFrontmatterTemplate, now, strings.Replace(base, "_", " ", -1), base, url)
}
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/commands/" + strings.ToLower(base) + "/"
}
jww.FEEDBACK.Println("Generating Hugo command-line documentation in", gendocdir, "...")
doc.GenMarkdownTreeCustom(cmd.Root(), gendocdir, prepender, linkHandler)
jww.FEEDBACK.Println("Done.")
return nil
},
}
func init() {
gendocCmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
// For bash-completion
gendocCmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
}

70
commands/gendocshelper.go Normal file
View file

@ -0,0 +1,70 @@
// Copyright 2017-present 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 (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/gohugoio/hugo/docshelper"
"github.com/spf13/cobra"
)
type genDocsHelper struct {
target string
cmd *cobra.Command
}
func createGenDocsHelper() *genDocsHelper {
g := &genDocsHelper{
cmd: &cobra.Command{
Use: "docshelper",
Short: "Generate some data files for the Hugo docs.",
Hidden: true,
},
}
g.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return g.generate()
}
g.cmd.PersistentFlags().StringVarP(&g.target, "dir", "", "docs/data", "data dir")
return g
}
func (g *genDocsHelper) generate() error {
fmt.Println("Generate docs data to", g.target)
targetFile := filepath.Join(g.target, "docs.json")
f, err := os.Create(targetFile)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(docshelper.DocProviders); err != nil {
return err
}
fmt.Println("Done!")
return nil
}

66
commands/genman.go Normal file
View file

@ -0,0 +1,66 @@
// Copyright 2016 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 (
"fmt"
"strings"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
jww "github.com/spf13/jwalterweatherman"
)
var genmandir string
var genmanCmd = &cobra.Command{
Use: "man",
Short: "Generate man pages for the Hugo CLI",
Long: `This command automatically generates up-to-date man pages of Hugo's
command-line interface. By default, it creates the man page files
in the "man" directory under the current directory.`,
RunE: func(cmd *cobra.Command, args []string) error {
header := &doc.GenManHeader{
Section: "1",
Manual: "Hugo Manual",
Source: fmt.Sprintf("Hugo %s", helpers.CurrentHugoVersion),
}
if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) {
genmandir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(genmandir, hugofs.Os); !found {
jww.FEEDBACK.Println("Directory", genmandir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(genmandir, 0777); err != nil {
return err
}
}
cmd.Root().DisableAutoGenTag = true
jww.FEEDBACK.Println("Generating Hugo man pages in", genmandir, "...")
doc.GenManTree(cmd.Root(), header, genmandir)
jww.FEEDBACK.Println("Done.")
return nil
},
}
func init() {
genmanCmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.")
// For bash-completion
genmanCmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
}

View file

@ -1,121 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/spf13/pflag"
)
const (
ansiEsc = "\u001B"
clearLine = "\r\033[K"
hideCursor = ansiEsc + "[?25l"
showCursor = ansiEsc + "[?25h"
)
func newUserError(a ...any) *simplecobra.CommandError {
return &simplecobra.CommandError{Err: errors.New(fmt.Sprint(a...))}
}
func setValueFromFlag(flags *pflag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
key = strings.TrimSpace(key)
if (force && flags.Lookup(key) != nil) || flags.Changed(key) {
f := flags.Lookup(key)
configKey := key
if targetKey != "" {
configKey = targetKey
}
// Gotta love this API.
switch f.Value.Type() {
case "bool":
bv, _ := flags.GetBool(key)
cfg.Set(configKey, bv)
case "string":
cfg.Set(configKey, f.Value.String())
case "stringSlice":
bv, _ := flags.GetStringSlice(key)
cfg.Set(configKey, bv)
case "int":
iv, _ := flags.GetInt(key)
cfg.Set(configKey, iv)
default:
panic(fmt.Sprintf("update switch with %s", f.Value.Type()))
}
}
}
func flagsToCfg(cd *simplecobra.Commandeer, cfg config.Provider) config.Provider {
return flagsToCfgWithAdditionalConfigBase(cd, cfg, "")
}
func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.Provider, additionalConfigBase string) config.Provider {
if cfg == nil {
cfg = config.New()
}
// Flags with a different name in the config.
keyMap := map[string]string{
"minify": "minifyOutput",
"destination": "publishDir",
"editor": "newContentEditor",
}
// Flags that we for some reason don't want to expose in the site config.
internalKeySet := map[string]bool{
"quiet": true,
"verbose": true,
"watch": true,
"liveReloadPort": true,
"renderToMemory": true,
"clock": true,
}
cmd := cd.CobraCommand
flags := cmd.Flags()
flags.VisitAll(func(f *pflag.Flag) {
if f.Changed {
targetKey := f.Name
if internalKeySet[targetKey] {
targetKey = "internal." + targetKey
} else if mapped, ok := keyMap[targetKey]; ok {
targetKey = mapped
}
setValueFromFlag(flags, f.Name, cfg, targetKey, false)
if additionalConfigBase != "" {
setValueFromFlag(flags, f.Name, cfg, additionalConfigBase+"."+targetKey, true)
}
}
})
return cfg
}
func mkdir(x ...string) {
p := filepath.Join(x...)
err := os.MkdirAll(p, 0o777) // before umask
if err != nil {
log.Fatal(err)
}
}

1078
commands/hugo.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -13,21 +13,15 @@
package commands
import (
// For time zone lookups on Windows without Go installed.
// See #8892
_ "time/tzdata"
"github.com/spf13/cobra"
)
import "github.com/spf13/cobra"
func init() {
// This message to show to Windows users if Hugo is opened from explorer.exe
cobra.MousetrapHelpText = `
Hugo is a command-line tool for generating static websites.
Hugo is a command-line tool for generating static website.
You need to open PowerShell and run Hugo from there.
Visit https://gohugo.io/ for more information.`
You need to open cmd.exe and run Hugo from there.
Visit http://gohugo.io/ for more information.`
}

File diff suppressed because it is too large Load diff

View file

@ -1,618 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
func newImportCommand() *importCommand {
var c *importCommand
c = &importCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "jekyll",
short: "hugo import from Jekyll",
long: `hugo import from Jekyll.
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 2 {
return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
}
return c.importFromJekyll(args)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.Flags().BoolVar(&c.force, "force", false, "allow import into non-empty target directory")
},
},
},
}
return c
}
type importCommand struct {
r *rootCommand
force bool
commands []simplecobra.Commander
}
func (c *importCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *importCommand) Name() string {
return "import"
}
func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *importCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "Import a site from another system"
cmd.Long = `Import a site from another system.
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
cmd.RunE = nil
return nil
}
func (c *importCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}
func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
title := "My New Hugo Site"
baseURL := "http://example.org/"
for key, value := range jekyllConfig {
lowerKey := strings.ToLower(key)
switch lowerKey {
case "title":
if str, ok := value.(string); ok {
title = str
}
case "url":
if str, ok := value.(string); ok {
baseURL = str
}
}
}
in := map[string]any{
"baseURL": baseURL,
"title": title,
"languageCode": "en-us",
"disablePathToLower": true,
}
var buf bytes.Buffer
err = parser.InterfaceToConfig(in, kind, &buf)
if err != nil {
return err
}
return helpers.WriteToDisk(filepath.Join(inpath, "hugo."+string(kind)), &buf, fs)
}
func (c *importCommand) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) {
postDirs := make(map[string]bool)
hasAnyPost := false
if entries, err := os.ReadDir(jekyllRoot); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(jekyllRoot, entry.Name())
if isPostDir, hasAnyPostInDir := c.retrieveJekyllPostDir(fs, subDir); isPostDir {
postDirs[entry.Name()] = hasAnyPostInDir
if hasAnyPostInDir {
hasAnyPost = true
}
}
}
}
}
return postDirs, hasAnyPost
}
func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
fs := &afero.OsFs{}
if exists, _ := helpers.Exists(targetDir, fs); exists {
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
return errors.New("target path \"" + targetDir + "\" exists but is not a directory")
}
isEmpty, _ := helpers.IsEmpty(targetDir, fs)
if !isEmpty && !c.force {
return errors.New("target path \"" + targetDir + "\" exists and is not empty")
}
}
jekyllConfig := c.loadJekyllConfig(fs, jekyllRoot)
mkdir(targetDir, "layouts")
mkdir(targetDir, "content")
mkdir(targetDir, "archetypes")
mkdir(targetDir, "static")
mkdir(targetDir, "data")
mkdir(targetDir, "themes")
c.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
c.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
return nil
}
func (c *importCommand) convertJekyllContent(m any, content string) (string, error) {
metadata, _ := maps.ToStringMapE(m)
lines := strings.Split(content, "\n")
var resultLines []string
for _, line := range lines {
resultLines = append(resultLines, strings.Trim(line, "\r\n"))
}
content = strings.Join(resultLines, "\n")
excerptSep := "<!--more-->"
if value, ok := metadata["excerpt_separator"]; ok {
if str, strOk := value.(string); strOk {
content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1)
}
}
replaceList := []struct {
re *regexp.Regexp
replace string
}{
{regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"},
{regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"},
{regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"},
}
for _, replace := range replaceList {
content = replace.re.ReplaceAllString(content, replace.replace)
}
replaceListFunc := []struct {
re *regexp.Regexp
replace func(string) string
}{
// Octopress image tag: http://octopress.org/docs/plugins/image-tag/
{regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), c.replaceImageTag},
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), c.replaceHighlightTag},
}
for _, replace := range replaceListFunc {
content = replace.re.ReplaceAllStringFunc(content, replace.replace)
}
var buf bytes.Buffer
if len(metadata) != 0 {
err := parser.InterfaceToFrontMatter(m, metadecoders.YAML, &buf)
if err != nil {
return "", err
}
}
buf.WriteString(content)
return buf.String(), nil
}
func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
metadata, err := maps.ToStringMapE(m)
if err != nil {
return nil, err
}
if draft {
metadata["draft"] = true
}
for key, value := range metadata {
lowerKey := strings.ToLower(key)
switch lowerKey {
case "layout":
delete(metadata, key)
case "permalink":
if str, ok := value.(string); ok {
metadata["url"] = str
}
delete(metadata, key)
case "category":
if str, ok := value.(string); ok {
metadata["categories"] = []string{str}
}
delete(metadata, key)
case "excerpt_separator":
if key != lowerKey {
delete(metadata, key)
metadata[lowerKey] = value
}
case "date":
if str, ok := value.(string); ok {
re := regexp.MustCompile(`(\d+):(\d+):(\d+)`)
r := re.FindAllStringSubmatch(str, -1)
if len(r) > 0 {
hour, _ := strconv.Atoi(r[0][1])
minute, _ := strconv.Atoi(r[0][2])
second, _ := strconv.Atoi(r[0][3])
postDate = time.Date(postDate.Year(), postDate.Month(), postDate.Day(), hour, minute, second, 0, time.UTC)
}
}
delete(metadata, key)
}
}
metadata["date"] = postDate.Format(time.RFC3339)
return metadata, nil
}
func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft bool) error {
log.Println("Converting", path)
filename := filepath.Base(path)
postDate, postName, err := c.parseJekyllFilename(filename)
if err != nil {
c.r.Printf("Failed to parse filename '%s': %s. Skipping.", filename, err)
return nil
}
log.Println(filename, postDate, postName)
targetFile := filepath.Join(targetDir, relPath)
targetParentDir := filepath.Dir(targetFile)
os.MkdirAll(targetParentDir, 0o777)
contentBytes, err := os.ReadFile(path)
if err != nil {
c.r.logger.Errorln("Read file error:", path)
return err
}
pf, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(contentBytes))
if err != nil {
return fmt.Errorf("failed to parse file %q: %s", filename, err)
}
newmetadata, err := c.convertJekyllMetaData(pf.FrontMatter, postName, postDate, draft)
if err != nil {
return fmt.Errorf("failed to convert metadata for file %q: %s", filename, err)
}
content, err := c.convertJekyllContent(newmetadata, string(pf.Content))
if err != nil {
return fmt.Errorf("failed to convert content for file %q: %s", filename, err)
}
fs := hugofs.Os
if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil {
return fmt.Errorf("failed to save file %q: %s", filename, err)
}
return nil
}
func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) {
fs := hugofs.Os
fi, err := fs.Stat(jekyllRoot)
if err != nil {
return err
}
if !fi.IsDir() {
return errors.New(jekyllRoot + " is not a directory")
}
err = os.MkdirAll(dest, fi.Mode())
if err != nil {
return err
}
entries, err := os.ReadDir(jekyllRoot)
if err != nil {
return err
}
for _, entry := range entries {
sfp := filepath.Join(jekyllRoot, entry.Name())
dfp := filepath.Join(dest, entry.Name())
if entry.IsDir() {
if entry.Name()[0] != '_' && entry.Name()[0] != '.' {
if _, ok := jekyllPostDirs[entry.Name()]; !ok {
err = hugio.CopyDir(fs, sfp, dfp, nil)
if err != nil {
c.r.logger.Errorln(err)
}
}
}
} else {
lowerEntryName := strings.ToLower(entry.Name())
exceptSuffix := []string{
".md", ".markdown", ".html", ".htm",
".xml", ".textile", "rakefile", "gemfile", ".lock",
}
isExcept := false
for _, suffix := range exceptSuffix {
if strings.HasSuffix(lowerEntryName, suffix) {
isExcept = true
break
}
}
if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' {
err = hugio.CopyFile(fs, sfp, dfp)
if err != nil {
c.r.logger.Errorln(err)
}
}
}
}
return nil
}
func (c *importCommand) importFromJekyll(args []string) error {
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return newUserError("path error:", args[0])
}
targetDir, err := filepath.Abs(filepath.Clean(args[1]))
if err != nil {
return newUserError("path error:", args[1])
}
c.r.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir)
if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) {
return newUserError("abort: target path should not be inside the Jekyll root")
}
fs := afero.NewOsFs()
jekyllPostDirs, hasAnyPost := c.getJekyllDirInfo(fs, jekyllRoot)
if !hasAnyPost {
return errors.New("abort: jekyll root contains neither posts nor drafts")
}
err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
if err != nil {
return newUserError(err)
}
c.r.Println("Importing...")
fileCount := 0
callback := func(path string, fi hugofs.FileMetaInfo) error {
if fi.IsDir() {
return nil
}
relPath, err := filepath.Rel(jekyllRoot, path)
if err != nil {
return newUserError("get rel path error:", path)
}
relPath = filepath.ToSlash(relPath)
draft := false
switch {
case strings.Contains(relPath, "_posts/"):
relPath = filepath.Join("content/post", strings.Replace(relPath, "_posts/", "", -1))
case strings.Contains(relPath, "_drafts/"):
relPath = filepath.Join("content/draft", strings.Replace(relPath, "_drafts/", "", -1))
draft = true
default:
return nil
}
fileCount++
return c.convertJekyllPost(path, relPath, targetDir, draft)
}
for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs {
if hasAnyPostInDir {
if err = helpers.Walk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
return err
}
}
}
c.r.Println("Congratulations!", fileCount, "post(s) imported!")
c.r.Println("Now, start Hugo by yourself:\n")
c.r.Println("cd " + args[1])
c.r.Println("git init")
c.r.Println("git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke themes/ananke")
c.r.Println("echo \"theme = 'ananke'\" > hugo.toml")
c.r.Println("hugo server")
return nil
}
func (c *importCommand) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]any {
path := filepath.Join(jekyllRoot, "_config.yml")
exists, err := helpers.Exists(path, fs)
if err != nil || !exists {
c.r.Println("_config.yaml not found: Is the specified Jekyll root correct?")
return nil
}
f, err := fs.Open(path)
if err != nil {
return nil
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil
}
m, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
if err != nil {
return nil
}
return m
}
func (c *importCommand) parseJekyllFilename(filename string) (time.Time, string, error) {
re := regexp.MustCompile(`(\d+-\d+-\d+)-(.+)\..*`)
r := re.FindAllStringSubmatch(filename, -1)
if len(r) == 0 {
return htime.Now(), "", errors.New("filename not match")
}
postDate, err := time.Parse("2006-1-2", r[0][1])
if err != nil {
return htime.Now(), "", err
}
postName := r[0][2]
return postDate, postName, nil
}
func (c *importCommand) replaceHighlightTag(match string) string {
r := regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`)
parts := r.FindStringSubmatch(match)
lastQuote := rune(0)
f := func(c rune) bool {
switch {
case c == lastQuote:
lastQuote = rune(0)
return false
case lastQuote != rune(0):
return false
case unicode.In(c, unicode.Quotation_Mark):
lastQuote = c
return false
default:
return unicode.IsSpace(c)
}
}
// splitting string by space but considering quoted section
items := strings.FieldsFunc(parts[1], f)
result := bytes.NewBufferString("{{< highlight ")
result.WriteString(items[0]) // language
options := items[1:]
for i, opt := range options {
opt = strings.Replace(opt, "\"", "", -1)
if opt == "linenos" {
opt = "linenos=table"
}
if i == 0 {
opt = " \"" + opt
}
if i < len(options)-1 {
opt += ","
} else if i == len(options)-1 {
opt += "\""
}
result.WriteString(opt)
}
result.WriteString(" >}}")
return result.String()
}
func (c *importCommand) replaceImageTag(match string) string {
r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`)
result := bytes.NewBufferString("{{< figure ")
parts := r.FindStringSubmatch(match)
// Index 0 is the entire string, ignore
c.replaceOptionalPart(result, "class", parts[1])
c.replaceOptionalPart(result, "src", parts[2])
c.replaceOptionalPart(result, "width", parts[3])
c.replaceOptionalPart(result, "height", parts[4])
// title + alt
part := parts[5]
if len(part) > 0 {
splits := strings.Split(part, "'")
lenSplits := len(splits)
if lenSplits == 1 {
c.replaceOptionalPart(result, "title", splits[0])
} else if lenSplits == 3 {
c.replaceOptionalPart(result, "title", splits[1])
} else if lenSplits == 5 {
c.replaceOptionalPart(result, "title", splits[1])
c.replaceOptionalPart(result, "alt", splits[3])
}
}
result.WriteString(">}}")
return result.String()
}
func (c *importCommand) replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) {
if len(part) > 0 {
buffer.WriteString(partName + "=\"" + part + "\" ")
}
}
func (c *importCommand) retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool) {
if strings.HasSuffix(dir, "_posts") || strings.HasSuffix(dir, "_drafts") {
isEmpty, _ := helpers.IsEmpty(dir, fs)
return true, !isEmpty
}
if entries, err := os.ReadDir(dir); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(dir, entry.Name())
if isPostDir, hasAnyPost := c.retrieveJekyllPostDir(fs, subDir); isPostDir {
return isPostDir, hasAnyPost
}
}
}
}
return false, true
}

614
commands/import_jekyll.go Normal file
View file

@ -0,0 +1,614 @@
// Copyright 2016 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 (
"bytes"
"errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser"
"github.com/spf13/afero"
"github.com/spf13/cast"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
func init() {
importCmd.AddCommand(importJekyllCmd)
}
var importCmd = &cobra.Command{
Use: "import",
Short: "Import your site from others.",
Long: `Import your site from other web site generators like Jekyll.
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
RunE: nil,
}
var importJekyllCmd = &cobra.Command{
Use: "jekyll",
Short: "hugo import from Jekyll",
Long: `hugo import from Jekyll.
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
RunE: importFromJekyll,
}
func init() {
importJekyllCmd.Flags().Bool("force", false, "allow import into non-empty target directory")
}
func importFromJekyll(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return newUserError(`Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
}
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return newUserError("Path error:", args[0])
}
targetDir, err := filepath.Abs(filepath.Clean(args[1]))
if err != nil {
return newUserError("Path error:", args[1])
}
jww.INFO.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir)
if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) {
return newUserError("Target path should not be inside the Jekyll root, aborting.")
}
forceImport, _ := cmd.Flags().GetBool("force")
fs := afero.NewOsFs()
jekyllPostDirs, hasAnyPost := getJekyllDirInfo(fs, jekyllRoot)
if !hasAnyPost {
return errors.New("Your Jekyll root contains neither posts nor drafts, aborting.")
}
site, err := createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs, forceImport)
if err != nil {
return newUserError(err)
}
jww.FEEDBACK.Println("Importing...")
fileCount := 0
callback := func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if fi.IsDir() {
return nil
}
relPath, err := filepath.Rel(jekyllRoot, path)
if err != nil {
return newUserError("Get rel path error:", path)
}
relPath = filepath.ToSlash(relPath)
draft := false
switch {
case strings.Contains(relPath, "_posts/"):
relPath = filepath.Join("content/post", strings.Replace(relPath, "_posts/", "", -1))
case strings.Contains(relPath, "_drafts/"):
relPath = filepath.Join("content/draft", strings.Replace(relPath, "_drafts/", "", -1))
draft = true
default:
return nil
}
fileCount++
return convertJekyllPost(site, path, relPath, targetDir, draft)
}
for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs {
if hasAnyPostInDir {
if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
return err
}
}
}
jww.FEEDBACK.Println("Congratulations!", fileCount, "post(s) imported!")
jww.FEEDBACK.Println("Now, start Hugo by yourself:\n" +
"$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove")
jww.FEEDBACK.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove")
jww.FEEDBACK.Println("Congratulations!", fileCount, "post(s) imported!")
jww.FEEDBACK.Println("Now, start Hugo by yourself:\n" +
"$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove")
jww.FEEDBACK.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove")
return nil
}
func getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) {
postDirs := make(map[string]bool)
hasAnyPost := false
if entries, err := ioutil.ReadDir(jekyllRoot); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(jekyllRoot, entry.Name())
if isPostDir, hasAnyPostInDir := retrieveJekyllPostDir(fs, subDir); isPostDir {
postDirs[entry.Name()] = hasAnyPostInDir
if hasAnyPostInDir {
hasAnyPost = true
}
}
}
}
}
return postDirs, hasAnyPost
}
func retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool) {
if strings.HasSuffix(dir, "_posts") || strings.HasSuffix(dir, "_drafts") {
isEmpty, _ := helpers.IsEmpty(dir, fs)
return true, !isEmpty
}
if entries, err := ioutil.ReadDir(dir); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(dir, entry.Name())
if isPostDir, hasAnyPost := retrieveJekyllPostDir(fs, subDir); isPostDir {
return isPostDir, hasAnyPost
}
}
}
}
return false, true
}
func createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool, force bool) (*hugolib.Site, error) {
s, err := hugolib.NewSiteDefaultLang()
if err != nil {
return nil, err
}
fs := s.Fs.Source
if exists, _ := helpers.Exists(targetDir, fs); exists {
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
return nil, errors.New("Target path \"" + targetDir + "\" already exists but not a directory")
}
isEmpty, _ := helpers.IsEmpty(targetDir, fs)
if !isEmpty && !force {
return nil, errors.New("Target path \"" + targetDir + "\" already exists and is not empty")
}
}
jekyllConfig := loadJekyllConfig(fs, jekyllRoot)
mkdir(targetDir, "layouts")
mkdir(targetDir, "content")
mkdir(targetDir, "archetypes")
mkdir(targetDir, "static")
mkdir(targetDir, "data")
mkdir(targetDir, "themes")
createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
return s, nil
}
func loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]interface{} {
path := filepath.Join(jekyllRoot, "_config.yml")
exists, err := helpers.Exists(path, fs)
if err != nil || !exists {
jww.WARN.Println("_config.yaml not found: Is the specified Jekyll root correct?")
return nil
}
f, err := fs.Open(path)
if err != nil {
return nil
}
defer f.Close()
b, err := ioutil.ReadAll(f)
if err != nil {
return nil
}
c, err := parser.HandleYAMLMetaData(b)
if err != nil {
return nil
}
return c.(map[string]interface{})
}
func createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfig map[string]interface{}) (err error) {
title := "My New Hugo Site"
baseURL := "http://example.org/"
for key, value := range jekyllConfig {
lowerKey := strings.ToLower(key)
switch lowerKey {
case "title":
if str, ok := value.(string); ok {
title = str
}
case "url":
if str, ok := value.(string); ok {
baseURL = str
}
}
}
in := map[string]interface{}{
"baseURL": baseURL,
"title": title,
"languageCode": "en-us",
"disablePathToLower": true,
}
kind = parser.FormatSanitize(kind)
var buf bytes.Buffer
err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf)
if err != nil {
return err
}
return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs)
}
func copyFile(source string, dest string) error {
sf, err := os.Open(source)
if err != nil {
return err
}
defer sf.Close()
df, err := os.Create(dest)
if err != nil {
return err
}
defer df.Close()
_, err = io.Copy(df, sf)
if err == nil {
si, err := os.Stat(source)
if err != nil {
err = os.Chmod(dest, si.Mode())
if err != nil {
return err
}
}
}
return nil
}
func copyDir(source string, dest string) error {
fi, err := os.Stat(source)
if err != nil {
return err
}
if !fi.IsDir() {
return errors.New(source + " is not a directory")
}
err = os.MkdirAll(dest, fi.Mode())
if err != nil {
return err
}
entries, err := ioutil.ReadDir(source)
for _, entry := range entries {
sfp := filepath.Join(source, entry.Name())
dfp := filepath.Join(dest, entry.Name())
if entry.IsDir() {
err = copyDir(sfp, dfp)
if err != nil {
jww.ERROR.Println(err)
}
} else {
err = copyFile(sfp, dfp)
if err != nil {
jww.ERROR.Println(err)
}
}
}
return nil
}
func copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) {
fi, err := os.Stat(jekyllRoot)
if err != nil {
return err
}
if !fi.IsDir() {
return errors.New(jekyllRoot + " is not a directory")
}
err = os.MkdirAll(dest, fi.Mode())
if err != nil {
return err
}
entries, err := ioutil.ReadDir(jekyllRoot)
for _, entry := range entries {
sfp := filepath.Join(jekyllRoot, entry.Name())
dfp := filepath.Join(dest, entry.Name())
if entry.IsDir() {
if entry.Name()[0] != '_' && entry.Name()[0] != '.' {
if _, ok := jekyllPostDirs[entry.Name()]; !ok {
err = copyDir(sfp, dfp)
if err != nil {
jww.ERROR.Println(err)
}
}
}
} else {
lowerEntryName := strings.ToLower(entry.Name())
exceptSuffix := []string{".md", ".markdown", ".html", ".htm",
".xml", ".textile", "rakefile", "gemfile", ".lock"}
isExcept := false
for _, suffix := range exceptSuffix {
if strings.HasSuffix(lowerEntryName, suffix) {
isExcept = true
break
}
}
if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' {
err = copyFile(sfp, dfp)
if err != nil {
jww.ERROR.Println(err)
}
}
}
}
return nil
}
func parseJekyllFilename(filename string) (time.Time, string, error) {
re := regexp.MustCompile(`(\d+-\d+-\d+)-(.+)\..*`)
r := re.FindAllStringSubmatch(filename, -1)
if len(r) == 0 {
return time.Now(), "", errors.New("filename not match")
}
postDate, err := time.Parse("2006-1-2", r[0][1])
if err != nil {
return time.Now(), "", err
}
postName := r[0][2]
return postDate, postName, nil
}
func convertJekyllPost(s *hugolib.Site, path, relPath, targetDir string, draft bool) error {
jww.TRACE.Println("Converting", path)
filename := filepath.Base(path)
postDate, postName, err := parseJekyllFilename(filename)
if err != nil {
jww.WARN.Printf("Failed to parse filename '%s': %s. Skipping.", filename, err)
return nil
}
jww.TRACE.Println(filename, postDate, postName)
targetFile := filepath.Join(targetDir, relPath)
targetParentDir := filepath.Dir(targetFile)
os.MkdirAll(targetParentDir, 0777)
contentBytes, err := ioutil.ReadFile(path)
if err != nil {
jww.ERROR.Println("Read file error:", path)
return err
}
psr, err := parser.ReadFrom(bytes.NewReader(contentBytes))
if err != nil {
jww.ERROR.Println("Parse file error:", path)
return err
}
metadata, err := psr.Metadata()
if err != nil {
jww.ERROR.Println("Processing file error:", path)
return err
}
newmetadata, err := convertJekyllMetaData(metadata, postName, postDate, draft)
if err != nil {
jww.ERROR.Println("Convert metadata error:", path)
return err
}
jww.TRACE.Println(newmetadata)
content := convertJekyllContent(newmetadata, string(psr.Content()))
page, err := s.NewPage(filename)
if err != nil {
jww.ERROR.Println("New page error", filename)
return err
}
page.SetDir(targetParentDir)
page.SetSourceContent([]byte(content))
page.SetSourceMetaData(newmetadata, parser.FormatToLeadRune("yaml"))
page.SaveSourceAs(targetFile)
jww.TRACE.Println("Target file:", targetFile)
return nil
}
func convertJekyllMetaData(m interface{}, postName string, postDate time.Time, draft bool) (interface{}, error) {
url := postDate.Format("/2006/01/02/") + postName + "/"
metadata, err := cast.ToStringMapE(m)
if err != nil {
return nil, err
}
if draft {
metadata["draft"] = true
}
for key, value := range metadata {
lowerKey := strings.ToLower(key)
switch lowerKey {
case "layout":
delete(metadata, key)
case "permalink":
if str, ok := value.(string); ok {
url = str
}
delete(metadata, key)
case "category":
if str, ok := value.(string); ok {
metadata["categories"] = []string{str}
}
delete(metadata, key)
case "excerpt_separator":
if key != lowerKey {
delete(metadata, key)
metadata[lowerKey] = value
}
case "date":
if str, ok := value.(string); ok {
re := regexp.MustCompile(`(\d+):(\d+):(\d+)`)
r := re.FindAllStringSubmatch(str, -1)
if len(r) > 0 {
hour, _ := strconv.Atoi(r[0][1])
minute, _ := strconv.Atoi(r[0][2])
second, _ := strconv.Atoi(r[0][3])
postDate = time.Date(postDate.Year(), postDate.Month(), postDate.Day(), hour, minute, second, 0, time.UTC)
}
}
delete(metadata, key)
}
}
metadata["url"] = url
metadata["date"] = postDate.Format(time.RFC3339)
return metadata, nil
}
func convertJekyllContent(m interface{}, content string) string {
metadata, _ := cast.ToStringMapE(m)
lines := strings.Split(content, "\n")
var resultLines []string
for _, line := range lines {
resultLines = append(resultLines, strings.Trim(line, "\r\n"))
}
content = strings.Join(resultLines, "\n")
excerptSep := "<!--more-->"
if value, ok := metadata["excerpt_separator"]; ok {
if str, strOk := value.(string); strOk {
content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1)
}
}
replaceList := []struct {
re *regexp.Regexp
replace string
}{
{regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"},
{regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"},
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), "{{< highlight $1 >}}"},
{regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"},
}
for _, replace := range replaceList {
content = replace.re.ReplaceAllString(content, replace.replace)
}
replaceListFunc := []struct {
re *regexp.Regexp
replace func(string) string
}{
// Octopress image tag: http://octopress.org/docs/plugins/image-tag/
{regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), replaceImageTag},
}
for _, replace := range replaceListFunc {
content = replace.re.ReplaceAllStringFunc(content, replace.replace)
}
return content
}
func replaceImageTag(match string) string {
r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`)
result := bytes.NewBufferString("{{< figure ")
parts := r.FindStringSubmatch(match)
// Index 0 is the entire string, ignore
replaceOptionalPart(result, "class", parts[1])
replaceOptionalPart(result, "src", parts[2])
replaceOptionalPart(result, "width", parts[3])
replaceOptionalPart(result, "height", parts[4])
// title + alt
part := parts[5]
if len(part) > 0 {
splits := strings.Split(part, "'")
lenSplits := len(splits)
if lenSplits == 1 {
replaceOptionalPart(result, "title", splits[0])
} else if lenSplits == 3 {
replaceOptionalPart(result, "title", splits[1])
} else if lenSplits == 5 {
replaceOptionalPart(result, "title", splits[1])
replaceOptionalPart(result, "alt", splits[3])
}
}
result.WriteString(">}}")
return result.String()
}
func replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) {
if len(part) > 0 {
buffer.WriteString(partName + "=\"" + part + "\" ")
}
}

View file

@ -0,0 +1,126 @@
// 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 (
"encoding/json"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestParseJekyllFilename(t *testing.T) {
filenameArray := []string{
"2015-01-02-test.md",
"2012-03-15-中文.markup",
}
expectResult := []struct {
postDate time.Time
postName string
}{
{time.Date(2015, time.January, 2, 0, 0, 0, 0, time.UTC), "test"},
{time.Date(2012, time.March, 15, 0, 0, 0, 0, time.UTC), "中文"},
}
for i, filename := range filenameArray {
postDate, postName, err := parseJekyllFilename(filename)
assert.Equal(t, err, nil)
assert.Equal(t, expectResult[i].postDate.Format("2006-01-02"), postDate.Format("2006-01-02"))
assert.Equal(t, expectResult[i].postName, postName)
}
}
func TestConvertJekyllMetadata(t *testing.T) {
testDataList := []struct {
metadata interface{}
postName string
postDate time.Time
draft bool
expect string
}{
{map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z","url":"/2015/10/01/testPost/"}`},
{map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true,
`{"date":"2015-10-01T00:00:00Z","draft":true,"url":"/2015/10/01/testPost/"}`},
{map[interface{}]interface{}{"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"},
"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},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z","url":"/2015/10/01/testPost/"}`},
{map[interface{}]interface{}{"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","url":"/2015/10/01/testPost/"}`},
{map[interface{}]interface{}{"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","url":"/2015/10/01/testPost/"}`},
}
for _, data := range testDataList {
result, err := convertJekyllMetaData(data.metadata, data.postName, data.postDate, data.draft)
assert.Equal(t, nil, err)
jsonResult, err := json.Marshal(result)
assert.Equal(t, nil, err)
assert.Equal(t, data.expect, string(jsonResult))
}
}
func TestConvertJekyllContent(t *testing.T) {
testDataList := []struct {
metadata interface{}
content string
expect string
}{
{map[interface{}]interface{}{},
`Test content\n<!-- more -->\npart2 content`, `Test content\n<!--more-->\npart2 content`},
{map[interface{}]interface{}{},
`Test content\n<!-- More -->\npart2 content`, `Test content\n<!--more-->\npart2 content`},
{map[interface{}]interface{}{"excerpt_separator": "<!--sep-->"},
`Test content\n<!--sep-->\npart2 content`, `Test content\n<!--more-->\npart2 content`},
{map[interface{}]interface{}{}, "{% raw %}text{% endraw %}", "text"},
{map[interface{}]interface{}{}, "{%raw%} text2 {%endraw %}", "text2"},
{map[interface{}]interface{}{},
"{% highlight go %}\nvar s int\n{% endhighlight %}",
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}"},
// Octopress image tag
{map[interface{}]interface{}{},
"{% img http://placekitten.com/890/280 %}",
"{{< figure src=\"http://placekitten.com/890/280\" >}}"},
{map[interface{}]interface{}{},
"{% 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{}{},
"{% 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{}{},
"{% 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{}{},
"{% 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{}{},
"{% 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\" >}}"},
}
for _, data := range testDataList {
result := convertJekyllContent(data.metadata, data.content)
assert.Equal(t, data.expect, result)
}
}

85
commands/limit_darwin.go Normal file
View file

@ -0,0 +1,85 @@
// 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.
// 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 (
"syscall"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
func init() {
checkCmd.AddCommand(limit)
}
var limit = &cobra.Command{
Use: "ulimit",
Short: "Check system ulimit settings",
Long: `Hugo will inspect the current ulimit settings on the system.
This is primarily to ensure that Hugo can watch enough files on some OSs`,
RunE: func(cmd *cobra.Command, args []string) error {
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return newSystemError("Error Getting Rlimit ", err)
}
jww.FEEDBACK.Println("Current rLimit:", rLimit)
jww.FEEDBACK.Println("Attempting to increase limit")
rLimit.Max = 999999
rLimit.Cur = 999999
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return newSystemError("Error Setting rLimit ", err)
}
err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return newSystemError("Error Getting rLimit ", err)
}
jww.FEEDBACK.Println("rLimit after change:", rLimit)
return nil
},
}
func tweakLimit() {
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
jww.ERROR.Println("Unable to obtain rLimit", err)
}
if rLimit.Cur < rLimit.Max {
rLimit.Max = 64000
rLimit.Cur = 64000
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
jww.WARN.Println("Unable to increase number of open files limit", err)
}
}
}

32
commands/limit_others.go Normal file
View file

@ -0,0 +1,32 @@
// 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.
// +build !darwin
// 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
func tweakLimit() {
// nothing to do
}

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,200 +14,148 @@
package commands
import (
"context"
"encoding/csv"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
// newListCommand creates a new list command and its subcommands.
func newListCommand() *listCommand {
createRecord := func(workingDir string, p page.Page) []string {
return []string{
filepath.ToSlash(strings.TrimPrefix(p.File().Filename(), workingDir+string(os.PathSeparator))),
p.Slug(),
p.Title(),
p.Date().Format(time.RFC3339),
p.ExpiryDate().Format(time.RFC3339),
p.PublishDate().Format(time.RFC3339),
strconv.FormatBool(p.Draft()),
p.Permalink(),
p.Kind(),
p.Section(),
}
}
func init() {
listCmd.AddCommand(listDraftsCmd)
listCmd.AddCommand(listFutureCmd)
listCmd.AddCommand(listExpiredCmd)
listCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from")
listCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
}
list := func(cd *simplecobra.Commandeer, r *rootCommand, shouldInclude func(page.Page) bool, opts ...any) error {
bcfg := hugolib.BuildCfg{SkipRender: true}
cfg := flagsToCfg(cd, nil)
for i := 0; i < len(opts); i += 2 {
cfg.Set(opts[i].(string), opts[i+1])
}
h, err := r.Build(cd, bcfg, cfg)
var listCmd = &cobra.Command{
Use: "list",
Short: "Listing out various types of content",
Long: `Listing out various types of content.
List requires a subcommand, e.g. ` + "`hugo list drafts`.",
RunE: nil,
}
var listDraftsCmd = &cobra.Command{
Use: "drafts",
Short: "List all drafts",
Long: `List all of the drafts in your content directory.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := InitializeConfig()
if err != nil {
return err
}
writer := csv.NewWriter(r.StdOut)
defer writer.Flush()
c, err := newCommandeer(cfg)
if err != nil {
return err
}
writer.Write([]string{
"path",
"slug",
"title",
"date",
"expiryDate",
"publishDate",
"draft",
"permalink",
"kind",
"section",
})
c.Set("buildDrafts", true)
for _, p := range h.Pages() {
if shouldInclude(p) {
record := createRecord(h.Conf.BaseConfig().WorkingDir, p)
if err := writer.Write(record); err != nil {
return err
}
sites, err := hugolib.NewHugoSites(*cfg)
if err != nil {
return newSystemError("Error creating sites", err)
}
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return newSystemError("Error Processing Source Content", err)
}
for _, p := range sites.Pages() {
if p.IsDraft() {
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
}
}
return nil
}
return &listCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "drafts",
short: "List draft content",
long: `List draft content.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
shouldInclude := func(p page.Page) bool {
if !p.Draft() || p.File() == nil {
return false
}
return true
}
return list(cd, r, shouldInclude,
"buildDrafts", true,
"buildFuture", true,
"buildExpired", true,
)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
},
&simpleCommand{
name: "future",
short: "List future content",
long: `List content with a future publication date.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
shouldInclude := func(p page.Page) bool {
if !resource.IsFuture(p) || p.File() == nil {
return false
}
return true
}
return list(cd, r, shouldInclude,
"buildFuture", true,
"buildDrafts", true,
)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
},
&simpleCommand{
name: "expired",
short: "List expired content",
long: `List content with a past expiration date.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
shouldInclude := func(p page.Page) bool {
if !resource.IsExpired(p) || p.File() == nil {
return false
}
return true
}
return list(cd, r, shouldInclude,
"buildExpired", true,
"buildDrafts", true,
)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
},
&simpleCommand{
name: "all",
short: "List all content",
long: `List all content including draft, future, and expired.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
shouldInclude := func(p page.Page) bool {
return p.File() != nil
}
return list(cd, r, shouldInclude, "buildDrafts", true, "buildFuture", true, "buildExpired", true)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
},
&simpleCommand{
name: "published",
short: "List published content",
long: `List content that is not draft, future, or expired.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
shouldInclude := func(p page.Page) bool {
return !p.Draft() && !resource.IsFuture(p) && !resource.IsExpired(p) && p.File() != nil
}
return list(cd, r, shouldInclude)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
},
},
}
},
}
type listCommand struct {
commands []simplecobra.Commander
var listFutureCmd = &cobra.Command{
Use: "future",
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 {
cfg, err := InitializeConfig()
if err != nil {
return err
}
c, err := newCommandeer(cfg)
if err != nil {
return err
}
c.Set("buildFuture", true)
sites, err := hugolib.NewHugoSites(*cfg)
if err != nil {
return newSystemError("Error creating sites", err)
}
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return newSystemError("Error Processing Source Content", err)
}
for _, p := range sites.Pages() {
if p.IsFuture() {
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
}
}
return nil
},
}
func (c *listCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *listCommand) Name() string {
return "list"
}
func (c *listCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
// Do nothing.
return nil
}
func (c *listCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "List content"
cmd.Long = `List content.
List requires a subcommand, e.g. hugo list drafts`
cmd.RunE = nil
return nil
}
func (c *listCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
return nil
var listExpiredCmd = &cobra.Command{
Use: "expired",
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 {
cfg, err := InitializeConfig()
if err != nil {
return err
}
c, err := newCommandeer(cfg)
if err != nil {
return err
}
c.Set("buildExpired", true)
sites, err := hugolib.NewHugoSites(*cfg)
if err != nil {
return newSystemError("Error creating sites", err)
}
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return newSystemError("Error Processing Source Content", err)
}
for _, p := range sites.Pages() {
if p.IsExpired() {
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
}
}
return nil
},
}

66
commands/list_config.go Normal file
View file

@ -0,0 +1,66 @@
// 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.Print the version number of Hug
package commands
import (
"reflect"
"sort"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "Print the site configuration",
Long: `Print the site configuration, both default and custom settings.`,
}
func init() {
configCmd.RunE = printConfig
}
func printConfig(cmd *cobra.Command, args []string) error {
cfg, err := InitializeConfig(configCmd)
if err != nil {
return err
}
allSettings := cfg.Cfg.(*viper.Viper).AllSettings()
var separator string
if allSettings["metadataformat"] == "toml" {
separator = " = "
} else {
separator = ": "
}
var keys []string
for k := range allSettings {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
kv := reflect.ValueOf(allSettings[k])
if kv.Kind() == reflect.String {
jww.FEEDBACK.Printf("%s%s\"%+v\"\n", k, separator, allSettings[k])
} else {
jww.FEEDBACK.Printf("%s%s%+v\n", k, separator, allSettings[k])
}
}
return nil
}

View file

@ -1,344 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"errors"
"os"
"path/filepath"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/modules/npm"
"github.com/spf13/cobra"
)
const commonUsageMod = `
Note that Hugo will always start out by resolving the components defined in the site
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.
`
// buildConfigCommands creates a new config command and its subcommands.
func newModCommands() *modCommands {
var (
clean bool
pattern string
all bool
)
npmCommand := &simpleCommand{
name: "npm",
short: "Various npm helpers",
long: `Various npm (Node package manager) helpers.`,
commands: []simplecobra.Commander{
&simpleCommand{
name: "pack",
short: "Experimental: Prepares and writes a composite package.json file for your project",
long: `Prepares and writes a composite package.json file for your project.
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
with the base dependency set.
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
so this may/will change in future versions of Hugo.
`,
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r)
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs)
},
},
},
}
return &modCommands{
commands: []simplecobra.Commander{
&simpleCommand{
name: "init",
short: "Initialize this project as a Hugo Module",
long: `Initialize this project as a Hugo Module.
It will try to guess the module path, but you may help by passing it as an argument, e.g:
hugo mod init github.com/gohugoio/testshortcodes
Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module
inside a subfolder on GitHub, as one example.
`,
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r)
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.getOrCreateHugo(flagsToCfg(cd, nil), true)
if err != nil {
return err
}
var initPath string
if len(args) >= 1 {
initPath = args[0]
}
c := h.Configs.ModulesClient
if err := c.Init(initPath); err != nil {
return err
}
return nil
},
},
&simpleCommand{
name: "verify",
short: "Verify dependencies",
long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`,
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r)
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Verify(clean)
},
},
&simpleCommand{
name: "graph",
short: "Print a module dependency graph",
long: `Print a module dependency graph with information about module status (disabled, vendored).
Note that for vendored modules, that is the version listed and not the one from go.mod.
`,
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r)
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Graph(os.Stdout)
},
},
&simpleCommand{
name: "clean",
short: "Delete the Hugo Module cache for the current project",
long: `Delete the Hugo Module cache for the current project.`,
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r)
cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`)
_ = cmd.RegisterFlagCompletionFunc("pattern", cobra.NoFileCompletions)
cmd.Flags().BoolVarP(&all, "all", "", false, "clean entire module cache")
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
if all {
modCache := h.ResourceSpec.FileCaches.ModulesCache()
count, err := modCache.Prune(true)
r.Printf("Deleted %d files from module cache.", count)
return err
}
return h.Configs.ModulesClient.Clean(pattern)
},
},
&simpleCommand{
name: "tidy",
short: "Remove unused entries in go.mod and go.sum",
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r)
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return h.Configs.ModulesClient.Tidy()
},
},
&simpleCommand{
name: "vendor",
short: "Vendor all module dependencies into the _vendor directory",
long: `Vendor all module dependencies into the _vendor directory.
If a module is vendored, that is where Hugo will look for it's dependencies.
`,
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r)
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return h.Configs.ModulesClient.Vendor()
},
},
&simpleCommand{
name: "get",
short: "Resolves dependencies in your current Hugo project",
long: `
Resolves dependencies in your current Hugo project.
Some examples:
Install the latest version possible for a given module:
hugo mod get github.com/gohugoio/testshortcodes
Install a specific version:
hugo mod get github.com/gohugoio/testshortcodes@v0.3.0
Install the latest versions of all direct module dependencies:
hugo mod get
hugo mod get ./... (recursive)
Install the latest versions of all module dependencies (direct and indirect):
hugo mod get -u
hugo mod get -u ./... (recursive)
Run "go help get" for more information. All flags available for "go get" is also relevant here.
` + commonUsageMod,
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.DisableFlagParsing = true
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
// We currently just pass on the flags we get to Go and
// need to do the flag handling manually.
if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") {
return errHelp
}
var lastArg string
if len(args) != 0 {
lastArg = args[len(args)-1]
}
if lastArg == "./..." {
args = args[:len(args)-1]
// Do a recursive update.
dirname, err := os.Getwd()
if err != nil {
return err
}
// Sanity chesimplecobra. We do recursive walking and want to avoid
// accidents.
if len(dirname) < 5 {
return errors.New("must not be run from the file system root")
}
filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if info.Name() == "go.mod" {
// Found a module.
dir := filepath.Dir(path)
cfg := config.New()
cfg.Set("workingDir", dir)
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Add(1)}, flagsToCfg(cd, cfg))
if err != nil {
return err
}
r.Println("Update module in", conf.configs.Base.WorkingDir)
client := conf.configs.ModulesClient
return client.Get(args...)
}
return nil
})
return nil
} else {
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Get(args...)
}
},
},
npmCommand,
},
}
}
type modCommands struct {
r *rootCommand
commands []simplecobra.Commander
}
func (c *modCommands) Commands() []simplecobra.Commander {
return c.commands
}
func (c *modCommands) Name() string {
return "mod"
}
func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
_, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, nil)
if err != nil {
return err
}
// config := conf.configs.Base
return nil
}
func (c *modCommands) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "Manage modules"
cmd.Long = `Various helpers to help manage the modules in your project's dependency graph.
Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git).
This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor".
` + commonUsageMod
cmd.RunE = nil
return nil
}
func (c *modCommands) PreRun(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2016 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.
@ -15,213 +15,385 @@ package commands
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/create"
"github.com/gohugoio/hugo/create/skeletons"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser"
"github.com/spf13/afero"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
)
func newNewCommand() *newCommand {
var (
force bool
contentType string
format string
)
var (
configFormat string
contentEditor string
contentType string
)
var c *newCommand
c = &newCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "content",
use: "content [path]",
short: "Create new content",
long: `Create a new content file and automatically set the date and title.
func init() {
newSiteCmd.Flags().StringVarP(&configFormat, "format", "f", "toml", "config & frontmatter format")
newSiteCmd.Flags().Bool("force", false, "init inside non-empty directory")
newCmd.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create")
newCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from")
newCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
newCmd.Flags().StringVar(&contentEditor, "editor", "", "edit new content with this editor, if provided")
newCmd.AddCommand(newSiteCmd)
newCmd.AddCommand(newThemeCmd)
}
var newCmd = &cobra.Command{
Use: "new [path]",
Short: "Create new content for your site",
Long: `Create a new content file and automatically set the date and title.
It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or site, they will be used.
If archetypes are provided in your theme or site, they will be used.`,
Ensure you run this within the root directory of your site.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
}
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return create.NewContent(h, contentType, args[0], force)
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return []string{}, cobra.ShellCompDirectiveNoFileComp
}
return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs
}
cmd.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create")
cmd.Flags().String("editor", "", "edit new content with this editor, if provided")
_ = cmd.RegisterFlagCompletionFunc("editor", cobra.NoFileCompletions)
cmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite file if it already exists")
applyLocalFlagsBuildConfig(cmd, r)
},
},
&simpleCommand{
name: "site",
use: "site [path]",
short: "Create a new site",
long: `Create a new site at the specified path.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
}
createpath, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return err
}
RunE: NewContent,
}
cfg := config.New()
cfg.Set("workingDir", createpath)
cfg.Set("publishDir", "public")
var newSiteCmd = &cobra.Command{
Use: "site [path]",
Short: "Create a new site (skeleton)",
Long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
RunE: NewSite,
}
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg))
if err != nil {
return err
}
sourceFs := conf.fs.Source
var newThemeCmd = &cobra.Command{
Use: "theme [name]",
Short: "Create a new theme",
Long: `Create a new theme (skeleton) called [name] in the current directory.
New theme is a skeleton. Please add content to the touched files. Add your
name to the copyright line in the license and adjust the theme.toml file
as you see fit.`,
RunE: NewTheme,
}
err = skeletons.CreateSite(createpath, sourceFs, force, format)
if err != nil {
return err
}
// NewContent adds new content to a Hugo site.
func NewContent(cmd *cobra.Command, args []string) error {
cfg, err := InitializeConfig()
r.Printf("Congratulations! Your new Hugo site was created in %s.\n\n", createpath)
r.Println(c.newSiteNextStepsText(createpath, format))
return nil
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return []string{}, cobra.ShellCompDirectiveNoFileComp
}
return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs
}
cmd.Flags().BoolVarP(&force, "force", "f", false, "init inside non-empty directory")
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
},
},
&simpleCommand{
name: "theme",
use: "theme [name]",
short: "Create a new theme",
long: `Create a new theme with the specified name in the ./themes directory.
This generates a functional theme including template examples and sample content.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("theme name needs to be provided")
}
cfg := config.New()
cfg.Set("publishDir", "public")
conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg))
if err != nil {
return err
}
sourceFs := conf.fs.Source
createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0]))
r.Println("Creating new theme in", createpath)
err = skeletons.CreateTheme(createpath, sourceFs, format)
if err != nil {
return err
}
return nil
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return []string{}, cobra.ShellCompDirectiveNoFileComp
}
return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs
}
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
},
},
},
if err != nil {
return err
}
return c
c, err := newCommandeer(cfg)
if err != nil {
return err
}
if cmd.Flags().Changed("editor") {
c.Set("newContentEditor", contentEditor)
}
if len(args) < 1 {
return newUserError("path needs to be provided")
}
createPath := args[0]
var kind string
createPath, kind = newContentPathSection(createPath)
if contentType != "" {
kind = contentType
}
ps, err := helpers.NewPathSpec(cfg.Fs, cfg.Cfg)
if err != nil {
return err
}
// If a site isn't in use in the archetype template, we can skip the build.
siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) {
if !siteUsed {
return hugolib.NewSite(*cfg)
}
var s *hugolib.Site
if err := c.initSites(); err != nil {
return nil, err
}
if err := Hugo.Build(hugolib.BuildCfg{SkipRender: true, PrintStats: false}); err != nil {
return nil, err
}
s = Hugo.Sites[0]
if len(Hugo.Sites) > 1 {
// Find the best match.
for _, ss := range Hugo.Sites {
if strings.Contains(createPath, "."+ss.Language.Lang) {
s = ss
break
}
}
}
return s, nil
}
return create.NewContent(ps, siteFactory, kind, createPath)
}
type newCommand struct {
rootCmd *rootCommand
func doNewSite(fs *hugofs.Fs, basepath string, force bool) error {
archeTypePath := filepath.Join(basepath, "archetypes")
dirs := []string{
filepath.Join(basepath, "layouts"),
filepath.Join(basepath, "content"),
archeTypePath,
filepath.Join(basepath, "static"),
filepath.Join(basepath, "data"),
filepath.Join(basepath, "themes"),
}
commands []simplecobra.Commander
}
if exists, _ := helpers.Exists(basepath, fs.Source); exists {
if isDir, _ := helpers.IsDir(basepath, fs.Source); !isDir {
return errors.New(basepath + " already exists but not a directory")
}
func (c *newCommand) Commands() []simplecobra.Commander {
return c.commands
}
isEmpty, _ := helpers.IsEmpty(basepath, fs.Source)
func (c *newCommand) Name() string {
return "new"
}
switch {
case !isEmpty && !force:
return errors.New(basepath + " already exists and is not empty")
case !isEmpty && force:
all := append(dirs, filepath.Join(basepath, "config."+configFormat))
for _, path := range all {
if exists, _ := helpers.Exists(path, fs.Source); exists {
return errors.New(path + " already exists")
}
}
}
}
for _, dir := range dirs {
if err := fs.Source.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("Failed to create dir: %s", err)
}
}
createConfig(fs, basepath, configFormat)
// Create a defaul archetype file.
helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"),
strings.NewReader(create.ArchetypeTemplateTemplate), fs.Source)
jww.FEEDBACK.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", basepath)
jww.FEEDBACK.Println(nextStepsText())
func (c *newCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *newCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "Create new content"
cmd.Long = `Create a new content file and automatically set the date and title.
It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or site, they will be used.
Ensure you run this within the root directory of your site.`
cmd.RunE = nil
return nil
}
func (c *newCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
c.rootCmd = cd.Root.Command.(*rootCommand)
return nil
}
func (c *newCommand) newSiteNextStepsText(path string, format string) string {
format = strings.ToLower(format)
func nextStepsText() string {
var nextStepsText bytes.Buffer
nextStepsText.WriteString(`Just a few more steps...
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
1. Change the current directory to ` + path + `.
2. Create or install a theme:
- Create a new theme with the command "hugo new theme <THEMENAME>"
- Or, install a theme from https://themes.gohugo.io/
3. Edit hugo.` + format + `, setting the "theme" property to the theme name.
4. Create new content with the command "hugo new content `)
1. Download a theme into the same-named folder.
Choose a theme from https://themes.gohugo.io/, or
create your own with the "hugo new theme <THEMENAME>" command.
2. Perhaps you want to add some content. You can add single files
with "hugo new `)
nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>"))
nextStepsText.WriteString(`".
5. Start the embedded web server with the command "hugo server --buildDrafts".
3. Start the built-in live server via "hugo server".
See documentation at https://gohugo.io/.`)
Visit https://gohugo.io/ for quickstart guide and full documentation.`)
return nextStepsText.String()
}
// NewSite creates a new Hugo site and initializes a structured Hugo directory.
func NewSite(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
}
createpath, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return newUserError(err)
}
forceNew, _ := cmd.Flags().GetBool("force")
return doNewSite(hugofs.NewDefault(viper.New()), createpath, forceNew)
}
// NewTheme creates a new Hugo theme.
func NewTheme(cmd *cobra.Command, args []string) error {
cfg, err := InitializeConfig()
if err != nil {
return err
}
if len(args) < 1 {
return newUserError("theme name needs to be provided")
}
c, err := newCommandeer(cfg)
if err != nil {
return err
}
createpath := c.PathSpec().AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0]))
jww.INFO.Println("creating theme at", createpath)
if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x {
return newUserError(createpath, "already exists")
}
mkdir(createpath, "layouts", "_default")
mkdir(createpath, "layouts", "partials")
touchFile(cfg.Fs.Source, createpath, "layouts", "index.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "404.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "list.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "single.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "header.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "footer.html")
mkdir(createpath, "archetypes")
archDefault := []byte("+++\n+++\n")
err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), cfg.Fs.Source)
if err != nil {
return err
}
mkdir(createpath, "static", "js")
mkdir(createpath, "static", "css")
by := []byte(`The MIT License (MIT)
Copyright (c) ` + time.Now().Format("2006") + ` YOUR_NAME_HERE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`)
err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE.md"), bytes.NewReader(by), cfg.Fs.Source)
if err != nil {
return err
}
createThemeMD(cfg.Fs, createpath)
return nil
}
func mkdir(x ...string) {
p := filepath.Join(x...)
err := os.MkdirAll(p, 0777) // before umask
if err != nil {
jww.FATAL.Fatalln(err)
}
}
func touchFile(fs afero.Fs, x ...string) {
inpath := filepath.Join(x...)
mkdir(filepath.Dir(inpath))
err := helpers.WriteToDisk(inpath, bytes.NewReader([]byte{}), fs)
if err != nil {
jww.FATAL.Fatalln(err)
}
}
func createThemeMD(fs *hugofs.Fs, inpath string) (err error) {
by := []byte(`# theme.toml template for a Hugo theme
# See https://github.com/gohugoio/hugoThemes#themetoml for an example
name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `"
license = "MIT"
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE.md"
description = ""
homepage = "http://example.com/"
tags = []
features = []
min_version = "0.25"
[author]
name = ""
homepage = ""
# If porting an existing theme
[original]
name = ""
homepage = ""
repo = ""
`)
err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs.Source)
if err != nil {
return
}
return nil
}
func newContentPathSection(path string) (string, string) {
// Forward slashes is used in all examples. Convert if needed.
// Issue #1133
createpath := filepath.FromSlash(path)
var section string
// assume the first directory is the section (kind)
if strings.Contains(createpath[1:], helpers.FilePathSeparator) {
section = helpers.GuessSection(createpath)
}
return createpath, section
}
func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
in := map[string]string{
"baseURL": "http://example.org/",
"title": "My New Hugo Site",
"languageCode": "en-us",
}
kind = parser.FormatSanitize(kind)
var buf bytes.Buffer
err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf)
if err != nil {
return err
}
return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source)
}

122
commands/new_test.go Normal file
View file

@ -0,0 +1,122 @@
// Copyright 2016 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 (
"path/filepath"
"testing"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Issue #1133
func TestNewContentPathSectionWithForwardSlashes(t *testing.T) {
p, s := newContentPathSection("/post/new.md")
assert.Equal(t, filepath.FromSlash("/post/new.md"), p)
assert.Equal(t, "post", s)
}
func checkNewSiteInited(fs *hugofs.Fs, basepath string, t *testing.T) {
paths := []string{
filepath.Join(basepath, "layouts"),
filepath.Join(basepath, "content"),
filepath.Join(basepath, "archetypes"),
filepath.Join(basepath, "static"),
filepath.Join(basepath, "data"),
filepath.Join(basepath, "config.toml"),
}
for _, path := range paths {
_, err := fs.Source.Stat(path)
require.NoError(t, err)
}
}
func TestDoNewSite(t *testing.T) {
basepath := filepath.Join("base", "blog")
_, fs := newTestCfg()
require.NoError(t, doNewSite(fs, basepath, false))
checkNewSiteInited(fs, basepath, t)
}
func TestDoNewSite_noerror_base_exists_but_empty(t *testing.T) {
basepath := filepath.Join("base", "blog")
_, fs := newTestCfg()
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
require.NoError(t, doNewSite(fs, basepath, false))
}
func TestDoNewSite_error_base_exists(t *testing.T) {
basepath := filepath.Join("base", "blog")
_, fs := newTestCfg()
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
_, err := fs.Source.Create(filepath.Join(basepath, "foo"))
require.NoError(t, err)
// Since the directory already exists and isn't empty, expect an error
require.Error(t, doNewSite(fs, basepath, false))
}
func TestDoNewSite_force_empty_dir(t *testing.T) {
basepath := filepath.Join("base", "blog")
_, fs := newTestCfg()
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
require.NoError(t, doNewSite(fs, basepath, true))
checkNewSiteInited(fs, basepath, t)
}
func TestDoNewSite_error_force_dir_inside_exists(t *testing.T) {
basepath := filepath.Join("base", "blog")
_, fs := newTestCfg()
contentPath := filepath.Join(basepath, "content")
require.NoError(t, fs.Source.MkdirAll(contentPath, 777))
require.Error(t, doNewSite(fs, basepath, true))
}
func TestDoNewSite_error_force_config_inside_exists(t *testing.T) {
basepath := filepath.Join("base", "blog")
_, fs := newTestCfg()
configPath := filepath.Join(basepath, "config.toml")
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
_, err := fs.Source.Create(configPath)
require.NoError(t, err)
require.Error(t, doNewSite(fs, basepath, true))
}
func newTestCfg() (*viper.Viper, *hugofs.Fs) {
v := viper.New()
fs := hugofs.NewMem(v)
v.SetFs(fs.Source)
return v, fs
}

View file

@ -1,4 +1,6 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// +build release
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,40 +16,53 @@
package commands
import (
"context"
"errors"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/releaser"
"github.com/spf13/cobra"
)
// Note: This is a command only meant for internal use and must be run
// via "go run -tags release main.go release" on the actual code base that is in the release.
func newReleaseCommand() simplecobra.Commander {
var (
step int
skipPush bool
try bool
)
func init() {
HugoCmd.AddCommand(createReleaser().cmd)
}
return &simpleCommand{
name: "release",
short: "Release a new version of Hugo",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
rel, err := releaser.New(skipPush, try, step)
if err != nil {
return err
}
type releaseCommandeer struct {
cmd *cobra.Command
return rel.Run()
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.Hidden = true
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes")
cmd.PersistentFlags().IntVarP(&step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
_ = cmd.RegisterFlagCompletionFunc("step", cobra.FixedCompletions([]string{"1", "2"}, cobra.ShellCompDirectiveNoFileComp))
version string
skipPublish bool
try bool
step int
}
func createReleaser() *releaseCommandeer {
// Note: This is a command only meant for internal use and must be run
// via "go run -tags release main.go release" on the actual code base that is in the release.
r := &releaseCommandeer{
cmd: &cobra.Command{
Use: "release",
Short: "Release a new version of Hugo.",
Hidden: true,
},
}
r.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return r.release()
}
r.cmd.PersistentFlags().StringVarP(&r.version, "rel", "r", "", "new release version, i.e. 0.25.1")
r.cmd.PersistentFlags().IntVarP(&r.step, "step", "s", -1, "release step, defaults to -1 for all steps.")
r.cmd.PersistentFlags().BoolVarP(&r.skipPublish, "skip-publish", "", false, "skip all publishing pipes of the release")
r.cmd.PersistentFlags().BoolVarP(&r.try, "try", "", false, "simulate a release, i.e. no changes")
return r
}
func (r *releaseCommandeer) release() error {
if r.version == "" {
return errors.New("must set the --rel flag to the relevant version number")
}
return releaser.New(r.version, r.step, r.skipPublish, r.try).Run()
}

File diff suppressed because it is too large Load diff

58
commands/server_test.go Normal file
View file

@ -0,0 +1,58 @@
// 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 (
"testing"
"github.com/spf13/viper"
)
func TestFixURL(t *testing.T) {
type data struct {
TestName string
CLIBaseURL string
CfgBaseURL string
AppendPort bool
Port int
Result string
}
tests := []data{
{"Basic http localhost", "", "http://foo.com", true, 1313, "http://localhost:1313/"},
{"Basic https production, http localhost", "", "https://foo.com", true, 1313, "http://localhost:1313/"},
{"Basic subdir", "", "http://foo.com/bar", true, 1313, "http://localhost:1313/bar/"},
{"Basic production", "http://foo.com", "http://foo.com", false, 80, "http://foo.com/"},
{"Production subdir", "http://foo.com/bar", "http://foo.com/bar", false, 80, "http://foo.com/bar/"},
{"No http", "", "foo.com", true, 1313, "//localhost:1313/"},
{"Override configured port", "", "foo.com:2020", true, 1313, "//localhost:1313/"},
{"No http production", "foo.com", "foo.com", false, 80, "//foo.com/"},
{"No http production with port", "foo.com", "foo.com", true, 2020, "//foo.com:2020/"},
{"No config", "", "", true, 1313, "//localhost:1313/"},
}
for i, test := range tests {
v := viper.New()
baseURL = test.CLIBaseURL
v.Set("baseURL", test.CfgBaseURL)
serverAppend = test.AppendPort
serverPort = test.Port
result, err := fixURL(v, baseURL)
if err != nil {
t.Errorf("Test #%d %s: unexpected error %s", i, test.TestName, err)
}
if result != test.Result {
t.Errorf("Test #%d %s: expected %q, got %q", i, test.TestName, test.Result, result)
}
}
}

155
commands/undraft.go Normal file
View file

@ -0,0 +1,155 @@
// 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 (
"bytes"
"errors"
"os"
"time"
"github.com/gohugoio/hugo/parser"
"github.com/spf13/cobra"
)
var undraftCmd = &cobra.Command{
Use: "undraft path/to/content",
Short: "Undraft resets the content's draft status",
Long: `Undraft resets the content's draft status
and updates the date to the current date and time.
If the content's draft status is 'False', nothing is done.`,
RunE: Undraft,
}
// Undraft publishes the specified content by setting its draft status
// to false and setting its publish date to now. If the specified content is
// not a draft, it will log an error.
func Undraft(cmd *cobra.Command, args []string) error {
cfg, err := InitializeConfig()
if err != nil {
return err
}
if len(args) < 1 {
return newUserError("a piece of content needs to be specified")
}
location := args[0]
// open the file
f, err := cfg.Fs.Source.Open(location)
if err != nil {
return err
}
// get the page from file
p, err := parser.ReadFrom(f)
f.Close()
if err != nil {
return err
}
w, err := undraftContent(p)
if err != nil {
return newSystemErrorF("an error occurred while undrafting %q: %s", location, err)
}
f, err = cfg.Fs.Source.OpenFile(location, os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return newSystemErrorF("%q not be undrafted due to error opening file to save changes: %q\n", location, err)
}
defer f.Close()
_, err = w.WriteTo(f)
if err != nil {
return newSystemErrorF("%q not be undrafted due to save error: %q\n", location, err)
}
return nil
}
// undraftContent: if the content is a draft, change its draft status to
// 'false' and set the date to time.Now(). If the draft status is already
// 'false', don't do anything.
func undraftContent(p parser.Page) (bytes.Buffer, error) {
var buff bytes.Buffer
// get the metadata; easiest way to see if it's a draft
meta, err := p.Metadata()
if err != nil {
return buff, err
}
// since the metadata was obtainable, we can also get the key/value separator for
// Front Matter
fm := p.FrontMatter()
if fm == nil {
return buff, errors.New("Front Matter was found, nothing was finalized")
}
var isDraft, gotDate bool
var date string
L:
for k, v := range meta.(map[string]interface{}) {
switch k {
case "draft":
if !v.(bool) {
return buff, errors.New("not a Draft: nothing was done")
}
isDraft = true
if gotDate {
break L
}
case "date":
date = v.(string) // capture the value to make replacement easier
gotDate = true
if isDraft {
break L
}
}
}
// if draft wasn't found in FrontMatter, it isn't a draft.
if !isDraft {
return buff, errors.New("not a Draft: nothing was done")
}
// get the front matter as bytes and split it into lines
var lineEnding []byte
fmLines := bytes.Split(fm, []byte("\n"))
if len(fmLines) == 1 { // if the result is only 1 element, try to split on dos line endings
fmLines = bytes.Split(fm, []byte("\r\n"))
if len(fmLines) == 1 {
return buff, errors.New("unable to split FrontMatter into lines")
}
lineEnding = append(lineEnding, []byte("\r\n")...)
} else {
lineEnding = append(lineEnding, []byte("\n")...)
}
// Write the front matter lines to the buffer, replacing as necessary
for _, v := range fmLines {
pos := bytes.Index(v, []byte("draft"))
if pos != -1 {
continue
}
pos = bytes.Index(v, []byte("date"))
if pos != -1 { // if date field wasn't found, add it
v = bytes.Replace(v, []byte(date), []byte(time.Now().Format(time.RFC3339)), 1)
}
buff.Write(v)
buff.Write(lineEnding)
}
// append the actual content
buff.Write(p.Content())
return buff, nil
}

87
commands/undraft_test.go Normal file
View file

@ -0,0 +1,87 @@
// 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
// TODO Support Mac Encoding (\r)
import (
"bytes"
"strings"
"testing"
"time"
"github.com/gohugoio/hugo/parser"
)
var (
jsonFM = "{\n \"date\": \"12-04-06\",\n \"title\": \"test json\"\n}"
jsonDraftFM = "{\n \"draft\": true,\n \"date\": \"12-04-06\",\n \"title\":\"test json\"\n}"
tomlFM = "+++\n date= \"12-04-06\"\n title= \"test toml\"\n+++"
tomlDraftFM = "+++\n draft= true\n date= \"12-04-06\"\n title=\"test toml\"\n+++"
yamlFM = "---\n date: \"12-04-06\"\n title: \"test yaml\"\n---"
yamlDraftFM = "---\n draft: true\n date: \"12-04-06\"\n title: \"test yaml\"\n---"
yamlYesDraftFM = "---\n draft: yes\n date: \"12-04-06\"\n title: \"test yaml\"\n---"
)
func TestUndraftContent(t *testing.T) {
tests := []struct {
fm string
expectedErr string
}{
{jsonFM, "not a Draft: nothing was done"},
{jsonDraftFM, ""},
{tomlFM, "not a Draft: nothing was done"},
{tomlDraftFM, ""},
{yamlFM, "not a Draft: nothing was done"},
{yamlDraftFM, ""},
{yamlYesDraftFM, ""},
}
for i, test := range tests {
r := bytes.NewReader([]byte(test.fm))
p, _ := parser.ReadFrom(r)
res, err := undraftContent(p)
if test.expectedErr != "" {
if err == nil {
t.Errorf("[%d] Expected error, got none", i)
continue
}
if err.Error() != test.expectedErr {
t.Errorf("[%d] Expected %q, got %q", i, test.expectedErr, err)
continue
}
} else {
r = bytes.NewReader(res.Bytes())
p, _ = parser.ReadFrom(r)
meta, err := p.Metadata()
if err != nil {
t.Errorf("[%d] unexpected error %q", i, err)
continue
}
for k, v := range meta.(map[string]interface{}) {
if k == "draft" {
if v.(bool) {
t.Errorf("[%d] Expected %q to be \"false\", got \"true\"", i, k)
continue
}
}
if k == "date" {
if !strings.HasPrefix(v.(string), time.Now().Format("2006-01-02")) {
t.Errorf("[%d] Expected %v to start with %v", i, v.(string), time.Now().Format("2006-01-02"))
}
}
}
}
}
}

80
commands/version.go Normal file
View file

@ -0,0 +1,80 @@
// 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 (
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib"
"github.com/kardianos/osext"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's.`,
RunE: func(cmd *cobra.Command, args []string) error {
printHugoVersion()
return nil
},
}
func printHugoVersion() {
if hugolib.BuildDate == "" {
setBuildDate() // set the build date from executable's mdate
} else {
formatBuildDate() // format the compile time
}
if hugolib.CommitHash == "" {
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
} else {
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s-%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, strings.ToUpper(hugolib.CommitHash), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
}
}
// setBuildDate checks the ModTime of the Hugo executable and returns it as a
// formatted string. This assumes that the executable name is Hugo, if it does
// not exist, an empty string will be returned. This is only called if the
// hugolib.BuildDate wasn't set during compile time.
//
// osext is used for cross-platform.
func setBuildDate() {
fname, _ := osext.Executable()
dir, err := filepath.Abs(filepath.Dir(fname))
if err != nil {
jww.ERROR.Println(err)
return
}
fi, err := os.Lstat(filepath.Join(dir, filepath.Base(fname)))
if err != nil {
jww.ERROR.Println(err)
return
}
t := fi.ModTime()
hugolib.BuildDate = t.Format(time.RFC3339)
}
// formatBuildDate formats the hugolib.BuildDate according to the value in
// .Params.DateFormat, if it's set.
func formatBuildDate() {
t, _ := time.Parse("2006-01-02T15:04:05-0700", hugolib.BuildDate)
hugolib.BuildDate = t.Format(time.RFC3339)
}

View file

@ -1,152 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// 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 collections
import (
"fmt"
"reflect"
)
// 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 any, from ...any) (any, error) {
if len(from) == 0 {
return to, nil
}
tov, toIsNil := indirect(reflect.ValueOf(to))
toIsNil = toIsNil || to == nil
var tot reflect.Type
if !toIsNil {
if tov.Kind() == reflect.Slice {
// Create a copy of tov, so we don't modify the original.
c := reflect.MakeSlice(tov.Type(), tov.Len(), tov.Len()+len(from))
reflect.Copy(c, tov)
tov = c
}
if tov.Kind() != reflect.Slice {
return nil, fmt.Errorf("expected a slice, got %T", to)
}
tot = tov.Type().Elem()
if tot.Kind() == reflect.Slice {
totvt := tot.Elem()
fromvs := make([]reflect.Value, len(from))
for i, f := range from {
fromv := reflect.ValueOf(f)
fromt := fromv.Type()
if fromt.Kind() == reflect.Slice {
fromt = fromt.Elem()
}
if totvt != fromt {
return nil, fmt.Errorf("cannot append slice of %s to slice of %s", fromt, totvt)
} else {
fromvs[i] = fromv
}
}
return reflect.Append(tov, fromvs...).Interface(), nil
}
toIsNil = tov.Len() == 0
if len(from) == 1 {
fromv := reflect.ValueOf(from[0])
if !fromv.IsValid() {
// from[0] is nil
return appendToInterfaceSliceFromValues(tov, fromv)
}
fromt := fromv.Type()
if fromt.Kind() == reflect.Slice {
fromt = fromt.Elem()
}
if fromv.Kind() == reflect.Slice {
if toIsNil {
// If we get nil []string, we just return the []string
return from[0], nil
}
// If we get []string []string, we append the from slice to to
if tot == fromt {
return reflect.AppendSlice(tov, fromv).Interface(), nil
} else if !fromt.AssignableTo(tot) {
// Fall back to a []interface{} slice.
return appendToInterfaceSliceFromValues(tov, fromv)
}
}
}
}
if toIsNil {
return Slice(from...), nil
}
for _, f := range from {
fv := reflect.ValueOf(f)
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
// Fall back to a []interface{} slice.
tov, _ := indirect(reflect.ValueOf(to))
return appendToInterfaceSlice(tov, from...)
}
tov = reflect.Append(tov, fv)
}
return tov.Interface(), nil
}
func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, error) {
var tos []any
for _, slice := range []reflect.Value{slice1, slice2} {
if !slice.IsValid() {
tos = append(tos, nil)
continue
}
for i := range slice.Len() {
tos = append(tos, slice.Index(i).Interface())
}
}
return tos, nil
}
func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
var tos []any
for i := range tov.Len() {
tos = append(tos, tov.Index(i).Interface())
}
tos = append(tos, from...)
return tos, nil
}
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
// TODO(bep) consolidate
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
break
}
}
return v, false
}

View file

@ -1,213 +0,0 @@
// 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 collections
import (
"html/template"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
)
func TestAppend(t *testing.T) {
t.Parallel()
c := qt.New(t)
for i, test := range []struct {
start any
addend []any
expected any
}{
{[]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"}},
[]any{&tstSlicer{"c"}},
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}},
},
{
&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
[]any{&tstSlicer{"c"}},
tstSlicers{
&tstSlicer{"a"},
&tstSlicer{"b"},
&tstSlicer{"c"},
},
},
{
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}},
[]any{&tstSlicerIn1{"c"}},
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}},
},
// https://github.com/gohugoio/hugo/issues/5361
{
[]string{"a", "b"},
[]any{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
[]any{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}},
},
{
[]string{"a", "b"},
[]any{&tstSlicer{"a"}},
[]any{"a", "b", &tstSlicer{"a"}},
},
// Errors
{"", []any{[]string{"a", "b"}}, false},
// No string concatenation.
{
"ab",
[]any{"c"},
false,
},
{[]string{"a", "b"}, []any{nil}, []any{"a", "b", nil}},
{[]string{"a", "b"}, []any{nil, "d", nil}, []any{"a", "b", nil, "d", nil}},
{[]any{"a", nil, "c"}, []any{"d", nil, "f"}, []any{"a", nil, "c", "d", nil, "f"}},
{[]string{"a", "b"}, []any{}, []string{"a", "b"}},
} {
result, err := Append(test.start, test.addend...)
if b, ok := test.expected.(bool); ok && !b {
c.Assert(err, qt.Not(qt.IsNil))
continue
}
c.Assert(err, qt.IsNil)
c.Assert(result, qt.DeepEquals, test.expected, qt.Commentf("test: [%d] %v", i, test))
}
}
// #11093
func TestAppendToMultiDimensionalSlice(t *testing.T) {
t.Parallel()
c := qt.New(t)
for _, test := range []struct {
to any
from []any
expected any
}{
{
[][]string{{"a", "b"}},
[]any{[]string{"c", "d"}},
[][]string{
{"a", "b"},
{"c", "d"},
},
},
{
[][]string{{"a", "b"}},
[]any{[]string{"c", "d"}, []string{"e", "f"}},
[][]string{
{"a", "b"},
{"c", "d"},
{"e", "f"},
},
},
{
[][]string{{"a", "b"}},
[]any{[]int{1, 2}},
false,
},
} {
result, err := Append(test.to, test.from...)
if b, ok := test.expected.(bool); ok && !b {
c.Assert(err, qt.Not(qt.IsNil))
} else {
c.Assert(err, qt.IsNil)
c.Assert(result, qt.DeepEquals, test.expected)
}
}
}
func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) {
t.Parallel()
c := qt.New(t)
slice := make([]string, 0, 100)
slice = append(slice, "a", "b")
result, err := Append(slice, "c")
c.Assert(err, qt.IsNil)
slice[0] = "d"
c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(slice, qt.DeepEquals, []string{"d", "b"})
}
func TestIndirect(t *testing.T) {
t.Parallel()
c := qt.New(t)
type testStruct struct {
Field string
}
var (
nilPtr *testStruct
nilIface interface{} = nil
nonNilIface interface{} = &testStruct{Field: "hello"}
)
tests := []struct {
name string
input any
wantKind reflect.Kind
wantNil bool
}{
{
name: "nil pointer",
input: nilPtr,
wantKind: reflect.Ptr,
wantNil: true,
},
{
name: "nil interface",
input: nilIface,
wantKind: reflect.Invalid,
wantNil: false,
},
{
name: "non-nil pointer to struct",
input: &testStruct{Field: "abc"},
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "non-nil interface holding pointer",
input: nonNilIface,
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "plain value",
input: testStruct{Field: "xyz"},
wantKind: reflect.Struct,
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := reflect.ValueOf(tt.input)
got, isNil := indirect(v)
c.Assert(got.Kind(), qt.Equals, tt.wantKind)
c.Assert(isNil, qt.Equals, tt.wantNil)
})
}
}

View file

@ -1,21 +0,0 @@
// 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 collections contains common Hugo functionality related to collection
// handling.
package collections
// Grouper defines a very generic way to group items by a given key.
type Grouper interface {
Group(key any, items any) (any, error)
}

View file

@ -1,20 +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 collections
type Order interface {
// Ordinal is a zero-based ordinal that represents the order of an object
// in a collection.
Ordinal() int
}

View file

@ -1,95 +0,0 @@
// 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 collections
import (
"reflect"
"sort"
)
// Slicer defines a very generic way to create a typed slice. This is used
// in collections.Slice template func to get types such as Pages, PageGroups etc.
// instead of the less useful []interface{}.
type Slicer interface {
Slice(items any) (any, error)
}
// Slice returns a slice of all passed arguments.
func Slice(args ...any) any {
if len(args) == 0 {
return args
}
first := args[0]
firstType := reflect.TypeOf(first)
if firstType == nil {
return args
}
if g, ok := first.(Slicer); ok {
v, err := g.Slice(args)
if err == nil {
return v
}
// If Slice fails, the items are not of the same type and
// []interface{} is the best we can do.
return args
}
if len(args) > 1 {
// This can be a mix of types.
for i := 1; i < len(args); i++ {
if firstType != reflect.TypeOf(args[i]) {
// []interface{} is the best we can do
return args
}
}
}
slice := reflect.MakeSlice(reflect.SliceOf(firstType), len(args), len(args))
for i, arg := range args {
slice.Index(i).Set(reflect.ValueOf(arg))
}
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
}
type SortedStringSlice []string
// Contains returns true if s is in ss.
func (ss SortedStringSlice) Contains(s string) bool {
i := sort.SearchStrings(ss, s)
return i < len(ss) && ss[i] == s
}
// Count returns the number of times s is in ss.
func (ss SortedStringSlice) Count(s string) int {
var count int
i := sort.SearchStrings(ss, s)
for i < len(ss) && ss[i] == s {
count++
i++
}
return count
}

View file

@ -1,172 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// 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 collections
import (
"errors"
"testing"
qt "github.com/frankban/quicktest"
)
var (
_ Slicer = (*tstSlicer)(nil)
_ Slicer = (*tstSlicerIn1)(nil)
_ Slicer = (*tstSlicerIn2)(nil)
_ testSlicerInterface = (*tstSlicerIn1)(nil)
_ testSlicerInterface = (*tstSlicerIn1)(nil)
)
type testSlicerInterface interface {
Name() string
}
type testSlicerInterfaces []testSlicerInterface
type tstSlicerIn1 struct {
TheName string
}
type tstSlicerIn2 struct {
TheName string
}
type tstSlicer struct {
TheName string
}
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) {
case testSlicerInterface:
result[i] = vv
default:
return nil, errors.New("invalid type")
}
}
return result, nil
}
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) {
case testSlicerInterface:
result[i] = vv
default:
return nil, errors.New("invalid type")
}
}
return result, nil
}
func (p *tstSlicerIn1) Name() string {
return p.TheName
}
func (p *tstSlicerIn2) Name() string {
return p.TheName
}
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) {
case *tstSlicer:
result[i] = vv
default:
return nil, errors.New("invalid type")
}
}
return result, nil
}
type tstSlicers []*tstSlicer
func TestSlice(t *testing.T) {
t.Parallel()
c := qt.New(t)
for i, test := range []struct {
args []any
expected any
}{
{[]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)
result := Slice(test.args...)
c.Assert(test.expected, qt.DeepEquals, result, errMsg)
}
}
func TestSortedStringSlice(t *testing.T) {
t.Parallel()
c := qt.New(t)
var s SortedStringSlice = []string{"a", "b", "b", "b", "c", "d"}
c.Assert(s.Contains("a"), qt.IsTrue)
c.Assert(s.Contains("b"), qt.IsTrue)
c.Assert(s.Contains("z"), qt.IsFalse)
c.Assert(s.Count("b"), qt.Equals, 3)
c.Assert(s.Count("z"), qt.Equals, 0)
c.Assert(s.Count("a"), qt.Equals, 1)
}
func TestStringSliceToInterfaceSlice(t *testing.T) {
t.Parallel()
c := qt.New(t)
tests := []struct {
name string
in []string
want []any
}{
{
name: "empty slice",
in: []string{},
want: []any{},
},
{
name: "single element",
in: []string{"hello"},
want: []any{"hello"},
},
{
name: "multiple elements",
in: []string{"a", "b", "c"},
want: []any{"a", "b", "c"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := StringSliceToInterfaceSlice(tt.in)
c.Assert(got, qt.DeepEquals, tt.want)
})
}
}

View file

@ -1,82 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collections
import "slices"
import "sync"
// Stack is a simple LIFO stack that is safe for concurrent use.
type Stack[T any] struct {
items []T
zero T
mu sync.RWMutex
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{}
}
func (s *Stack[T]) Push(item T) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.items) == 0 {
return s.zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.items) == 0 {
return s.zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.items)
}
func (s *Stack[T]) Drain() []T {
s.mu.Lock()
defer s.mu.Unlock()
items := s.items
s.items = nil
return items
}
func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
s.mu.Lock()
defer s.mu.Unlock()
var items []T
for i := len(s.items) - 1; i >= 0; i-- {
if predicate(s.items[i]) {
items = append(items, s.items[i])
s.items = slices.Delete(s.items, i, i+1)
}
}
return items
}

View file

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

View file

@ -1,49 +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 constants
// Error/Warning IDs.
// Do not change these values.
const (
// IDs for remote errors in tpl/data.
ErrRemoteGetJSON = "error-remote-getjson"
ErrRemoteGetCSV = "error-remote-getcsv"
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle"
)
// Field/method names with special meaning.
const (
FieldRelPermalink = "RelPermalink"
FieldPermalink = "Permalink"
)
// IsFieldRelOrPermalink returns whether the given name is a RelPermalink or Permalink.
func IsFieldRelOrPermalink(name string) bool {
return name == FieldRelPermalink || name == FieldPermalink
}
// Resource transformations.
const (
ResourceTransformationFingerprint = "fingerprint"
)
// IsResourceTransformationPermalinkHash returns whether the given name is a resource transformation that changes the permalink based on the content.
func IsResourceTransformationPermalinkHash(name string) bool {
return name == ResourceTransformationFingerprint
}

View file

@ -1,2 +0,0 @@
// Package common provides common helper functionality for Hugo.
package common

View file

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

View file

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

View file

@ -1,46 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hcontext
import "context"
// ContextDispatcher is a generic interface for setting and getting values from a context.
type ContextDispatcher[T any] interface {
Set(ctx context.Context, value T) context.Context
Get(ctx context.Context) T
}
// NewContextDispatcher creates a new ContextDispatcher with the given key.
func NewContextDispatcher[T any, R comparable](key R) ContextDispatcher[T] {
return keyInContext[T, R]{
id: key,
}
}
type keyInContext[T any, R comparable] struct {
zero T
id R
}
func (f keyInContext[T, R]) Get(ctx context.Context) T {
v := ctx.Value(f.id)
if v == nil {
return f.zero
}
return v.(T)
}
func (f keyInContext[T, R]) Set(ctx context.Context, value T) context.Context {
return context.WithValue(ctx, f.id, value)
}

View file

@ -1,170 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package herrors contains common Hugo errors and error related utilities.
package herrors
import (
"io"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/common/text"
)
// LineMatcher contains the elements used to match an error to a line
type LineMatcher struct {
Position text.Position
Error error
LineNumber int
Offset int
Line string
}
// LineMatcherFn is used to match a line with an error.
// It returns the column number or 0 if the line was found, but column could not be determined. Returns -1 if no line match.
type LineMatcherFn func(m LineMatcher) int
// SimpleLineMatcher simply matches by line number.
var SimpleLineMatcher = func(m LineMatcher) int {
if m.Position.LineNumber == m.LineNumber {
// We found the line, but don't know the column.
return 0
}
return -1
}
// NopLineMatcher is a matcher that always returns 1.
// This will effectively give line 1, column 1.
var NopLineMatcher = func(m LineMatcher) int {
return 1
}
// OffsetMatcher is a line matcher that matches by offset.
var OffsetMatcher = func(m LineMatcher) int {
if m.Offset+len(m.Line) >= m.Position.Offset {
// We found the line, but return 0 to signal that we want to determine
// the column from the error.
return 0
}
return -1
}
// ContainsMatcher is a line matcher that matches by line content.
func ContainsMatcher(text string) func(m LineMatcher) int {
return func(m LineMatcher) int {
if idx := strings.Index(m.Line, text); idx != -1 {
return idx + 1
}
return -1
}
}
// ErrorContext contains contextual information about an error. This will
// typically be the lines surrounding some problem in a file.
type ErrorContext struct {
// If a match will contain the matched line and up to 2 lines before and after.
// Will be empty if no match.
Lines []string
// The position of the error in the Lines above. 0 based.
LinesPos int
// The position of the content in the file. Note that this may be different from the error's position set
// in FileError.
Position text.Position
// The lexer to use for syntax highlighting.
// https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages
ChromaLexer string
}
func chromaLexerFromType(fileType string) string {
switch fileType {
case "html", "htm":
return "go-html-template"
}
return fileType
}
func extNoDelimiter(filename string) string {
return strings.TrimPrefix(filepath.Ext(filename), ".")
}
func chromaLexerFromFilename(filename string) string {
if strings.Contains(filename, "layouts") {
return "go-html-template"
}
ext := extNoDelimiter(filename)
return chromaLexerFromType(ext)
}
func locateErrorInString(src string, matcher LineMatcherFn) *ErrorContext {
return locateError(strings.NewReader(src), &fileError{}, matcher)
}
func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext {
if le == nil {
panic("must provide an error")
}
ectx := &ErrorContext{LinesPos: -1, Position: text.Position{Offset: -1}}
b, err := io.ReadAll(r)
if err != nil {
return ectx
}
lines := strings.Split(string(b), "\n")
lineNo := 0
posBytes := 0
for li, line := range lines {
lineNo = li + 1
m := LineMatcher{
Position: le.Position(),
Error: le,
LineNumber: lineNo,
Offset: posBytes,
Line: line,
}
v := matches(m)
if ectx.LinesPos == -1 && v != -1 {
ectx.Position.LineNumber = lineNo
ectx.Position.ColumnNumber = v
break
}
posBytes += len(line)
}
if ectx.Position.LineNumber > 0 {
low := max(ectx.Position.LineNumber-3, 0)
if ectx.Position.LineNumber > 2 {
ectx.LinesPos = 2
} else {
ectx.LinesPos = ectx.Position.LineNumber - 1
}
high := min(ectx.Position.LineNumber+2, len(lines))
ectx.Lines = lines[low:high]
}
return ectx
}

View file

@ -1,152 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package herrors contains common Hugo errors and error related utilities.
package herrors
import (
"strings"
"testing"
qt "github.com/frankban/quicktest"
)
func TestErrorLocator(t *testing.T) {
c := qt.New(t)
lineMatcher := func(m LineMatcher) int {
if strings.Contains(m.Line, "THEONE") {
return 1
}
return -1
}
lines := `LINE 1
LINE 2
LINE 3
LINE 4
This is THEONE
LINE 6
LINE 7
LINE 8
`
location := locateErrorInString(lines, lineMatcher)
pos := location.Position
c.Assert(location.Lines, qt.DeepEquals, []string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"})
c.Assert(pos.LineNumber, qt.Equals, 5)
c.Assert(location.LinesPos, qt.Equals, 2)
locate := func(s string, m LineMatcherFn) *ErrorContext {
ctx := locateErrorInString(s, m)
return ctx
}
c.Assert(locate(`This is THEONE`, lineMatcher).Lines, qt.DeepEquals, []string{"This is THEONE"})
location = locateErrorInString(`L1
This is THEONE
L2
`, lineMatcher)
pos = location.Position
c.Assert(pos.LineNumber, qt.Equals, 2)
c.Assert(location.LinesPos, qt.Equals, 1)
c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "This is THEONE", "L2", ""})
location = locate(`This is THEONE
L2
`, lineMatcher)
c.Assert(location.LinesPos, qt.Equals, 0)
c.Assert(location.Lines, qt.DeepEquals, []string{"This is THEONE", "L2", ""})
location = locate(`L1
This THEONE
`, lineMatcher)
c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "This THEONE", ""})
c.Assert(location.LinesPos, qt.Equals, 1)
location = locate(`L1
L2
This THEONE
`, lineMatcher)
c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "L2", "This THEONE", ""})
c.Assert(location.LinesPos, qt.Equals, 2)
location = locateErrorInString("NO MATCH", lineMatcher)
pos = location.Position
c.Assert(pos.LineNumber, qt.Equals, 0)
c.Assert(location.LinesPos, qt.Equals, -1)
c.Assert(len(location.Lines), qt.Equals, 0)
lineMatcher = func(m LineMatcher) int {
if m.LineNumber == 6 {
return 1
}
return -1
}
location = locateErrorInString(`A
B
C
D
E
F
G
H
I
J`, lineMatcher)
pos = location.Position
c.Assert(location.Lines, qt.DeepEquals, []string{"D", "E", "F", "G", "H"})
c.Assert(pos.LineNumber, qt.Equals, 6)
c.Assert(location.LinesPos, qt.Equals, 2)
// Test match EOF
lineMatcher = func(m LineMatcher) int {
if m.LineNumber == 4 {
return 1
}
return -1
}
location = locateErrorInString(`A
B
C
`, lineMatcher)
pos = location.Position
c.Assert(location.Lines, qt.DeepEquals, []string{"B", "C", ""})
c.Assert(pos.LineNumber, qt.Equals, 4)
c.Assert(location.LinesPos, qt.Equals, 2)
offsetMatcher := func(m LineMatcher) int {
if m.Offset == 1 {
return 1
}
return -1
}
location = locateErrorInString(`A
B
C
D
E`, offsetMatcher)
pos = location.Position
c.Assert(location.Lines, qt.DeepEquals, []string{"A", "B", "C", "D"})
c.Assert(pos.LineNumber, qt.Equals, 2)
c.Assert(location.LinesPos, qt.Equals, 1)
}

View file

@ -1,187 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package herrors contains common Hugo errors and error related utilities.
package herrors
import (
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime"
"runtime/debug"
"strings"
"time"
)
// PrintStackTrace prints the current stacktrace to w.
func PrintStackTrace(w io.Writer) {
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
fmt.Fprintf(w, "%s", buf)
}
// ErrorSender is a, typically, non-blocking error handler.
type ErrorSender interface {
SendError(err error)
}
// 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 ...any) {
if r := recover(); r != nil {
fmt.Println("ERR:", r)
args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
fmt.Println(args...)
}
}
// IsTimeoutError returns true if the given error is or contains a TimeoutError.
func IsTimeoutError(err error) bool {
return errors.Is(err, &TimeoutError{})
}
type TimeoutError struct {
Duration time.Duration
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("timeout after %s", e.Duration)
}
func (e *TimeoutError) Is(target error) bool {
_, ok := target.(*TimeoutError)
return ok
}
// errMessage wraps an error with a message.
type errMessage struct {
msg string
err error
}
func (e *errMessage) Error() string {
return e.msg
}
func (e *errMessage) Unwrap() error {
return e.err
}
// IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError.
func IsFeatureNotAvailableError(err error) bool {
return errors.Is(err, &FeatureNotAvailableError{})
}
// ErrFeatureNotAvailable denotes that a feature is unavailable.
//
// 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 = &FeatureNotAvailableError{Cause: errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")}
// FeatureNotAvailableError is an error type used to signal that a feature is not available.
type FeatureNotAvailableError struct {
Cause error
}
func (e *FeatureNotAvailableError) Unwrap() error {
return e.Cause
}
func (e *FeatureNotAvailableError) Error() string {
return e.Cause.Error()
}
func (e *FeatureNotAvailableError) Is(target error) bool {
_, ok := target.(*FeatureNotAvailableError)
return ok
}
// Must panics if err != nil.
func Must(err error) {
if err != nil {
panic(err)
}
}
// IsNotExist returns true if the error is a file not found error.
// Unlike os.IsNotExist, this also considers wrapped errors.
func IsNotExist(err error) bool {
if os.IsNotExist(err) {
return true
}
// os.IsNotExist does not consider wrapped errors.
if os.IsNotExist(errors.Unwrap(err)) {
return true
}
return false
}
// IsExist returns true if the error is a file exists error.
// Unlike os.IsExist, this also considers wrapped errors.
func IsExist(err error) bool {
if os.IsExist(err) {
return true
}
// os.IsExist does not consider wrapped errors.
if os.IsExist(errors.Unwrap(err)) {
return true
}
return false
}
var nilPointerErrRe = regexp.MustCompile(`at <(.*)>: error calling (.*?): runtime error: invalid memory address or nil pointer dereference`)
const deferredPrefix = "__hdeferred/"
var deferredStringToRemove = regexp.MustCompile(`executing "__hdeferred/.*?" `)
// ImproveRenderErr improves the error message for rendering errors.
func ImproveRenderErr(inErr error) (outErr error) {
outErr = inErr
msg := improveIfNilPointerMsg(inErr)
if msg != "" {
outErr = &errMessage{msg: msg, err: outErr}
}
if strings.Contains(inErr.Error(), deferredPrefix) {
msg := deferredStringToRemove.ReplaceAllString(inErr.Error(), "executing ")
outErr = &errMessage{msg: msg, err: outErr}
}
return
}
func improveIfNilPointerMsg(inErr error) string {
m := nilPointerErrRe.FindStringSubmatch(inErr.Error())
if len(m) == 0 {
return ""
}
call := m[1]
field := m[2]
parts := strings.Split(call, ".")
if len(parts) < 2 {
return ""
}
receiverName := parts[len(parts)-2]
receiver := strings.Join(parts[:len(parts)-1], ".")
s := fmt.Sprintf(" %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field)
return nilPointerErrRe.ReplaceAllString(inErr.Error(), s)
}

View file

@ -1,45 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package herrors
import (
"errors"
"fmt"
"testing"
qt "github.com/frankban/quicktest"
"github.com/spf13/afero"
)
func TestIsNotExist(t *testing.T) {
c := qt.New(t)
c.Assert(IsNotExist(afero.ErrFileNotFound), qt.Equals, true)
c.Assert(IsNotExist(afero.ErrFileExists), qt.Equals, false)
c.Assert(IsNotExist(afero.ErrDestinationExists), qt.Equals, false)
c.Assert(IsNotExist(nil), qt.Equals, false)
c.Assert(IsNotExist(fmt.Errorf("foo")), qt.Equals, false)
// os.IsNotExist returns false for wrapped errors.
c.Assert(IsNotExist(fmt.Errorf("foo: %w", afero.ErrFileNotFound)), qt.Equals, true)
}
func TestIsFeatureNotAvailableError(t *testing.T) {
c := qt.New(t)
c.Assert(IsFeatureNotAvailableError(ErrFeatureNotAvailable), qt.Equals, true)
c.Assert(IsFeatureNotAvailableError(&FeatureNotAvailableError{}), qt.Equals, true)
c.Assert(IsFeatureNotAvailableError(errors.New("asdf")), qt.Equals, false)
}

View file

@ -1,430 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable lfmtaw 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 herrors
import (
"encoding/json"
"errors"
"fmt"
"io"
"path/filepath"
"github.com/bep/godartsass/v2"
"github.com/bep/golibsass/libsass/libsasserrors"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/text"
"github.com/pelletier/go-toml/v2"
"github.com/spf13/afero"
"github.com/tdewolff/parse/v2"
)
// FileError represents an error when handling a file: Parsing a config file,
// execute a template etc.
type FileError interface {
error
// ErrorContext holds some context information about the error.
ErrorContext() *ErrorContext
text.Positioner
// UpdatePosition updates the position of the error.
UpdatePosition(pos text.Position) FileError
// UpdateContent updates the error with a new ErrorContext from the content of the file.
UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError
// SetFilename sets the filename of the error.
SetFilename(filename string) FileError
}
// Unwrapper can unwrap errors created with fmt.Errorf.
type Unwrapper interface {
Unwrap() error
}
var (
_ FileError = (*fileError)(nil)
_ Unwrapper = (*fileError)(nil)
)
func (fe *fileError) SetFilename(filename string) FileError {
fe.position.Filename = filename
return fe
}
func (fe *fileError) UpdatePosition(pos text.Position) FileError {
oldFilename := fe.Position().Filename
if pos.Filename != "" && fe.fileType == "" {
_, fe.fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename))
}
if pos.Filename == "" {
pos.Filename = oldFilename
}
fe.position = pos
return fe
}
func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError {
if linematcher == nil {
linematcher = SimpleLineMatcher
}
var (
posle = fe.position
ectx *ErrorContext
)
if posle.LineNumber <= 1 && posle.Offset > 0 {
// Try to locate the line number from the content if offset is set.
ectx = locateError(r, fe, func(m LineMatcher) int {
if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) {
lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber
m.Position = text.Position{LineNumber: lno}
return linematcher(m)
}
return -1
})
} else {
ectx = locateError(r, fe, linematcher)
}
if ectx.ChromaLexer == "" {
if fe.fileType != "" {
ectx.ChromaLexer = chromaLexerFromType(fe.fileType)
} else {
ectx.ChromaLexer = chromaLexerFromFilename(fe.Position().Filename)
}
}
fe.errorContext = ectx
if ectx.Position.LineNumber > 0 {
fe.position.LineNumber = ectx.Position.LineNumber
}
if ectx.Position.ColumnNumber > 0 {
fe.position.ColumnNumber = ectx.Position.ColumnNumber
}
return fe
}
type fileError struct {
position text.Position
errorContext *ErrorContext
fileType string
cause error
}
func (e *fileError) ErrorContext() *ErrorContext {
return e.errorContext
}
// Position returns the text position of this error.
func (e fileError) Position() text.Position {
return e.position
}
func (e *fileError) Error() string {
return fmt.Sprintf("%s: %s", e.position, e.causeString())
}
func (e *fileError) causeString() string {
if e.cause == nil {
return ""
}
switch v := e.cause.(type) {
// Avoid repeating the file info in the error message.
case godartsass.SassError:
return v.Message
case libsasserrors.Error:
return v.Message
default:
return v.Error()
}
}
func (e *fileError) Unwrap() error {
return e.cause
}
// NewFileError creates a new FileError that wraps err.
// It will try to extract the filename and line number from err.
func NewFileError(err error) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err)
return &fileError{cause: err, fileType: fileType, position: pos}
}
// NewFileErrorFromName creates a new FileError that wraps err.
// The value for name should identify the file, the best
// being the full filename to the file on disk.
func NewFileErrorFromName(err error, name string) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err)
pos.Filename = name
if fileType == "" {
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
}
return &fileError{cause: err, fileType: fileType, position: pos}
}
// NewFileErrorFromPos will use the filename and line number from pos to create a new FileError, wrapping err.
func NewFileErrorFromPos(err error, pos text.Position) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, _ := extractFileTypePos(err)
if fileType == "" {
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename))
}
return &fileError{cause: err, fileType: fileType, position: pos}
}
func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError {
fe := NewFileError(err)
pos := fe.Position()
if pos.Filename == "" {
return fe
}
f, realFilename, err2 := openFile(pos.Filename, fs)
if err2 != nil {
return fe
}
pos.Filename = realFilename
defer f.Close()
return fe.UpdateContent(f, linematcher)
}
func NewFileErrorFromFileInPos(err error, pos text.Position, fs afero.Fs, linematcher LineMatcherFn) FileError {
if err == nil {
panic("err is nil")
}
f, realFilename, err2 := openFile(pos.Filename, fs)
if err2 != nil {
return NewFileErrorFromPos(err, pos)
}
pos.Filename = realFilename
defer f.Close()
return NewFileErrorFromPos(err, pos).UpdateContent(f, linematcher)
}
// NewFileErrorFromFile is a convenience method to create a new FileError from a file.
func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher LineMatcherFn) FileError {
if err == nil {
panic("err is nil")
}
f, realFilename, err2 := openFile(filename, fs)
if err2 != nil {
return NewFileErrorFromName(err, realFilename)
}
defer f.Close()
return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
}
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
realFilename := filename
// We want the most specific filename possible in the error message.
fi, err2 := fs.Stat(filename)
if err2 == nil {
if s, ok := fi.(interface {
Filename() string
}); ok {
realFilename = s.Filename()
}
}
f, err2 := fs.Open(filename)
if err2 != nil {
return nil, realFilename, err2
}
return f, realFilename, nil
}
// Cause returns the underlying error, that is,
// it unwraps errors until it finds one that does not implement
// the Unwrap method.
// For a shallow variant, see Unwrap.
func Cause(err error) error {
type unwrapper interface {
Unwrap() error
}
for err != nil {
cause, ok := err.(unwrapper)
if !ok {
break
}
err = cause.Unwrap()
}
return err
}
// Unwrap returns the underlying error or itself if it does not implement Unwrap.
func Unwrap(err error) error {
if u := errors.Unwrap(err); u != nil {
return u
}
return err
}
func extractFileTypePos(err error) (string, text.Position) {
err = Unwrap(err)
var fileType string
// LibSass, DartSass
if pos := extractPosition(err); pos.LineNumber > 0 || pos.Offset > 0 {
_, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
return fileType, pos
}
// Default to line 1 col 1 if we don't find any better.
pos := text.Position{
Offset: -1,
LineNumber: 1,
ColumnNumber: 1,
}
// JSON errors.
offset, typ := extractOffsetAndType(err)
if fileType == "" {
fileType = typ
}
if offset >= 0 {
pos.Offset = offset
}
// The error type from the minifier contains line number and column number.
if line, col := extractLineNumberAndColumnNumber(err); line >= 0 {
pos.LineNumber = line
pos.ColumnNumber = col
return fileType, pos
}
// Look in the error message for the line number.
for _, handle := range lineNumberExtractors {
lno, col := handle(err)
if lno > 0 {
pos.ColumnNumber = col
pos.LineNumber = lno
break
}
}
if fileType == "" && pos.Filename != "" {
_, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
}
return fileType, pos
}
// UnwrapFileError tries to unwrap a FileError from err.
// It returns nil if this is not possible.
func UnwrapFileError(err error) FileError {
for err != nil {
switch v := err.(type) {
case FileError:
return v
default:
err = errors.Unwrap(err)
}
}
return nil
}
// UnwrapFileErrors tries to unwrap all FileError.
func UnwrapFileErrors(err error) []FileError {
var errs []FileError
for err != nil {
if v, ok := err.(FileError); ok {
errs = append(errs, v)
}
err = errors.Unwrap(err)
}
return errs
}
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
var errs []FileError
for err != nil {
if v, ok := err.(FileError); ok && v.ErrorContext() != nil {
errs = append(errs, v)
}
err = errors.Unwrap(err)
}
return errs
}
func extractOffsetAndType(e error) (int, string) {
switch v := e.(type) {
case *json.UnmarshalTypeError:
return int(v.Offset), "json"
case *json.SyntaxError:
return int(v.Offset), "json"
default:
return -1, ""
}
}
func extractLineNumberAndColumnNumber(e error) (int, int) {
switch v := e.(type) {
case *parse.Error:
return v.Line, v.Column
case *toml.DecodeError:
return v.Position()
}
return -1, -1
}
func extractPosition(e error) (pos text.Position) {
switch v := e.(type) {
case godartsass.SassError:
span := v.Span
start := span.Start
filename, _ := paths.UrlStringToFilename(span.Url)
pos.Filename = filename
pos.Offset = start.Offset
pos.ColumnNumber = start.Column
case libsasserrors.Error:
pos.Filename = v.File
pos.LineNumber = v.Line
pos.ColumnNumber = v.Column
}
return
}
// TextSegmentError is an error with a text segment attached.
type TextSegmentError struct {
Segment string
Err error
}
func (e TextSegmentError) Unwrap() error {
return e.Err
}
func (e TextSegmentError) Error() string {
return e.Err.Error()
}

View file

@ -1,80 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package herrors
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/gohugoio/hugo/common/text"
qt "github.com/frankban/quicktest"
)
func TestNewFileError(t *testing.T) {
t.Parallel()
c := qt.New(t)
fe := NewFileErrorFromName(errors.New("bar"), "foo.html")
c.Assert(fe.Error(), qt.Equals, `"foo.html:1:1": bar`)
lines := ""
for i := 1; i <= 100; i++ {
lines += fmt.Sprintf("line %d\n", i)
}
fe.UpdatePosition(text.Position{LineNumber: 32, ColumnNumber: 2})
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:2": bar`)
fe.UpdatePosition(text.Position{LineNumber: 0, ColumnNumber: 0, Offset: 212})
fe.UpdateContent(strings.NewReader(lines), nil)
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:0": bar`)
errorContext := fe.ErrorContext()
c.Assert(errorContext, qt.IsNotNil)
c.Assert(errorContext.Lines, qt.DeepEquals, []string{"line 30", "line 31", "line 32", "line 33", "line 34"})
c.Assert(errorContext.LinesPos, qt.Equals, 2)
c.Assert(errorContext.ChromaLexer, qt.Equals, "go-html-template")
}
func TestNewFileErrorExtractFromMessage(t *testing.T) {
t.Parallel()
c := qt.New(t)
for i, test := range []struct {
in error
offset int
lineNumber int
columnNumber int
}{
{errors.New("no line number for you"), 0, 1, 1},
{errors.New(`template: _default/single.html:4:15: executing "_default/single.html" at <.Titles>: can't evaluate field Titles in type *hugolib.PageOutput`), 0, 4, 15},
{errors.New("parse failed: template: _default/bundle-resource-meta.html:11: unexpected in operand"), 0, 11, 1},
{errors.New(`failed:: template: _default/bundle-resource-meta.html:2:7: executing "main" at <.Titles>`), 0, 2, 7},
{errors.New(`failed to load translations: (6, 7): was expecting token =, but got "g" instead`), 0, 6, 7},
{errors.New(`execute of template failed: template: index.html:2:5: executing "index.html" at <partial "foo.html" .>: error calling partial: "/layouts/partials/foo.html:3:6": execute of template failed: template: partials/foo.html:3:6: executing "partials/foo.html" at <.ThisDoesNotExist>: can't evaluate field ThisDoesNotExist in type *hugolib.pageStat`), 0, 2, 5},
} {
got := NewFileErrorFromName(test.in, "test.txt")
errMsg := qt.Commentf("[%d][%T]", i, got)
pos := got.Position()
c.Assert(pos.LineNumber, qt.Equals, test.lineNumber, errMsg)
c.Assert(pos.ColumnNumber, qt.Equals, test.columnNumber, errMsg)
c.Assert(errors.Unwrap(got), qt.Not(qt.IsNil))
}
}

View file

@ -1,63 +0,0 @@
// 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 herrors
import (
"regexp"
"strconv"
)
var lineNumberExtractors = []lineNumberExtractor{
// Template/shortcode parse errors
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
// YAML parse errors
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
// i18n bundle errors
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
}
type lineNumberExtractor func(e error) (int, int)
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
re := regexp.MustCompile(expression)
return extractLineNo(re)
}
func extractLineNo(re *regexp.Regexp) lineNumberExtractor {
return func(e error) (int, int) {
if e == nil {
panic("no error")
}
col := 1
s := e.Error()
m := re.FindStringSubmatch(s)
if len(m) >= 2 {
lno, _ := strconv.Atoi(m[1])
if len(m) > 2 {
col, _ = strconv.Atoi(m[2])
}
if col <= 0 {
col = 1
}
return lno, col
}
return 0, col
}
}

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