mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 15:58:53 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fbcc4b7b3 |
+32
-98
@@ -1,117 +1,51 @@
|
||||
parameters:
|
||||
|
||||
# v2: 11m.
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22100.20100
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
- image: bepsays/ci-goreleaser:1.1800.300
|
||||
environment:
|
||||
CGO_ENABLED: "0"
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
prepare_release:
|
||||
<<: *defaults
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- &remote-docker
|
||||
setup_remote_docker:
|
||||
version: 20.10.14
|
||||
- checkout:
|
||||
- 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]
|
||||
command: |
|
||||
git clone git@github.com:gohugoio/hugoDocs.git
|
||||
cd hugo
|
||||
go mod download
|
||||
sleep 5
|
||||
go mod verify
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: .
|
||||
release:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- &restore-cache
|
||||
restore_cache:
|
||||
key: git-sha-{{ .Revision }}
|
||||
- attach_workspace:
|
||||
at: /root/project
|
||||
- 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.22100.20100
|
||||
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
|
||||
command: |
|
||||
cd hugo
|
||||
git config --global user.email "bjorn.erik.pedersen+hugoreleaser@gmail.com"
|
||||
git config --global user.name "hugoreleaser"
|
||||
go run -tags release main.go release -r ${CIRCLE_BRANCH}
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
release:
|
||||
jobs:
|
||||
- prepare_release:
|
||||
- build:
|
||||
filters:
|
||||
branches:
|
||||
only: /release-.*/
|
||||
- build_container1:
|
||||
- hold:
|
||||
type: approval
|
||||
requires:
|
||||
- prepare_release
|
||||
- build_container2:
|
||||
requires:
|
||||
- prepare_release
|
||||
- archive_and_release:
|
||||
- build
|
||||
- release:
|
||||
context: org-global
|
||||
requires:
|
||||
- build_container1
|
||||
- build_container2
|
||||
|
||||
|
||||
|
||||
- hold
|
||||
|
||||
@@ -5,12 +5,7 @@ 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`)?
|
||||
|
||||
@@ -3,13 +3,8 @@ 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@08e671be8ac8944d0e132aa71d0ae8ccfb347675
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
name: TestDartSassV1
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
DART_SASS_VERSION: 1.62.1
|
||||
DART_SASS_SHA_LINUX: 3574da75a7322a539034648b8ff84ff2cca162eb924d72b663d718cd3936f075
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753
|
||||
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@ee2113536afb7f793eed4ce60e8d3b26db912da4
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
bundler-cache: true #
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df
|
||||
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@7570212ae20b63653481675fb1ff62d1073632b0
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
rst2html.py --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 -PackageName pandoc
|
||||
- run: pandoc -v
|
||||
- name: Install dart-sass-embedded Linux
|
||||
run: |
|
||||
echo "Install Dart Sass version ${DART_SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${DART_SASS_VERSION}/sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_LINUX} sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf "sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- name: Check
|
||||
run: |
|
||||
dart-sass-embedded --version
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
+39
-62
@@ -1,49 +1,40 @@
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
on: [push, pull_request]
|
||||
name: Test
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
SASS_VERSION: 1.63.2
|
||||
DART_SASS_SHA_LINUX: 3ea33c95ad5c35fda6e9a0956199eef38a398f496cfb8750e02479d7d1dd42af
|
||||
DART_SASS_SHA_MACOS: 11c70f259836b250b44a9cb57fed70e030f21f45069b467d371685855f1eb4f0
|
||||
DART_SASS_SHA_WINDOWS: cd8cd36a619dd8e27f93d3186c52d70eb7d69472aa6c85f5094b29693e773f64
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.20.x,1.21.x]
|
||||
# Note: We upgraded to Go 1.18 in Hugo v0.95.0
|
||||
# Go 1.18 had some breaking changes on the source level which means Hugo cannot be built
|
||||
# with older Go versions, but the improvements in Go 1.18 were too good to pass on (e.g. break and continue).
|
||||
# Note that you don't need Go (or Go 1.18) to run a pre-built binary.
|
||||
go-version: [1.18.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753
|
||||
uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8
|
||||
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@ee2113536afb7f793eed4ce60e8d3b26db912da4
|
||||
uses: actions/setup-ruby@5f29a1cd8dfebf420691c4c9a0e832e2fae5a526
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
bundler-cache: true #
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install Mage
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
run: go install github.com/magefile/mage@07afc7d24f4d6d6442305d49552f04fbda5ccb3e
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@7570212ae20b63653481675fb1ff62d1073632b0
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
@@ -58,56 +49,42 @@ jobs:
|
||||
brew install pandoc
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
Choco-Install -PackageName pandoc
|
||||
choco install pandoc
|
||||
choco install mingw --version 10.2.0 --allow-downgrade
|
||||
- run: pandoc -v
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
Choco-Install -PackageName mingw -ArgumentList "--version","12.2.0","--allow-downgrade"
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install dart-sass Linux
|
||||
name: Install dart-sass-embedded 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
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.6/sass_embedded-1.0.0-beta.6-linux-x64.tar.gz;
|
||||
echo "04fc1e5e28d29a4585a701941b6dace56771d94bfbe7f9e4db28d24417ceeec3 sass_embedded-1.0.0-beta.6-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf sass_embedded-1.0.0-beta.6-linux-x64.tar.gz;
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'macos-latest'
|
||||
name: Install dart-sass MacOS
|
||||
name: Install dart-sass-embedded 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
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.6/sass_embedded-1.0.0-beta.6-macos-x64.tar.gz;
|
||||
echo "b3b984675a9b04aa22f6f2302dda4191b507ac2ca124467db2dfe7e58e72fbad sass_embedded-1.0.0-beta.6-macos-x64.tar.gz" | shasum -a 256 -c;
|
||||
tar -xvf sass_embedded-1.0.0-beta.6-macos-x64.tar.gz;
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'windows-latest'
|
||||
name: Install dart-sass Windows
|
||||
name: Install dart-sass-embedded 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 != 'windows-latest'
|
||||
name: Check
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.6/sass_embedded-1.0.0-beta.6-windows-x64.zip;
|
||||
echo "6ae442129dbb3334bc21ef851261da6c0c1b560da790ca2e1350871d00ab816d sass_embedded-1.0.0-beta.6-windows-x64.zip" | sha256sum -c;
|
||||
unzip sass_embedded-1.0.0-beta.6-windows-x64.zip;
|
||||
echo "$env:GITHUB_WORKSPACE/sass_embedded/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append
|
||||
- name: Check
|
||||
run: |
|
||||
sass --version;
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
- 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;
|
||||
- name: Build Docs
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
- name: Build tags
|
||||
HUGO_TIMEOUT: 31000
|
||||
HUGO_IGNOREERRORS: error-remote-getjson
|
||||
HUGO_SERVICES_INSTAGRAM_ACCESSTOKEN: dummytoken
|
||||
run: |
|
||||
go install -tags extended,nodeploy
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Build for dragonfly
|
||||
run: |
|
||||
go install
|
||||
env:
|
||||
GOARCH: amd64
|
||||
GOOS: dragonfly
|
||||
mage -v hugo
|
||||
./hugo -s docs/
|
||||
./hugo --renderToMemory -s docs/
|
||||
|
||||
+28
-2
@@ -1,3 +1,29 @@
|
||||
|
||||
/hugo
|
||||
docs/public*
|
||||
/.idea
|
||||
.vscode/*
|
||||
hugo.exe
|
||||
*.test
|
||||
imports.*
|
||||
*.prof
|
||||
nohup.out
|
||||
cover.out
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
*~
|
||||
vendor/*/
|
||||
*.bench
|
||||
*.debug
|
||||
coverage*.out
|
||||
|
||||
dock.sh
|
||||
|
||||
GoBuilds
|
||||
dist
|
||||
|
||||
hugolib/hugo_stats.json
|
||||
resources/sunset.jpg
|
||||
|
||||
vendor
|
||||
|
||||
.hugo_build.lock
|
||||
|
||||
+6
-2
@@ -1,7 +1,7 @@
|
||||
>**Note:** We would apprecitate if you hold on with any big refactorings (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future.
|
||||
|
||||
# Contributing to Hugo
|
||||
|
||||
**Note March 16th 2022:** We are currently very constrained on human resources to do code reviews, so we currently require any new Pull Requests to be limited to bug fixes closing an existing issue. Also, we have updated to Go 1.18, but we will currently not accept any generic rewrites, "interface{} to any" replacements and similar.
|
||||
|
||||
We welcome contributions to Hugo of any kind including documentation, themes,
|
||||
organization, tutorials, blog posts, bug reports, issues, feature requests,
|
||||
feature implementations, pull requests, answering questions on the forum,
|
||||
@@ -52,6 +52,8 @@ Hugo has become a fully featured static site generator, so any new functionality
|
||||
|
||||
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.).
|
||||
|
||||
It is recommended to open up a discussion on the [Hugo Forum](https://discourse.gohugo.io/) to get feedback on your idea before you begin.
|
||||
|
||||
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).
|
||||
@@ -122,6 +124,8 @@ cd hugo
|
||||
go install
|
||||
```
|
||||
|
||||
>Note: Some Go tools may not be fully updated to support Go Modules yet. One example would be LiteIDE. Follow [this workaround](https://github.com/visualfc/liteide/issues/986#issuecomment-428117702) for how to continue to work with Hugo below `GOPATH`.
|
||||
|
||||
For some convenient build and test targets, you also will want to install Mage:
|
||||
|
||||
```bash
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
FROM golang:1.21-alpine AS build
|
||||
FROM golang:1.18-alpine AS build
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=extended .
|
||||
@@ -26,7 +26,7 @@ RUN mage hugo && mage install
|
||||
|
||||
# ---
|
||||
|
||||
FROM alpine:3.18
|
||||
FROM alpine:3.12
|
||||
|
||||
COPY --from=build /go/bin/hugo /usr/bin/hugo
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022 The Hugo Authors.
|
||||
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.
|
||||
|
||||
@@ -1,290 +1,248 @@
|
||||
[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
|
||||
[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/
|
||||
[twitter]: https://twitter.com/gohugoio
|
||||
[website]: https://gohugo.io
|
||||
[windows]: https://gohugo.io/installation/windows
|
||||
|
||||
<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 [bep], [spf13], and [friends] in [Go].
|
||||
A Fast and Flexible Static Site Generator built with love by [bep](https://github.com/bep), [spf13](https://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go][].
|
||||
|
||||
---
|
||||
[Website](https://gohugo.io) |
|
||||
[Forum](https://discourse.gohugo.io) |
|
||||
[Documentation](https://gohugo.io/getting-started/) |
|
||||
[Installation Guide](https://gohugo.io/getting-started/installing/) |
|
||||
[Contribution Guide](CONTRIBUTING.md) |
|
||||
[Twitter](https://twitter.com/gohugoio)
|
||||
|
||||
[](https://godoc.org/github.com/gohugoio/hugo)
|
||||
[](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
|
||||
|
||||
## 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, ease of 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 metadata, and you can run Hugo from any directory.
|
||||
This works well for shared hosts and other systems where you don’t 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
|
||||
|
||||
- CSS bundling – transpilation (Sass), tree shaking, minification, source maps, SRI hashing, and PostCSS integration
|
||||
- JavaScript bundling – transpilation (TypeScript, JSX), tree shaking, minification, source maps, and SRI hashing
|
||||
- Image processing – convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
|
||||
Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD, DragonFly BSD, OpenBSD, 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 Plan 9 and Solaris.
|
||||
|
||||
## Sponsors
|
||||
**Complete documentation is available at [Hugo Documentation](https://gohugo.io/getting-started/).**
|
||||
|
||||
<p> </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/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
|
||||
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/cloudcannon-blue.svg" width="220" alt="CloudCannon"></a>
|
||||
<p> </p>
|
||||
## Choose How to Install
|
||||
|
||||
## Installation
|
||||
If you want to use Hugo as your site generator, simply install the Hugo binaries.
|
||||
The Hugo binaries have no external dependencies.
|
||||
|
||||
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
|
||||
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.
|
||||
|
||||
- [macOS]
|
||||
- [Linux]
|
||||
- [Windows]
|
||||
- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD]
|
||||
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.
|
||||
|
||||
## Build from source
|
||||
### Install Hugo as Your Site Generator (Binary Install)
|
||||
|
||||
Hugo is available in two editions: standard and extended. With the extended edition you can:
|
||||
Use the [installation instructions in the Hugo documentation](https://gohugo.io/getting-started/installing/).
|
||||
|
||||
- Encode to the WebP format when processing images. You can decode WebP images with either edition.
|
||||
- Transpile Sass to CSS using the embedded LibSass transpiler. The extended edition is not required to use the Dart Sass transpiler.
|
||||
### Build and Install the Binaries from Source (Advanced Install)
|
||||
|
||||
Prerequisites to build Hugo from source:
|
||||
#### Prerequisite Tools
|
||||
|
||||
- Standard edition: Go 1.19 or later
|
||||
- Extended edition: Go 1.19 or later, and GCC
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Go (we test it with the last 2 major versions; but note that Hugo 0.95.0 only builds with >= Go 1.18.)](https://golang.org/dl/)
|
||||
|
||||
Build the standard edition:
|
||||
#### Fetch from GitHub
|
||||
|
||||
```text
|
||||
go install github.com/gohugoio/hugo@latest
|
||||
To fetch and build the source from GitHub:
|
||||
|
||||
```bash
|
||||
mkdir $HOME/src
|
||||
cd $HOME/src
|
||||
git clone https://github.com/gohugoio/hugo.git
|
||||
cd hugo
|
||||
go install
|
||||
```
|
||||
|
||||
Build the extended edition:
|
||||
**If you are a Windows user, substitute the `$HOME` environment variable above with `%USERPROFILE%`.**
|
||||
|
||||
```text
|
||||
CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest
|
||||
If you want to compile with Sass/SCSS support use `--tags extended` and make sure `CGO_ENABLED=1` is set in your go environment. If you don't want to have CGO enabled, you may use the following command to temporarily enable CGO only for hugo compilation:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=1 go install --tags extended
|
||||
```
|
||||
|
||||
## Documentation
|
||||
## The Hugo Documentation
|
||||
|
||||
Hugo's [documentation] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
|
||||
The Hugo documentation now lives in its own repository, see https://github.com/gohugoio/hugoDocs. But we do keep a version of that documentation as a `git subtree` in this repository. To build the sub folder `/docs` as a Hugo site, you need to clone this repo:
|
||||
|
||||
Please submit documentation issues and pull requests to the [documentation repository].
|
||||
```bash
|
||||
git clone git@github.com:gohugoio/hugo.git
|
||||
```
|
||||
## Contributing to Hugo
|
||||
|
||||
## 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].
|
||||
|
||||
Hugo’s [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.
|
||||
**Note March 16th 2022:** We are currently very constrained on human resources to do code reviews, so we currently require any new Pull Requests to be limited to bug fixes closing an existing issue. Also, we have updated to Go 1.18, but we will currently not accept any generic rewrites, "interface{} to any" replacements and similar.
|
||||
|
||||
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
We welcome contributions to Hugo of any kind including documentation, themes,
|
||||
organization, tutorials, blog posts, bug reports, issues, feature requests,
|
||||
feature implementations, pull requests, answering questions on the forum,
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
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).
|
||||
|
||||
[Go]: https://golang.org/
|
||||
[Hugo Documentation]: https://gohugo.io/overview/introduction/
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
|
||||
Hugo stands on the shoulder of many great open source libraries.
|
||||
|
||||
<details>
|
||||
<summary>See current dependencies</summary>
|
||||
If you run `hugo env -v` you will get a complete and up to date list.
|
||||
|
||||
```text
|
||||
cloud.google.com/go/compute/metadata="v0.2.3"
|
||||
cloud.google.com/go/iam="v1.1.0"
|
||||
cloud.google.com/go/storage="v1.30.1"
|
||||
cloud.google.com/go="v0.110.2"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore="v1.6.1"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity="v1.3.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal="v1.3.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob="v1.0.0"
|
||||
github.com/Azure/go-autorest/autorest/to="v0.4.0"
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go="v1.0.0"
|
||||
In Hugo 0.100.1 that list is, in lexical order:
|
||||
|
||||
```
|
||||
cloud.google.com/go/compute="v1.6.1"
|
||||
cloud.google.com/go/iam="v0.3.0"
|
||||
cloud.google.com/go/storage="v1.22.0"
|
||||
cloud.google.com/go="v0.101.0"
|
||||
github.com/Azure/azure-pipeline-go="v0.2.3"
|
||||
github.com/Azure/azure-storage-blob-go="v0.14.0"
|
||||
github.com/Azure/go-autorest/autorest/adal="v0.9.15"
|
||||
github.com/Azure/go-autorest/autorest/date="v0.3.0"
|
||||
github.com/Azure/go-autorest/autorest="v0.11.20"
|
||||
github.com/Azure/go-autorest/logger="v0.2.1"
|
||||
github.com/Azure/go-autorest/tracing="v0.6.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/PuerkitoBio/purell="v1.1.1"
|
||||
github.com/PuerkitoBio/urlesc="v0.0.0-20170810143723-de5bf2ad4578"
|
||||
github.com/alecthomas/chroma/v2="v2.7.0"
|
||||
github.com/alecthomas/chroma="v0.10.0"
|
||||
github.com/armon/go-radix="v1.0.0"
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream="v1.4.10"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.18.27"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.13.26"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.13.4"
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager="v1.11.70"
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources="v1.1.34"
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2="v2.4.28"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.3.35"
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a="v1.0.26"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding="v1.9.11"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum="v1.1.29"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.9.28"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared="v1.14.3"
|
||||
github.com/aws/aws-sdk-go-v2/service/s3="v1.35.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.12.12"
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc="v1.14.12"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.19.2"
|
||||
github.com/aws/aws-sdk-go-v2="v1.18.1"
|
||||
github.com/aws/aws-sdk-go="v1.44.284"
|
||||
github.com/aws/smithy-go="v1.13.5"
|
||||
github.com/bep/clocks="v0.5.0"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.7.0"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.4.0"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.5.0"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.2.2"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.3.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.4.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.7.0"
|
||||
github.com/aws/aws-sdk-go-v2="v1.9.0"
|
||||
github.com/aws/aws-sdk-go="v1.43.5"
|
||||
github.com/aws/smithy-go="v1.8.0"
|
||||
github.com/bep/clock="v0.3.0"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.1.2"
|
||||
github.com/bep/goat="v0.5.0"
|
||||
github.com/bep/godartsass/v2="v2.0.0"
|
||||
github.com/bep/godartsass="v1.2.0"
|
||||
github.com/bep/golibsass="v1.1.1"
|
||||
github.com/bep/gowebp="v0.2.0"
|
||||
github.com/bep/lazycache="v0.2.0"
|
||||
github.com/bep/logg="v0.2.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/godartsass="v0.14.0"
|
||||
github.com/bep/golibsass="v1.1.0"
|
||||
github.com/bep/gowebp="v0.1.0"
|
||||
github.com/bep/overlayfs="v0.6.0"
|
||||
github.com/bep/simplecobra="v0.3.2"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/clbanning/mxj/v2="v2.5.7"
|
||||
github.com/cli/safeexec="v1.0.1"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.2"
|
||||
github.com/clbanning/mxj/v2="v2.5.5"
|
||||
github.com/cli/safeexec="v1.0.0"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.1"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.10.0"
|
||||
github.com/dustin/go-humanize="v1.0.1"
|
||||
github.com/evanw/esbuild="v0.18.5"
|
||||
github.com/fatih/color="v1.15.0"
|
||||
github.com/frankban/quicktest="v1.14.5"
|
||||
github.com/fsnotify/fsnotify="v1.6.0"
|
||||
github.com/getkin/kin-openapi="v0.118.0"
|
||||
github.com/dlclark/regexp2="v1.4.0"
|
||||
github.com/dustin/go-humanize="v1.0.0"
|
||||
github.com/evanw/esbuild="v0.14.42"
|
||||
github.com/frankban/quicktest="v1.14.3"
|
||||
github.com/fsnotify/fsnotify="v1.5.4"
|
||||
github.com/getkin/kin-openapi="v0.94.0"
|
||||
github.com/ghodss/yaml="v1.0.0"
|
||||
github.com/go-openapi/jsonpointer="v0.19.6"
|
||||
github.com/go-openapi/swag="v0.22.3"
|
||||
github.com/gobuffalo/flect="v1.0.2"
|
||||
github.com/go-openapi/jsonpointer="v0.19.5"
|
||||
github.com/go-openapi/swag="v0.19.5"
|
||||
github.com/gobuffalo/flect="v0.2.5"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20210430103248-4c28c89f8013"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v1.0.1"
|
||||
github.com/golang-jwt/jwt/v4="v4.5.0"
|
||||
github.com/golang-jwt/jwt/v4="v4.0.0"
|
||||
github.com/golang/groupcache="v0.0.0-20210331224755-41bb18bfe9da"
|
||||
github.com/golang/protobuf="v1.5.3"
|
||||
github.com/google/go-cmp="v0.5.9"
|
||||
github.com/google/s2a-go="v0.1.4"
|
||||
github.com/golang/protobuf="v1.5.2"
|
||||
github.com/google/go-cmp="v0.5.8"
|
||||
github.com/google/uuid="v1.3.0"
|
||||
github.com/google/wire="v0.5.0"
|
||||
github.com/googleapis/enterprise-certificate-proxy="v0.2.5"
|
||||
github.com/googleapis/gax-go/v2="v2.11.0"
|
||||
github.com/googleapis/gax-go/v2="v2.3.0"
|
||||
github.com/googleapis/go-type-adapters="v1.0.0"
|
||||
github.com/gorilla/websocket="v1.5.0"
|
||||
github.com/hairyhenderson/go-codeowners="v0.3.0"
|
||||
github.com/hashicorp/golang-lru/v2="v2.0.1"
|
||||
github.com/invopop/yaml="v0.1.0"
|
||||
github.com/hairyhenderson/go-codeowners="v0.2.3-0.20201026200250-cdc7c0759690"
|
||||
github.com/inconshreveable/mousetrap="v1.0.0"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/jmespath/go-jmespath="v0.4.0"
|
||||
github.com/josharian/intern="v1.0.0"
|
||||
github.com/kr/pretty="v0.3.1"
|
||||
github.com/kr/pretty="v0.3.0"
|
||||
github.com/kr/text="v0.2.0"
|
||||
github.com/kylelemons/godebug="v1.1.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.12"
|
||||
github.com/mailru/easyjson="v0.7.7"
|
||||
github.com/marekm4/color-extractor="v1.2.0"
|
||||
github.com/mattn/go-colorable="v0.1.13"
|
||||
github.com/mattn/go-isatty="v0.0.19"
|
||||
github.com/kyokomi/emoji/v2="v2.2.9"
|
||||
github.com/mailru/easyjson="v0.0.0-20190626092158-b2ccc519800e"
|
||||
github.com/mattn/go-ieproxy="v0.0.1"
|
||||
github.com/mattn/go-isatty="v0.0.14"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/mitchellh/hashstructure="v1.1.0"
|
||||
github.com/mitchellh/mapstructure="v1.5.0"
|
||||
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/niklasfasching/go-org="v1.6.2"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/pelletier/go-toml/v2="v2.0.8"
|
||||
github.com/perimeterx/marshmallow="v1.1.4"
|
||||
github.com/pkg/browser="v0.0.0-20210911075715-681adbf594b8"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.10.1-0.20230508101108-a4f6fabd84c5"
|
||||
github.com/pelletier/go-toml/v2="v2.0.0-beta.7.0.20220408132554-2377ac4bc04c"
|
||||
github.com/rogpeppe/go-internal="v1.8.1"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
github.com/rwcarlsen/goexif="v0.0.0-20190401172101-9e8deecbddbd"
|
||||
github.com/sanity-io/litter="v1.5.5"
|
||||
github.com/sass/dart-sass/compiler="1.63.6"
|
||||
github.com/sass/dart-sass/implementation="1.63.6"
|
||||
github.com/sass/dart-sass/protocol="2.1.0"
|
||||
github.com/sass/libsass="3.6.5"
|
||||
github.com/spf13/afero="v1.9.5"
|
||||
github.com/spf13/cast="v1.5.1"
|
||||
github.com/spf13/cobra="v1.7.0"
|
||||
github.com/spf13/afero="v1.8.2"
|
||||
github.com/spf13/cast="v1.5.0"
|
||||
github.com/spf13/cobra="v1.4.0"
|
||||
github.com/spf13/fsync="v0.9.0"
|
||||
github.com/spf13/jwalterweatherman="v1.1.0"
|
||||
github.com/spf13/pflag="v1.0.5"
|
||||
github.com/tdewolff/minify/v2="v2.12.7"
|
||||
github.com/tdewolff/parse/v2="v2.6.6"
|
||||
github.com/webmproject/libwebp="v1.2.4"
|
||||
github.com/yuin/goldmark="v1.5.4"
|
||||
go.opencensus.io="v0.24.0"
|
||||
go.uber.org/atomic="v1.11.0"
|
||||
go.uber.org/automaxprocs="v1.5.2"
|
||||
gocloud.dev="v0.30.0"
|
||||
golang.org/x/crypto="v0.10.0"
|
||||
golang.org/x/exp="v0.0.0-20230321023759-10a507213a29"
|
||||
golang.org/x/image="v0.8.0"
|
||||
golang.org/x/mod="v0.10.0"
|
||||
golang.org/x/net="v0.11.0"
|
||||
golang.org/x/oauth2="v0.9.0"
|
||||
golang.org/x/sync="v0.3.0"
|
||||
golang.org/x/sys="v0.9.0"
|
||||
golang.org/x/text="v0.10.0"
|
||||
golang.org/x/tools="v0.9.3"
|
||||
golang.org/x/xerrors="v0.0.0-20220907171357-04be3eba64a2"
|
||||
google.golang.org/api="v0.128.0"
|
||||
google.golang.org/appengine="v1.6.7"
|
||||
google.golang.org/genproto/googleapis/api="v0.0.0-20230530153820-e85fd2cbaebc"
|
||||
google.golang.org/genproto/googleapis/rpc="v0.0.0-20230530153820-e85fd2cbaebc"
|
||||
google.golang.org/genproto="v0.0.0-20230530153820-e85fd2cbaebc"
|
||||
google.golang.org/grpc="v1.56.0"
|
||||
google.golang.org/protobuf="v1.30.0"
|
||||
github.com/tdewolff/minify/v2="v2.11.5"
|
||||
github.com/tdewolff/parse/v2="v2.5.31"
|
||||
github.com/webmproject/libwebp="v1.2.0"
|
||||
github.com/yuin/goldmark="v1.4.12"
|
||||
go.opencensus.io="v0.23.0"
|
||||
go.uber.org/atomic="v1.9.0"
|
||||
gocloud.dev="v0.24.0"
|
||||
golang.org/x/crypto="v0.0.0-20211108221036-ceb1ce70b4fa"
|
||||
golang.org/x/image="v0.0.0-20211028202545-6944b10bf410"
|
||||
golang.org/x/net="v0.0.0-20220425223048-2871e0cb64e4"
|
||||
golang.org/x/oauth2="v0.0.0-20220411215720-9780585627b5"
|
||||
golang.org/x/sync="v0.0.0-20210220032951-036812b2e83c"
|
||||
golang.org/x/sys="v0.0.0-20220422013727-9388b58f7150"
|
||||
golang.org/x/text="v0.3.7"
|
||||
golang.org/x/tools="v0.1.10"
|
||||
golang.org/x/xerrors="v0.0.0-20220411194840-2f41105eb62f"
|
||||
google.golang.org/api="v0.76.0"
|
||||
google.golang.org/genproto="v0.0.0-20220426171045-31bebdecfb46"
|
||||
google.golang.org/grpc="v1.46.0"
|
||||
google.golang.org/protobuf="v1.28.0"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
// Package cache contains the different cache implementations.
|
||||
package cache
|
||||
Vendored
+30
-44
@@ -17,6 +17,7 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -35,7 +36,7 @@ import (
|
||||
var ErrFatal = errors.New("fatal filecache error")
|
||||
|
||||
const (
|
||||
FilecacheRootDirname = "filecache"
|
||||
filecacheRootDirname = "filecache"
|
||||
)
|
||||
|
||||
// Cache caches a set of files in a directory. This is usually a file on
|
||||
@@ -51,9 +52,6 @@ type Cache struct {
|
||||
pruneAllRootDir string
|
||||
|
||||
nlocker *lockTracker
|
||||
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
}
|
||||
|
||||
type lockTracker struct {
|
||||
@@ -106,23 +104,9 @@ func (l *lockedFile) Close() error {
|
||||
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("", 0777); 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)
|
||||
|
||||
@@ -147,10 +131,6 @@ func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
|
||||
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)
|
||||
@@ -184,9 +164,6 @@ func (c *Cache) ReadOrCreate(id string,
|
||||
// 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)
|
||||
@@ -221,9 +198,6 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
|
||||
// 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)
|
||||
@@ -233,7 +207,7 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
defer r.Close()
|
||||
b, err := io.ReadAll(r)
|
||||
b, err := ioutil.ReadAll(r)
|
||||
return info, b, err
|
||||
}
|
||||
|
||||
@@ -259,9 +233,6 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
|
||||
// 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)
|
||||
@@ -271,18 +242,15 @@ func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
defer r.Close()
|
||||
b, err := io.ReadAll(r)
|
||||
b, err := ioutil.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.
|
||||
// Get gets the file with the given id from the cahce, 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)
|
||||
@@ -334,7 +302,7 @@ func (c *Cache) isExpired(modTime time.Time) bool {
|
||||
}
|
||||
|
||||
// For testing
|
||||
func (c *Cache) GetString(id string) string {
|
||||
func (c *Cache) getString(id string) string {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -346,7 +314,7 @@ func (c *Cache) GetString(id string) string {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b, _ := io.ReadAll(f)
|
||||
b, _ := ioutil.ReadAll(f)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
@@ -361,29 +329,47 @@ func (f Caches) Get(name string) *Cache {
|
||||
// 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)
|
||||
var dcfg Configs
|
||||
if c, ok := p.Cfg.Get("filecacheConfigs").(Configs); ok {
|
||||
dcfg = c
|
||||
} else {
|
||||
var err error
|
||||
dcfg, err = DecodeConfig(p.Fs.Source, p.Cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fs := p.Fs.Source
|
||||
|
||||
m := make(Caches)
|
||||
for k, v := range dcfg {
|
||||
var cfs afero.Fs
|
||||
|
||||
if v.IsResourceDir {
|
||||
if v.isResourceDir {
|
||||
cfs = p.BaseFs.ResourcesCache
|
||||
} else {
|
||||
cfs = fs
|
||||
}
|
||||
|
||||
if cfs == nil {
|
||||
panic("nil fs")
|
||||
// TODO(bep) we still have some places that do not initialize the
|
||||
// full dependencies of a site, e.g. the import Jekyll command.
|
||||
// That command does not need these caches, so let us just continue
|
||||
// for now.
|
||||
continue
|
||||
}
|
||||
|
||||
baseDir := v.DirCompiled
|
||||
baseDir := v.Dir
|
||||
|
||||
if err := cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bfs := afero.NewBasePathFs(cfs, baseDir)
|
||||
|
||||
var pruneAllRootDir string
|
||||
if k == CacheKeyModules {
|
||||
if k == cacheKeyModules {
|
||||
pruneAllRootDir = "pkg"
|
||||
}
|
||||
|
||||
|
||||
Vendored
+56
-47
@@ -11,7 +11,6 @@
|
||||
// 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 (
|
||||
@@ -22,8 +21,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@@ -31,102 +33,98 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
cachesConfigKey = "caches"
|
||||
|
||||
resourcesGenDir = ":resourceDir/_gen"
|
||||
cacheDirProject = ":cacheDir/:project"
|
||||
)
|
||||
|
||||
var defaultCacheConfig = FileCacheConfig{
|
||||
var defaultCacheConfig = Config{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: cacheDirProject,
|
||||
}
|
||||
|
||||
const (
|
||||
CacheKeyGetJSON = "getjson"
|
||||
CacheKeyGetCSV = "getcsv"
|
||||
CacheKeyImages = "images"
|
||||
CacheKeyAssets = "assets"
|
||||
CacheKeyModules = "modules"
|
||||
CacheKeyGetResource = "getresource"
|
||||
cacheKeyGetJSON = "getjson"
|
||||
cacheKeyGetCSV = "getcsv"
|
||||
cacheKeyImages = "images"
|
||||
cacheKeyAssets = "assets"
|
||||
cacheKeyModules = "modules"
|
||||
cacheKeyGetResource = "getresource"
|
||||
)
|
||||
|
||||
type Configs map[string]FileCacheConfig
|
||||
type Configs map[string]Config
|
||||
|
||||
// For internal use.
|
||||
func (c Configs) CacheDirModules() string {
|
||||
return c[CacheKeyModules].DirCompiled
|
||||
return c[cacheKeyModules].Dir
|
||||
}
|
||||
|
||||
var defaultCacheConfigs = Configs{
|
||||
CacheKeyModules: {
|
||||
cacheKeyModules: {
|
||||
MaxAge: -1,
|
||||
Dir: ":cacheDir/modules",
|
||||
},
|
||||
CacheKeyGetJSON: defaultCacheConfig,
|
||||
CacheKeyGetCSV: defaultCacheConfig,
|
||||
CacheKeyImages: {
|
||||
cacheKeyGetJSON: defaultCacheConfig,
|
||||
cacheKeyGetCSV: defaultCacheConfig,
|
||||
cacheKeyImages: {
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
},
|
||||
CacheKeyAssets: {
|
||||
cacheKeyAssets: {
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
},
|
||||
CacheKeyGetResource: FileCacheConfig{
|
||||
cacheKeyGetResource: Config{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: cacheDirProject,
|
||||
},
|
||||
}
|
||||
|
||||
type FileCacheConfig struct {
|
||||
type Config 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".
|
||||
// a negative value means forever, 0 means cache is disabled.
|
||||
MaxAge time.Duration
|
||||
|
||||
// The directory where files are stored.
|
||||
Dir string
|
||||
DirCompiled string `json:"-"`
|
||||
Dir string
|
||||
|
||||
// Will resources/_gen will get its own composite filesystem that
|
||||
// also checks any theme.
|
||||
IsResourceDir bool `json:"-"`
|
||||
isResourceDir bool
|
||||
}
|
||||
|
||||
// GetJSONCache gets the file cache for getJSON.
|
||||
func (f Caches) GetJSONCache() *Cache {
|
||||
return f[CacheKeyGetJSON]
|
||||
return f[cacheKeyGetJSON]
|
||||
}
|
||||
|
||||
// GetCSVCache gets the file cache for getCSV.
|
||||
func (f Caches) GetCSVCache() *Cache {
|
||||
return f[CacheKeyGetCSV]
|
||||
return f[cacheKeyGetCSV]
|
||||
}
|
||||
|
||||
// ImageCache gets the file cache for processed images.
|
||||
func (f Caches) ImageCache() *Cache {
|
||||
return f[CacheKeyImages]
|
||||
return f[cacheKeyImages]
|
||||
}
|
||||
|
||||
// ModulesCache gets the file cache for Hugo Modules.
|
||||
func (f Caches) ModulesCache() *Cache {
|
||||
return f[CacheKeyModules]
|
||||
return f[cacheKeyModules]
|
||||
}
|
||||
|
||||
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
|
||||
func (f Caches) AssetsCache() *Cache {
|
||||
return f[CacheKeyAssets]
|
||||
return f[cacheKeyAssets]
|
||||
}
|
||||
|
||||
// GetResourceCache gets the file cache for remote resources.
|
||||
func (f Caches) GetResourceCache() *Cache {
|
||||
return f[CacheKeyGetResource]
|
||||
return f[cacheKeyGetResource]
|
||||
}
|
||||
|
||||
func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Configs, error) {
|
||||
func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
|
||||
c := make(Configs)
|
||||
valid := make(map[string]bool)
|
||||
// Add defaults
|
||||
@@ -135,6 +133,8 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
valid[k] = true
|
||||
}
|
||||
|
||||
m := cfg.GetStringMap(cachesConfigKey)
|
||||
|
||||
_, isOsFs := fs.(*afero.OsFs)
|
||||
|
||||
for k, v := range m {
|
||||
@@ -170,6 +170,9 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
c[name] = cc
|
||||
}
|
||||
|
||||
// This is a very old flag in Hugo, but we need to respect it.
|
||||
disabled := cfg.GetBool("ignoreCache")
|
||||
|
||||
for k, v := range c {
|
||||
dir := filepath.ToSlash(filepath.Clean(v.Dir))
|
||||
hadSlash := strings.HasPrefix(dir, "/")
|
||||
@@ -177,12 +180,12 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
resolved, isResource, err := resolveDirPlaceholder(fs, bcfg, part)
|
||||
resolved, isResource, err := resolveDirPlaceholder(fs, cfg, part)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
if isResource {
|
||||
v.IsResourceDir = true
|
||||
v.isResourceDir = true
|
||||
}
|
||||
parts[i] = resolved
|
||||
}
|
||||
@@ -192,29 +195,33 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
if hadSlash {
|
||||
dir = "/" + dir
|
||||
}
|
||||
v.DirCompiled = filepath.Clean(filepath.FromSlash(dir))
|
||||
v.Dir = 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)
|
||||
if !v.isResourceDir {
|
||||
if isOsFs && !filepath.IsAbs(v.Dir) {
|
||||
return c, fmt.Errorf("%q must resolve to an absolute directory", v.Dir)
|
||||
}
|
||||
|
||||
// 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 len(strings.TrimPrefix(v.Dir, filepath.VolumeName(v.Dir))) == 1 {
|
||||
return c, fmt.Errorf("%q is a root folder and not allowed as cache dir", v.Dir)
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(v.DirCompiled, "_gen") {
|
||||
if !strings.HasPrefix(v.Dir, "_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)
|
||||
v.Dir = filepath.Join(v.Dir, filecacheRootDirname, k)
|
||||
} else {
|
||||
v.DirCompiled = filepath.Join(v.DirCompiled, k)
|
||||
v.Dir = filepath.Join(v.Dir, k)
|
||||
}
|
||||
|
||||
if disabled {
|
||||
v.MaxAge = 0
|
||||
}
|
||||
|
||||
c[k] = v
|
||||
@@ -224,15 +231,17 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
}
|
||||
|
||||
// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ...
|
||||
func resolveDirPlaceholder(fs afero.Fs, bcfg config.BaseConfig, placeholder string) (cacheDir string, isResource bool, err error) {
|
||||
func resolveDirPlaceholder(fs afero.Fs, cfg config.Provider, placeholder string) (cacheDir string, isResource bool, err error) {
|
||||
workingDir := cfg.GetString("workingDir")
|
||||
|
||||
switch strings.ToLower(placeholder) {
|
||||
case ":resourcedir":
|
||||
return "", true, nil
|
||||
case ":cachedir":
|
||||
return bcfg.CacheDir, false, nil
|
||||
d, err := helpers.GetCacheDir(fs, cfg)
|
||||
return d, false, err
|
||||
case ":project":
|
||||
return filepath.Base(bcfg.WorkingDir), false, nil
|
||||
return filepath.Base(workingDir), false, nil
|
||||
}
|
||||
|
||||
return "", false, fmt.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder)
|
||||
|
||||
+70
-18
@@ -11,19 +11,18 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package filecache_test
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"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"
|
||||
)
|
||||
@@ -58,20 +57,22 @@ 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
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(len(decoded), qt.Equals, 6)
|
||||
|
||||
c2 := decoded["getcsv"]
|
||||
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
|
||||
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv"))
|
||||
c.Assert(c2.Dir, 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"))
|
||||
c.Assert(c3.Dir, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images"))
|
||||
|
||||
c4 := decoded["getresource"]
|
||||
c.Assert(c4.MaxAge, qt.Equals, time.Duration(-1))
|
||||
c.Assert(c4.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource"))
|
||||
c.Assert(c4.Dir, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource"))
|
||||
}
|
||||
|
||||
func TestDecodeConfigIgnoreCache(t *testing.T) {
|
||||
@@ -105,7 +106,9 @@ 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
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(len(decoded), qt.Equals, 6)
|
||||
|
||||
for _, v := range decoded {
|
||||
@@ -115,7 +118,7 @@ dir = "/path/to/c4"
|
||||
|
||||
func TestDecodeConfigDefault(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
cfg := config.New()
|
||||
cfg := newTestConfig()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
cfg.Set("resourceDir", "c:\\cache\\resources")
|
||||
@@ -125,22 +128,71 @@ func TestDecodeConfigDefault(t *testing.T) {
|
||||
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
|
||||
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(len(decoded), qt.Equals, 6)
|
||||
|
||||
imgConfig := decoded[filecache.CacheKeyImages]
|
||||
jsonConfig := decoded[filecache.CacheKeyGetJSON]
|
||||
imgConfig := decoded[cacheKeyImages]
|
||||
jsonConfig := decoded[cacheKeyGetJSON]
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
c.Assert(imgConfig.DirCompiled, qt.Equals, filepath.FromSlash("_gen/images"))
|
||||
c.Assert(imgConfig.Dir, 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.Dir, qt.Equals, "_gen/images")
|
||||
c.Assert(jsonConfig.Dir, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson")
|
||||
}
|
||||
|
||||
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
|
||||
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
|
||||
c.Assert(imgConfig.isResourceDir, qt.Equals, true)
|
||||
c.Assert(jsonConfig.isResourceDir, qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestDecodeConfigInvalidDir(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 = "/"
|
||||
|
||||
`
|
||||
if runtime.GOOS == "windows" {
|
||||
configStr = strings.Replace(configStr, "/", "c:\\\\", 1)
|
||||
}
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
c.Assert(err, qt.IsNil)
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
_, err = DecodeConfig(fs, cfg)
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
}
|
||||
|
||||
func newTestConfig() config.Provider {
|
||||
cfg := config.NewWithTestDefaults()
|
||||
cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
|
||||
cfg.Set("contentDir", "content")
|
||||
cfg.Set("dataDir", "data")
|
||||
cfg.Set("resourceDir", "resources")
|
||||
cfg.Set("i18nDir", "i18n")
|
||||
cfg.Set("layoutDir", "layouts")
|
||||
cfg.Set("archetypeDir", "archetypes")
|
||||
cfg.Set("assetDir", "assets")
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
Vendored
+7
-20
@@ -18,7 +18,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -31,12 +30,13 @@ import (
|
||||
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) {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return counter, fmt.Errorf("failed to prune cache %q: %w", k, err)
|
||||
@@ -53,14 +53,10 @@ 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
|
||||
}
|
||||
@@ -69,24 +65,18 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
|
||||
if info.IsDir() {
|
||||
f, err := c.Fs.Open(name)
|
||||
|
||||
if err != nil {
|
||||
// This cache dir may not exist.
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
_, 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)
|
||||
}
|
||||
err = c.Fs.Remove(name)
|
||||
}
|
||||
|
||||
if err != nil && !herrors.IsNotExist(err) {
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -107,7 +97,7 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
counter++
|
||||
}
|
||||
|
||||
if err != nil && !herrors.IsNotExist(err) {
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -120,12 +110,9 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
}
|
||||
|
||||
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) {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
|
||||
+6
-7
@@ -11,14 +11,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package filecache_test
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
@@ -53,10 +52,10 @@ maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
`
|
||||
|
||||
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
|
||||
for _, name := range []string{cacheKeyGetCSV, cacheKeyGetJSON, cacheKeyAssets, cacheKeyImages} {
|
||||
msg := qt.Commentf("cache: %s", name)
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
caches, err := filecache.NewCaches(p)
|
||||
caches, err := NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
cache := caches[name]
|
||||
for i := 0; i < 10; i++ {
|
||||
@@ -76,7 +75,7 @@ dir = ":resourceDir/_gen"
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
v := cache.getString(id)
|
||||
if i < 5 {
|
||||
c.Assert(v, qt.Equals, "")
|
||||
} else {
|
||||
@@ -84,7 +83,7 @@ dir = ":resourceDir/_gen"
|
||||
}
|
||||
}
|
||||
|
||||
caches, err = filecache.NewCaches(p)
|
||||
caches, err = NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
cache = caches[name]
|
||||
// Touch one and then prune.
|
||||
@@ -99,7 +98,7 @@ dir = ":resourceDir/_gen"
|
||||
// Now only the i5 should be left.
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
v := cache.getString(id)
|
||||
if i != 5 {
|
||||
c.Assert(v, qt.Equals, "")
|
||||
} else {
|
||||
|
||||
Vendored
+87
-24
@@ -11,22 +11,27 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package filecache_test
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gobwas/glob"
|
||||
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
|
||||
"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"
|
||||
@@ -39,8 +44,13 @@ func TestFileCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
tempWorkingDir := t.TempDir()
|
||||
tempCacheDir := t.TempDir()
|
||||
tempWorkingDir, err := ioutil.TempDir("", "hugo_filecache_test_work")
|
||||
c.Assert(err, qt.IsNil)
|
||||
defer os.Remove(tempWorkingDir)
|
||||
|
||||
tempCacheDir, err := ioutil.TempDir("", "hugo_filecache_test_cache")
|
||||
c.Assert(err, qt.IsNil)
|
||||
defer os.Remove(tempCacheDir)
|
||||
|
||||
osfs := afero.NewOsFs()
|
||||
|
||||
@@ -80,19 +90,27 @@ dir = ":cacheDir/c"
|
||||
|
||||
p := newPathsSpec(t, osfs, configStr)
|
||||
|
||||
caches, err := filecache.NewCaches(p)
|
||||
caches, err := NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
cache := caches.Get("GetJSON")
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
c.Assert(cache.maxAge.String(), qt.Equals, "10h0m0s")
|
||||
|
||||
bfs, ok := cache.Fs.(*afero.BasePathFs)
|
||||
c.Assert(ok, qt.Equals, true)
|
||||
filename, err := bfs.RealPath("key")
|
||||
c.Assert(err, qt.IsNil)
|
||||
if test.cacheDir != "" {
|
||||
c.Assert(filename, qt.Equals, filepath.Join(test.cacheDir, "c/"+filecacheRootDirname+"/getjson/key"))
|
||||
} else {
|
||||
// Temp dir.
|
||||
c.Assert(filename, qt.Matches, ".*hugo_cache.*"+filecacheRootDirname+".*key")
|
||||
}
|
||||
|
||||
cache = caches.Get("Images")
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
c.Assert(cache.maxAge, qt.Equals, time.Duration(-1))
|
||||
bfs, ok = cache.Fs.(*afero.BasePathFs)
|
||||
c.Assert(ok, qt.Equals, true)
|
||||
filename, _ = bfs.RealPath("key")
|
||||
@@ -105,7 +123,7 @@ dir = ":cacheDir/c"
|
||||
io.Closer
|
||||
}{
|
||||
strings.NewReader(s),
|
||||
io.NopCloser(nil),
|
||||
ioutil.NopCloser(nil),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -114,13 +132,13 @@ dir = ":cacheDir/c"
|
||||
return []byte("bcd"), nil
|
||||
}
|
||||
|
||||
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
|
||||
for _, ca := range []*Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
|
||||
for i := 0; i < 2; i++ {
|
||||
info, r, err := ca.GetOrCreate("a", rf("abc"))
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(r, qt.Not(qt.IsNil))
|
||||
c.Assert(info.Name, qt.Equals, "a")
|
||||
b, _ := io.ReadAll(r)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, "abc")
|
||||
|
||||
@@ -136,7 +154,7 @@ dir = ":cacheDir/c"
|
||||
|
||||
_, r, err = ca.GetOrCreate("a", rf("bcd"))
|
||||
c.Assert(err, qt.IsNil)
|
||||
b, _ = io.ReadAll(r)
|
||||
b, _ = ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, "abc")
|
||||
}
|
||||
@@ -149,13 +167,13 @@ dir = ":cacheDir/c"
|
||||
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!")
|
||||
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)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
|
||||
@@ -190,7 +208,7 @@ dir = "/cache/c"
|
||||
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
|
||||
caches, err := filecache.NewCaches(p)
|
||||
caches, err := NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
const cacheName = "getjson"
|
||||
@@ -215,7 +233,7 @@ dir = "/cache/c"
|
||||
return hugio.ToReadCloser(strings.NewReader(data)), nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
b, _ := io.ReadAll(r)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, data)
|
||||
// Trigger some expiration.
|
||||
@@ -233,24 +251,24 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.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 {
|
||||
rf := func(failLevel int) func(info ItemInfo, r io.ReadSeeker) error {
|
||||
return func(info ItemInfo, r io.ReadSeeker) error {
|
||||
if failLevel > 0 {
|
||||
if failLevel > 1 {
|
||||
return filecache.ErrFatal
|
||||
return ErrFatal
|
||||
}
|
||||
return errors.New("fail")
|
||||
}
|
||||
|
||||
b, _ := io.ReadAll(r)
|
||||
b, _ := ioutil.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 {
|
||||
bf := func(s string) func(info ItemInfo, w io.WriteCloser) error {
|
||||
return func(info ItemInfo, w io.WriteCloser) error {
|
||||
defer w.Close()
|
||||
result = s
|
||||
_, err := w.Write([]byte(s))
|
||||
@@ -258,7 +276,7 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
cache := filecache.NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
|
||||
cache := NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
|
||||
|
||||
const id = "a32"
|
||||
|
||||
@@ -272,15 +290,60 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
|
||||
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)
|
||||
c.Assert(err, qt.Equals, ErrFatal)
|
||||
}
|
||||
|
||||
func TestCleanID(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert(cleanID(filepath.FromSlash("/a/b//c.txt")), qt.Equals, filepath.FromSlash("a/b/c.txt"))
|
||||
c.Assert(cleanID(filepath.FromSlash("a/b//c.txt")), qt.Equals, filepath.FromSlash("a/b/c.txt"))
|
||||
}
|
||||
|
||||
func initConfig(fs afero.Fs, cfg config.Provider) error {
|
||||
if _, err := langs.LoadLanguageSettings(cfg, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
modConfig, err := modules.DecodeConfig(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
workingDir := cfg.GetString("workingDir")
|
||||
themesDir := cfg.GetString("themesDir")
|
||||
if !filepath.IsAbs(themesDir) {
|
||||
themesDir = filepath.Join(workingDir, themesDir)
|
||||
}
|
||||
globAll := glob.MustCompile("**", '/')
|
||||
modulesClient := modules.NewClient(modules.ClientConfig{
|
||||
Fs: fs,
|
||||
WorkingDir: workingDir,
|
||||
ThemesDir: themesDir,
|
||||
ModuleConfig: modConfig,
|
||||
IgnoreVendor: globAll,
|
||||
})
|
||||
|
||||
moduleConfig, err := modulesClient.Collect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := modules.ApplyProjectConfigDefaults(cfg, moduleConfig.ActiveModules[len(moduleConfig.ActiveModules)-1]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Set("allModules", moduleConfig.ActiveModules)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
initConfig(fs, cfg)
|
||||
config.SetBaseTestDefaults(cfg)
|
||||
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, nil)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
}
|
||||
|
||||
Vendored
-108
@@ -1,108 +0,0 @@
|
||||
// Copyright 2023 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")
|
||||
}
|
||||
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(300 * 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)
|
||||
|
||||
}
|
||||
+6
-10
@@ -452,16 +452,12 @@ func collectMethodsRecursive(pkg string, f []*ast.Field) []string {
|
||||
}
|
||||
|
||||
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)...)
|
||||
}
|
||||
|
||||
// Embedded interface
|
||||
methodNames = append(
|
||||
methodNames,
|
||||
collectMethodsRecursive(
|
||||
pkg,
|
||||
ident.Obj.Decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List)...)
|
||||
} else {
|
||||
// Embedded, but in a different file/package. Return the
|
||||
// package.Name and deal with that later.
|
||||
|
||||
+446
-544
File diff suppressed because it is too large
Load Diff
+321
-19
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,28 +14,330 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/bep/simplecobra"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
hpaths "github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// newExec wires up all of Hugo's CLI.
|
||||
func newExec() (*simplecobra.Exec, error) {
|
||||
rootCmd := &rootCommand{
|
||||
commands: []simplecobra.Commander{
|
||||
newVersionCmd(),
|
||||
newEnvCommand(),
|
||||
newServerCommand(),
|
||||
newDeployCommand(),
|
||||
newConfigCommand(),
|
||||
newNewCommand(),
|
||||
newConvertCommand(),
|
||||
newImportCommand(),
|
||||
newListCommand(),
|
||||
newModCommands(),
|
||||
newGenCommand(),
|
||||
newReleaseCommand(),
|
||||
type commandsBuilder struct {
|
||||
hugoBuilderCommon
|
||||
|
||||
commands []cmder
|
||||
}
|
||||
|
||||
func newCommandsBuilder() *commandsBuilder {
|
||||
return &commandsBuilder{}
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) addCommands(commands ...cmder) *commandsBuilder {
|
||||
b.commands = append(b.commands, commands...)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) addAll() *commandsBuilder {
|
||||
b.addCommands(
|
||||
b.newServerCmd(),
|
||||
newVersionCmd(),
|
||||
newEnvCmd(),
|
||||
b.newConfigCmd(),
|
||||
b.newDeployCmd(),
|
||||
b.newConvertCmd(),
|
||||
b.newNewCmd(),
|
||||
b.newListCmd(),
|
||||
newImportCmd(),
|
||||
newGenCmd(),
|
||||
createReleaser(),
|
||||
b.newModCmd(),
|
||||
)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) build() *hugoCmd {
|
||||
h := b.newHugoCmd()
|
||||
addCommands(h.getCommand(), b.commands...)
|
||||
return h
|
||||
}
|
||||
|
||||
func addCommands(root *cobra.Command, commands ...cmder) {
|
||||
for _, command := range commands {
|
||||
cmd := command.getCommand()
|
||||
if cmd == nil {
|
||||
continue
|
||||
}
|
||||
root.AddCommand(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
type baseCmd struct {
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
var _ commandsBuilderGetter = (*baseBuilderCmd)(nil)
|
||||
|
||||
// Used in tests.
|
||||
type commandsBuilderGetter interface {
|
||||
getCommandsBuilder() *commandsBuilder
|
||||
}
|
||||
|
||||
type baseBuilderCmd struct {
|
||||
*baseCmd
|
||||
*commandsBuilder
|
||||
}
|
||||
|
||||
func (b *baseBuilderCmd) getCommandsBuilder() *commandsBuilder {
|
||||
return b.commandsBuilder
|
||||
}
|
||||
|
||||
func (c *baseCmd) getCommand() *cobra.Command {
|
||||
return c.cmd
|
||||
}
|
||||
|
||||
func newBaseCmd(cmd *cobra.Command) *baseCmd {
|
||||
return &baseCmd{cmd: cmd}
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newBuilderCmd(cmd *cobra.Command) *baseBuilderCmd {
|
||||
bcmd := &baseBuilderCmd{commandsBuilder: b, baseCmd: &baseCmd{cmd: cmd}}
|
||||
bcmd.hugoBuilderCommon.handleFlags(cmd)
|
||||
return bcmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newBuilderBasicCmd(cmd *cobra.Command) *baseBuilderCmd {
|
||||
bcmd := &baseBuilderCmd{commandsBuilder: b, baseCmd: &baseCmd{cmd: cmd}}
|
||||
bcmd.hugoBuilderCommon.handleCommonBuilderFlags(cmd)
|
||||
return bcmd
|
||||
}
|
||||
|
||||
func (c *baseCmd) flagsToConfig(cfg config.Provider) {
|
||||
initializeFlags(c.cmd, cfg)
|
||||
}
|
||||
|
||||
type hugoCmd struct {
|
||||
*baseBuilderCmd
|
||||
|
||||
// Need to get the sites once built.
|
||||
c *commandeer
|
||||
}
|
||||
|
||||
var _ cmder = (*nilCommand)(nil)
|
||||
|
||||
type nilCommand struct{}
|
||||
|
||||
func (c *nilCommand) getCommand() *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *nilCommand) flagsToConfig(cfg config.Provider) {
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newHugoCmd() *hugoCmd {
|
||||
cc := &hugoCmd{}
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderCmd(&cobra.Command{
|
||||
Use: "hugo",
|
||||
Short: "hugo builds your site",
|
||||
Long: `hugo 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/.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
defer cc.timeTrack(time.Now(), "Total")
|
||||
cfgInit := func(c *commandeer) error {
|
||||
if cc.buildWatch {
|
||||
c.Set("disableLiveReload", true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// prevent cobra printing error so it can be handled here (before the timeTrack prints)
|
||||
cmd.SilenceErrors = true
|
||||
|
||||
c, err := initializeConfig(true, true, cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
return err
|
||||
}
|
||||
cc.c = c
|
||||
|
||||
err = c.build()
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
}
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.cfgFile, "config", "", "config file (default is path/config.yaml|json|toml)")
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.cfgDir, "configDir", "config", "config dir")
|
||||
cc.cmd.PersistentFlags().BoolVar(&cc.quiet, "quiet", false, "build in quiet mode")
|
||||
|
||||
// Set bash-completion
|
||||
_ = cc.cmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, config.ValidConfigFileExtensions)
|
||||
|
||||
cc.cmd.PersistentFlags().BoolVarP(&cc.verbose, "verbose", "v", false, "verbose output")
|
||||
cc.cmd.PersistentFlags().BoolVarP(&cc.debug, "debug", "", false, "debug output")
|
||||
cc.cmd.PersistentFlags().BoolVar(&cc.logging, "log", false, "enable Logging")
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.logFile, "logFile", "", "log File path (if set, logging enabled automatically)")
|
||||
cc.cmd.PersistentFlags().BoolVar(&cc.verboseLog, "verboseLog", false, "verbose logging")
|
||||
|
||||
cc.cmd.Flags().BoolVarP(&cc.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
|
||||
|
||||
cc.cmd.Flags().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)")
|
||||
|
||||
// Set bash-completion
|
||||
_ = cc.cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{})
|
||||
|
||||
cc.cmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags)
|
||||
cc.cmd.SilenceUsage = true
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
type hugoBuilderCommon struct {
|
||||
source string
|
||||
baseURL string
|
||||
environment string
|
||||
|
||||
buildWatch bool
|
||||
poll string
|
||||
clock string
|
||||
|
||||
gc bool
|
||||
|
||||
// Profile flags (for debugging of performance problems)
|
||||
cpuprofile string
|
||||
memprofile string
|
||||
mutexprofile string
|
||||
traceprofile string
|
||||
printm bool
|
||||
|
||||
// TODO(bep) var vs string
|
||||
logging bool
|
||||
verbose bool
|
||||
verboseLog bool
|
||||
debug bool
|
||||
quiet bool
|
||||
|
||||
cfgFile string
|
||||
cfgDir string
|
||||
logFile string
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) timeTrack(start time.Time, name string) {
|
||||
if cc.quiet {
|
||||
return
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
fmt.Printf("%s in %v ms\n", name, int(1000*elapsed.Seconds()))
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string {
|
||||
if cc.cfgDir != "" {
|
||||
return hpaths.AbsPathify(baseDir, cc.cfgDir)
|
||||
}
|
||||
|
||||
return simplecobra.New(rootCmd)
|
||||
if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found {
|
||||
return hpaths.AbsPathify(baseDir, v)
|
||||
}
|
||||
|
||||
return hpaths.AbsPathify(baseDir, "config")
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string {
|
||||
if cc.environment != "" {
|
||||
return cc.environment
|
||||
}
|
||||
|
||||
if v, found := os.LookupEnv("HUGO_ENVIRONMENT"); found {
|
||||
return v
|
||||
}
|
||||
|
||||
// Used by Netlify and Forestry
|
||||
if v, found := os.LookupEnv("HUGO_ENV"); found {
|
||||
return v
|
||||
}
|
||||
|
||||
if isServer {
|
||||
return hugo.EnvironmentDevelopment
|
||||
}
|
||||
|
||||
return hugo.EnvironmentProduction
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) handleCommonBuilderFlags(cmd *cobra.Command) {
|
||||
cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
cmd.PersistentFlags().StringVarP(&cc.environment, "environment", "e", "", "build environment")
|
||||
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
|
||||
cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
|
||||
cmd.PersistentFlags().StringVar(&cc.clock, "clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00")
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
cc.handleCommonBuilderFlags(cmd)
|
||||
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().StringP("contentDir", "c", "", "filesystem path to content directory")
|
||||
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
|
||||
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/")
|
||||
cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
|
||||
cmd.Flags().StringP("destination", "d", "", "filesystem path to write files to")
|
||||
cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
|
||||
cmd.Flags().StringVarP(&cc.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. https://spf13.com/")
|
||||
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date, author, and CODEOWNERS info to the pages")
|
||||
cmd.Flags().BoolVar(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
|
||||
cmd.Flags().StringVar(&cc.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
|
||||
cmd.Flags().BoolVar(&loggers.PanicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
|
||||
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
|
||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||
cmd.Flags().BoolP("forceSyncStatic", "", false, "copy all files when static is changed.")
|
||||
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
|
||||
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
|
||||
cmd.Flags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
|
||||
cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations")
|
||||
cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
|
||||
cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.")
|
||||
cmd.Flags().StringVarP(&cc.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
|
||||
cmd.Flags().StringVarP(&cc.memprofile, "profile-mem", "", "", "write memory profile to `file`")
|
||||
cmd.Flags().BoolVarP(&cc.printm, "printMemoryUsage", "", false, "print memory usage to screen at intervals")
|
||||
cmd.Flags().StringVarP(&cc.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`")
|
||||
cmd.Flags().StringVarP(&cc.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)")
|
||||
|
||||
// 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.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)")
|
||||
|
||||
// Set bash-completion.
|
||||
// Each flag must first be defined before using the SetAnnotation() call.
|
||||
_ = cmd.Flags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.Flags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
|
||||
}
|
||||
|
||||
func checkErr(logger loggers.Logger, err error, s ...string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
for _, message := range s {
|
||||
logger.Errorln(message)
|
||||
}
|
||||
logger.Errorln(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
// 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 commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestExecute(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
createSite := func(c *qt.C) string {
|
||||
dir := createSimpleTestSite(t, testSiteConfig{})
|
||||
return dir
|
||||
}
|
||||
|
||||
c.Run("hugo", func(c *qt.C) {
|
||||
dir := createSite(c)
|
||||
resp := Execute([]string{"-s=" + dir})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
result := resp.Result
|
||||
c.Assert(len(result.Sites) == 1, qt.Equals, true)
|
||||
c.Assert(len(result.Sites[0].RegularPages()) == 1, qt.Equals, true)
|
||||
c.Assert(result.Sites[0].Info.Params()["myparam"], qt.Equals, "paramproduction")
|
||||
})
|
||||
|
||||
c.Run("hugo, set environment", func(c *qt.C) {
|
||||
dir := createSite(c)
|
||||
resp := Execute([]string{"-s=" + dir, "-e=staging"})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
result := resp.Result
|
||||
c.Assert(result.Sites[0].Info.Params()["myparam"], qt.Equals, "paramstaging")
|
||||
})
|
||||
|
||||
c.Run("convert toJSON", func(c *qt.C) {
|
||||
dir := createSite(c)
|
||||
output := filepath.Join(dir, "myjson")
|
||||
resp := Execute([]string{"convert", "toJSON", "-s=" + dir, "-e=staging", "-o=" + output})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
converted := readFileFrom(c, filepath.Join(output, "content", "p1.md"))
|
||||
c.Assert(converted, qt.Equals, "{\n \"title\": \"P1\",\n \"weight\": 1\n}\n\nContent\n\n", qt.Commentf(converted))
|
||||
})
|
||||
|
||||
c.Run("config, set environment", func(c *qt.C) {
|
||||
dir := createSite(c)
|
||||
out, err := captureStdout(func() error {
|
||||
resp := Execute([]string{"config", "-s=" + dir, "-e=staging"})
|
||||
return resp.Err
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(out, qt.Contains, "params = map[myparam:paramstaging]", qt.Commentf(out))
|
||||
})
|
||||
|
||||
c.Run("deploy, environment set", func(c *qt.C) {
|
||||
dir := createSite(c)
|
||||
resp := Execute([]string{"deploy", "-s=" + dir, "-e=staging", "--target=mydeployment", "--dryRun"})
|
||||
c.Assert(resp.Err, qt.Not(qt.IsNil))
|
||||
c.Assert(resp.Err.Error(), qt.Contains, `no driver registered for "hugocloud"`)
|
||||
})
|
||||
|
||||
c.Run("list", func(c *qt.C) {
|
||||
dir := createSite(c)
|
||||
out, err := captureStdout(func() error {
|
||||
resp := Execute([]string{"list", "all", "-s=" + dir, "-e=staging"})
|
||||
return resp.Err
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(out, qt.Contains, "p1.md")
|
||||
})
|
||||
|
||||
c.Run("new theme", func(c *qt.C) {
|
||||
dir := createSite(c)
|
||||
themesDir := filepath.Join(dir, "mythemes")
|
||||
resp := Execute([]string{"new", "theme", "mytheme", "-s=" + dir, "-e=staging", "--themesDir=" + themesDir})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
themeTOML := readFileFrom(c, filepath.Join(themesDir, "mytheme", "theme.toml"))
|
||||
c.Assert(themeTOML, qt.Contains, "name = \"Mytheme\"")
|
||||
})
|
||||
|
||||
c.Run("new site", func(c *qt.C) {
|
||||
dir := createSite(c)
|
||||
siteDir := filepath.Join(dir, "mysite")
|
||||
resp := Execute([]string{"new", "site", siteDir, "-e=staging"})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
config := readFileFrom(c, filepath.Join(siteDir, "config.toml"))
|
||||
c.Assert(config, qt.Contains, "baseURL = 'http://example.org/'")
|
||||
checkNewSiteInited(c, siteDir)
|
||||
})
|
||||
}
|
||||
|
||||
func checkNewSiteInited(c *qt.C, basepath string) {
|
||||
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 := os.Stat(path)
|
||||
c.Assert(err, qt.IsNil)
|
||||
}
|
||||
}
|
||||
|
||||
func readFileFrom(c *qt.C, filename string) string {
|
||||
c.Helper()
|
||||
filename = filepath.Clean(filename)
|
||||
b, err := afero.ReadFile(hugofs.Os, filename)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestFlags(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
noOpRunE := func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
check func(c *qt.C, cmd *serverCmd)
|
||||
}{
|
||||
{
|
||||
// https://github.com/gohugoio/hugo/issues/7642
|
||||
name: "ignoreVendorPaths",
|
||||
args: []string{"server", "--ignoreVendorPaths=github.com/**"},
|
||||
check: func(c *qt.C, cmd *serverCmd) {
|
||||
cfg := config.NewWithTestDefaults()
|
||||
cmd.flagsToConfig(cfg)
|
||||
c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Persistent flags",
|
||||
args: []string{
|
||||
"server",
|
||||
"--config=myconfig.toml",
|
||||
"--configDir=myconfigdir",
|
||||
"--contentDir=mycontent",
|
||||
"--disableKinds=page,home",
|
||||
"--environment=testing",
|
||||
"--configDir=myconfigdir",
|
||||
"--layoutDir=mylayouts",
|
||||
"--theme=mytheme",
|
||||
"--gc",
|
||||
"--themesDir=mythemes",
|
||||
"--cleanDestinationDir",
|
||||
"--navigateToChanged",
|
||||
"--disableLiveReload",
|
||||
"--noHTTPCache",
|
||||
"--printI18nWarnings",
|
||||
"--destination=/tmp/mydestination",
|
||||
"-b=https://example.com/b/",
|
||||
"--port=1366",
|
||||
"--renderToDisk",
|
||||
"--source=mysource",
|
||||
"--printPathWarnings",
|
||||
"--printUnusedTemplates",
|
||||
},
|
||||
check: func(c *qt.C, sc *serverCmd) {
|
||||
c.Assert(sc, qt.Not(qt.IsNil))
|
||||
c.Assert(sc.navigateToChanged, qt.Equals, true)
|
||||
c.Assert(sc.disableLiveReload, qt.Equals, true)
|
||||
c.Assert(sc.noHTTPCache, qt.Equals, true)
|
||||
c.Assert(sc.renderToDisk, qt.Equals, true)
|
||||
c.Assert(sc.serverPort, qt.Equals, 1366)
|
||||
c.Assert(sc.environment, qt.Equals, "testing")
|
||||
|
||||
cfg := config.NewWithTestDefaults()
|
||||
sc.flagsToConfig(cfg)
|
||||
c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination")
|
||||
c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent")
|
||||
c.Assert(cfg.GetString("layoutDir"), qt.Equals, "mylayouts")
|
||||
c.Assert(cfg.GetStringSlice("theme"), qt.DeepEquals, []string{"mytheme"})
|
||||
c.Assert(cfg.GetString("themesDir"), qt.Equals, "mythemes")
|
||||
c.Assert(cfg.GetString("baseURL"), qt.Equals, "https://example.com/b/")
|
||||
|
||||
c.Assert(cfg.Get("disableKinds"), qt.DeepEquals, []string{"page", "home"})
|
||||
|
||||
c.Assert(cfg.GetBool("gc"), qt.Equals, true)
|
||||
|
||||
// The flag is named printPathWarnings
|
||||
c.Assert(cfg.GetBool("logPathWarnings"), qt.Equals, true)
|
||||
|
||||
// The flag is named printI18nWarnings
|
||||
c.Assert(cfg.GetBool("logI18nWarnings"), qt.Equals, true)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
b := newCommandsBuilder()
|
||||
root := b.addAll().build()
|
||||
|
||||
for _, cmd := range b.commands {
|
||||
if cmd.getCommand() == nil {
|
||||
continue
|
||||
}
|
||||
// We are only intereseted in the flag handling here.
|
||||
cmd.getCommand().RunE = noOpRunE
|
||||
}
|
||||
rootCmd := root.getCommand()
|
||||
rootCmd.SetArgs(test.args)
|
||||
c.Assert(rootCmd.Execute(), qt.IsNil)
|
||||
test.check(c, b.commands[0].(*serverCmd))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandsExecute(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
dir := createSimpleTestSite(t, testSiteConfig{})
|
||||
dirOut := t.TempDir()
|
||||
|
||||
sourceFlag := fmt.Sprintf("-s=%s", dir)
|
||||
|
||||
tests := []struct {
|
||||
commands []string
|
||||
flags []string
|
||||
expectErrToContain string
|
||||
}{
|
||||
// TODO(bep) permission issue on my OSX? "operation not permitted" {[]string{"check", "ulimit"}, nil, false},
|
||||
{[]string{"env"}, nil, ""},
|
||||
{[]string{"version"}, nil, ""},
|
||||
// no args = hugo build
|
||||
{nil, []string{sourceFlag}, ""},
|
||||
{nil, []string{sourceFlag, "--renderToMemory"}, ""},
|
||||
{[]string{"completion", "bash"}, nil, ""},
|
||||
{[]string{"completion", "fish"}, nil, ""},
|
||||
{[]string{"completion", "powershell"}, nil, ""},
|
||||
{[]string{"completion", "zsh"}, nil, ""},
|
||||
{[]string{"config"}, []string{sourceFlag}, ""},
|
||||
{[]string{"convert", "toTOML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "toml")}, ""},
|
||||
{[]string{"convert", "toYAML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "yaml")}, ""},
|
||||
{[]string{"convert", "toJSON"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "json")}, ""},
|
||||
{[]string{"gen", "chromastyles"}, []string{"--style=manni"}, ""},
|
||||
{[]string{"gen", "doc"}, []string{"--dir=" + filepath.Join(dirOut, "doc")}, ""},
|
||||
{[]string{"gen", "man"}, []string{"--dir=" + filepath.Join(dirOut, "man")}, ""},
|
||||
{[]string{"list", "drafts"}, []string{sourceFlag}, ""},
|
||||
{[]string{"list", "expired"}, []string{sourceFlag}, ""},
|
||||
{[]string{"list", "future"}, []string{sourceFlag}, ""},
|
||||
{[]string{"new", "new-page.md"}, []string{sourceFlag}, ""},
|
||||
{[]string{"new", "site", filepath.Join(dirOut, "new-site")}, nil, ""},
|
||||
{[]string{"unknowncommand"}, nil, "unknown command"},
|
||||
// TODO(bep) cli refactor fix https://github.com/gohugoio/hugo/issues/4450
|
||||
//{[]string{"new", "theme", filepath.Join(dirOut, "new-theme")}, nil,false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
name := "hugo"
|
||||
if len(test.commands) > 0 {
|
||||
name = test.commands[0]
|
||||
}
|
||||
c.Run(name, func(c *qt.C) {
|
||||
b := newCommandsBuilder().addAll().build()
|
||||
hugoCmd := b.getCommand()
|
||||
test.flags = append(test.flags, "--quiet")
|
||||
hugoCmd.SetArgs(append(test.commands, test.flags...))
|
||||
|
||||
// TODO(bep) capture output and add some simple asserts
|
||||
// TODO(bep) misspelled subcommands does not return an error. We should investigate this
|
||||
// but before that, check for "Error: unknown command".
|
||||
|
||||
_, err := hugoCmd.ExecuteC()
|
||||
if test.expectErrToContain != "" {
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
c.Assert(err.Error(), qt.Contains, test.expectErrToContain)
|
||||
} else {
|
||||
c.Assert(err, qt.IsNil)
|
||||
}
|
||||
|
||||
// Assert that we have not left any development debug artifacts in
|
||||
// the code.
|
||||
if b.c != nil {
|
||||
_, ok := b.c.publishDirFs.(types.DevMarker)
|
||||
c.Assert(ok, qt.Equals, false)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
type testSiteConfig struct {
|
||||
configTOML string
|
||||
contentDir string
|
||||
}
|
||||
|
||||
func createSimpleTestSite(t testing.TB, cfg testSiteConfig) string {
|
||||
dir := t.TempDir()
|
||||
|
||||
cfgStr := `
|
||||
|
||||
baseURL = "https://example.org"
|
||||
title = "Hugo Commands"
|
||||
|
||||
|
||||
`
|
||||
|
||||
contentDir := "content"
|
||||
|
||||
if cfg.configTOML != "" {
|
||||
cfgStr = cfg.configTOML
|
||||
}
|
||||
if cfg.contentDir != "" {
|
||||
contentDir = cfg.contentDir
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Join(dir, "public"), 0777)
|
||||
|
||||
// Just the basic. These are for CLI tests, not site testing.
|
||||
writeFile(t, filepath.Join(dir, "config.toml"), cfgStr)
|
||||
writeFile(t, filepath.Join(dir, "config", "staging", "params.toml"), `myparam="paramstaging"`)
|
||||
writeFile(t, filepath.Join(dir, "config", "staging", "deployment.toml"), `
|
||||
[[targets]]
|
||||
name = "mydeployment"
|
||||
URL = "hugocloud://hugotestbucket"
|
||||
`)
|
||||
|
||||
writeFile(t, filepath.Join(dir, "config", "testing", "params.toml"), `myparam="paramtesting"`)
|
||||
writeFile(t, filepath.Join(dir, "config", "production", "params.toml"), `myparam="paramproduction"`)
|
||||
|
||||
writeFile(t, filepath.Join(dir, "static", "myfile.txt"), `Hello World!`)
|
||||
|
||||
writeFile(t, filepath.Join(dir, contentDir, "p1.md"), `
|
||||
---
|
||||
title: "P1"
|
||||
weight: 1
|
||||
---
|
||||
|
||||
Content
|
||||
|
||||
`)
|
||||
|
||||
writeFile(t, filepath.Join(dir, "layouts", "_default", "single.html"), `
|
||||
|
||||
Single: {{ .Title }}
|
||||
|
||||
`)
|
||||
|
||||
writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), `
|
||||
|
||||
List: {{ .Title }}
|
||||
Environment: {{ hugo.Environment }}
|
||||
|
||||
For issue 9788:
|
||||
{{ $foo :="abc" | resources.FromString "foo.css" | minify | resources.PostProcess }}
|
||||
PostProcess: {{ $foo.RelPermalink }}
|
||||
|
||||
`)
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func writeFile(t testing.TB, filename, content string) {
|
||||
must(t, os.MkdirAll(filepath.Dir(filename), os.FileMode(0755)))
|
||||
must(t, ioutil.WriteFile(filename, []byte(content), os.FileMode(0755)))
|
||||
}
|
||||
|
||||
func must(t testing.TB, err error) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
+85
-135
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 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.
|
||||
@@ -9,137 +9,128 @@
|
||||
// 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.
|
||||
// limitations under the License.Print the version number of Hug
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"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/gohugoio/hugo/modules"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// newConfigCommand creates a new config command and its subcommands.
|
||||
func newConfigCommand() *configCommand {
|
||||
return &configCommand{
|
||||
commands: []simplecobra.Commander{
|
||||
&configMountsCommand{},
|
||||
},
|
||||
var _ cmder = (*configCmd)(nil)
|
||||
|
||||
type configCmd struct {
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newConfigCmd() *configCmd {
|
||||
cc := &configCmd{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Print the site configuration",
|
||||
Long: `Print the site configuration, both default and custom settings.`,
|
||||
RunE: cc.printConfig,
|
||||
}
|
||||
|
||||
printMountsCmd := &cobra.Command{
|
||||
Use: "mounts",
|
||||
Short: "Print the configured file mounts",
|
||||
RunE: cc.printMounts,
|
||||
}
|
||||
|
||||
cmd.AddCommand(printMountsCmd)
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
type configCommand struct {
|
||||
r *rootCommand
|
||||
|
||||
format string
|
||||
lang string
|
||||
|
||||
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(c.r.configVersionID.Load(), flagsToCfg(cd, nil))
|
||||
func (c *configCmd) printMounts(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := initializeConfig(true, false, false, &c.hugoBuilderCommon, c, 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)
|
||||
|
||||
allModules := cfg.Cfg.Get("allmodules").(modules.Modules)
|
||||
|
||||
for _, m := range allModules {
|
||||
if err := parser.InterfaceToConfig(&modMounts{m: m, verbose: c.verbose}, metadecoders.JSON, os.Stdout); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
config = conf.configs.LanguageConfigSlice[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
dec := json.NewEncoder(&buf)
|
||||
dec.SetIndent("", " ")
|
||||
dec.SetEscapeHTML(false)
|
||||
|
||||
if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: true}); err != nil {
|
||||
func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := initializeConfig(true, false, false, &c.hugoBuilderCommon, c, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
format := strings.ToLower(c.format)
|
||||
allSettings := cfg.Cfg.Get("").(maps.Params)
|
||||
|
||||
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]interface{}
|
||||
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
|
||||
return err
|
||||
// We need to clean up this, but we store objects in the config that
|
||||
// isn't really interesting to the end user, so filter these.
|
||||
ignoreKeysRe := regexp.MustCompile("client|sorted|filecacheconfigs|allmodules|multilingual")
|
||||
|
||||
separator := ": "
|
||||
|
||||
if len(cfg.configFiles) > 0 && strings.HasSuffix(cfg.configFiles[0], ".toml") {
|
||||
separator = " = "
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for k := range allSettings {
|
||||
if ignoreKeysRe.MatchString(k) {
|
||||
continue
|
||||
}
|
||||
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)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
kv := reflect.ValueOf(allSettings[k])
|
||||
if kv.Kind() == reflect.String {
|
||||
fmt.Printf("%s%s\"%+v\"\n", k, separator, allSettings[k])
|
||||
} else {
|
||||
fmt.Printf("%s%s%+v\n", k, separator, allSettings[k])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
c.r = cd.Root.Command.(*rootCommand)
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Print the site configuration"
|
||||
cmd.Long = `Print the site configuration, both default and custom settings.`
|
||||
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
|
||||
applyLocalFlagsBuildConfig(cmd, c.r)
|
||||
|
||||
return nil
|
||||
type modMounts struct {
|
||||
verbose bool
|
||||
m modules.Module
|
||||
}
|
||||
|
||||
func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type configModMount struct {
|
||||
type modMount 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
|
||||
func (m *modMounts) MarshalJSON() ([]byte, error) {
|
||||
var mounts []modMount
|
||||
|
||||
for _, mount := range m.m.Mounts() {
|
||||
mounts = append(mounts, configModMount{
|
||||
mounts = append(mounts, modMount{
|
||||
Source: mount.Source,
|
||||
Target: mount.Target,
|
||||
Lang: mount.Lang,
|
||||
@@ -162,7 +153,7 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
|
||||
Meta map[string]any `json:"meta"`
|
||||
HugoVersion modules.HugoVersion `json:"hugoVersion"`
|
||||
|
||||
Mounts []configModMount `json:"mounts"`
|
||||
Mounts []modMount `json:"mounts"`
|
||||
}{
|
||||
Path: m.m.Path(),
|
||||
Version: m.m.Version(),
|
||||
@@ -176,12 +167,12 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
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 string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Time time.Time `json:"time"`
|
||||
Owner string `json:"owner"`
|
||||
Dir string `json:"dir"`
|
||||
Mounts []modMount `json:"mounts"`
|
||||
}{
|
||||
Path: m.m.Path(),
|
||||
Version: m.m.Version(),
|
||||
@@ -192,44 +183,3 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
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(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"
|
||||
applyLocalFlagsBuildConfig(cmd, c.r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *configMountsCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
c.configCmd = cd.Parent.Command.(*configCommand)
|
||||
return nil
|
||||
}
|
||||
|
||||
+100
-118
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,121 +15,122 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/parser/pageparser"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"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/gohugoio/hugo/hugolib"
|
||||
|
||||
"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
|
||||
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) {
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "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) {
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "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) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return c
|
||||
}
|
||||
var _ cmder = (*convertCmd)(nil)
|
||||
|
||||
type convertCommand struct {
|
||||
// Flags.
|
||||
type convertCmd struct {
|
||||
outputDir string
|
||||
unsafe bool
|
||||
|
||||
// Deps.
|
||||
r *rootCommand
|
||||
h *hugolib.HugoSites
|
||||
|
||||
// Commands.
|
||||
commands []simplecobra.Commander
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (c *convertCommand) Commands() []simplecobra.Commander {
|
||||
return c.commands
|
||||
func (b *commandsBuilder) newConvertCmd() *convertCmd {
|
||||
cc := &convertCmd{}
|
||||
|
||||
cmd := &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,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
&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.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cc.convertContents(metadecoders.JSON)
|
||||
},
|
||||
},
|
||||
&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.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cc.convertContents(metadecoders.TOML)
|
||||
},
|
||||
},
|
||||
&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.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cc.convertContents(metadecoders.YAML)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&cc.outputDir, "output", "o", "", "filesystem path to write files to")
|
||||
cmd.PersistentFlags().BoolVar(&cc.unsafe, "unsafe", false, "enable less safe operations, please backup first")
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
func (c *convertCommand) Name() string {
|
||||
return "convert"
|
||||
}
|
||||
func (cc *convertCmd) convertContents(format metadecoders.Format) error {
|
||||
if cc.outputDir == "" && !cc.unsafe {
|
||||
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
|
||||
}
|
||||
|
||||
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 your content to different formats"
|
||||
cmd.Long = `Convert your content (e.g. front matter) to different formats.
|
||||
|
||||
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.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))
|
||||
c, err := initializeConfig(true, false, false, &cc.hugoBuilderCommon, cc, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.h = h
|
||||
|
||||
c.Cfg.Set("buildDrafts", true)
|
||||
|
||||
h, err := hugolib.NewHugoSites(*c.DepsCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
site := h.Sites[0]
|
||||
|
||||
site.Log.Println("processing", len(site.AllPages()), "content files")
|
||||
for _, p := range site.AllPages() {
|
||||
if err := cc.convertAndSavePage(p, site, format); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
|
||||
func (cc *convertCmd) 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 {
|
||||
if err := cc.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -139,9 +140,9 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
|
||||
return nil
|
||||
}
|
||||
|
||||
errMsg := fmt.Errorf("error processing file %q", p.File().Path())
|
||||
errMsg := fmt.Errorf("Error processing file %q", p.File().Path())
|
||||
|
||||
site.Log.Infoln("attempting to convert", p.File().Filename())
|
||||
site.Log.Infoln("Attempting to convert", p.File().Filename())
|
||||
|
||||
f := p.File()
|
||||
file, err := f.FileInfo().Meta().Open()
|
||||
@@ -181,45 +182,26 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
|
||||
|
||||
newFilename := p.File().Filename()
|
||||
|
||||
if c.outputDir != "" {
|
||||
if cc.outputDir != "" {
|
||||
contentDir := strings.TrimSuffix(newFilename, p.File().Path())
|
||||
contentDir = filepath.Base(contentDir)
|
||||
|
||||
newFilename = filepath.Join(c.outputDir, contentDir, p.File().Path())
|
||||
newFilename = filepath.Join(cc.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 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")
|
||||
}
|
||||
type parsedFile struct {
|
||||
frontMatterFormat metadecoders.Format
|
||||
frontMatterSource []byte
|
||||
frontMatter map[string]any
|
||||
|
||||
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().IsZero() {
|
||||
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
|
||||
// Everything after Front Matter
|
||||
content []byte
|
||||
}
|
||||
|
||||
+48
-33
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,58 +14,73 @@
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
|
||||
// Copyright 2023 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"
|
||||
"github.com/gohugoio/hugo/deploy"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDeployCommand() simplecobra.Commander {
|
||||
var _ cmder = (*deployCmd)(nil)
|
||||
|
||||
return &simpleCommand{
|
||||
name: "deploy",
|
||||
short: "Deploy your site to a Cloud provider.",
|
||||
long: `Deploy your site to a Cloud provider.
|
||||
// deployCmd supports deploying sites to Cloud providers.
|
||||
type deployCmd struct {
|
||||
*baseBuilderCmd
|
||||
|
||||
invalidateCDN bool
|
||||
maxDeletes int
|
||||
}
|
||||
|
||||
// TODO: In addition to the "deploy" command, consider adding a "--deploy"
|
||||
// flag for the default command; this would build the site and then deploy it.
|
||||
// It's not obvious how to do this; would all of the deploy-specific flags
|
||||
// have to exist at the top level as well?
|
||||
|
||||
// TODO: The output files change every time "hugo" is executed, it looks
|
||||
// like because of map order randomization. This means that you can
|
||||
// run "hugo && hugo deploy" again and again and upload new stuff every time. Is
|
||||
// this intended?
|
||||
|
||||
func (b *commandsBuilder) newDeployCmd() *deployCmd {
|
||||
cc := &deployCmd{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "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"))
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
c.Set("invalidateCDN", cc.invalidateCDN)
|
||||
c.Set("maxDeletes", cc.maxDeletes)
|
||||
return nil
|
||||
}
|
||||
comm, err := initializeConfig(true, true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.Log, h.PathSpec.PublishFs)
|
||||
deployer, err := deploy.New(comm.Cfg, comm.hugo().PathSpec.PublishFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return deployer.Deploy(ctx)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
|
||||
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", deploy.DefaultConfig.InvalidateCDN, "invalidate the CDN cache listed in the deployment target")
|
||||
cmd.Flags().Int("maxDeletes", deploy.DefaultConfig.MaxDeletes, "maximum # of files to delete, or -1 to disable")
|
||||
cmd.Flags().Int("workers", deploy.DefaultConfig.Workers, "number of workers to transfer files. defaults to 10")
|
||||
return deployer.Deploy(context.Background())
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
|
||||
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().BoolVar(&cc.invalidateCDN, "invalidateCDN", true, "invalidate the CDN cache listed in the deployment target")
|
||||
cmd.Flags().IntVar(&cc.maxDeletes, "maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2023 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 nodeploy
|
||||
// +build nodeploy
|
||||
|
||||
// Copyright 2023 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"
|
||||
"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 nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Hidden = true
|
||||
},
|
||||
}
|
||||
}
|
||||
+35
-38
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 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,50 +14,47 @@
|
||||
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: "Print Hugo version and environment info",
|
||||
long: "Print Hugo version and environment info. This is useful in Hugo bug reports",
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
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 _ cmder = (*envCmd)(nil)
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
type envCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
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: "Print Hugo version and environment info",
|
||||
long: "Print Hugo version and environment info. This is useful in Hugo bug reports.",
|
||||
func newEnvCmd() *envCmd {
|
||||
return &envCmd{
|
||||
baseCmd: newBaseCmd(&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.
|
||||
|
||||
If you add the -v flag, you will get a full dependency list.
|
||||
`,
|
||||
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())
|
||||
|
||||
isVerbose, _ := cmd.Flags().GetBool("verbose")
|
||||
|
||||
if isVerbose {
|
||||
deps := hugo.GetDependencyList()
|
||||
for _, dep := range deps {
|
||||
jww.FEEDBACK.Printf("%s\n", dep)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-253
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 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,263 +14,27 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"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
|
||||
linesStyle string
|
||||
)
|
||||
|
||||
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 {
|
||||
builder := styles.Get(style).Builder()
|
||||
if highlightStyle != "" {
|
||||
builder.Add(chroma.LineHighlight, highlightStyle)
|
||||
}
|
||||
if linesStyle != "" {
|
||||
builder.Add(chroma.LineNumbers, linesStyle)
|
||||
}
|
||||
style, err := builder.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatter := html.New(html.WithAllClasses(true))
|
||||
formatter.WriteCSS(os.Stdout, style)
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
|
||||
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
|
||||
cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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, 0777); 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.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.")
|
||||
// For bash-completion
|
||||
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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, strings.Replace(base, "_", " ", -1), base, url)
|
||||
}
|
||||
|
||||
linkHandler := func(name string) string {
|
||||
base := strings.TrimSuffix(name, path.Ext(name))
|
||||
return "/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.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
|
||||
// For bash-completion
|
||||
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.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]interface{}
|
||||
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.PersistentFlags().StringVarP(&docsHelperTarget, "dir", "", "docs/data", "data dir")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &genCommand{
|
||||
commands: []simplecobra.Commander{
|
||||
newChromaStyles(),
|
||||
newGen(),
|
||||
newMan(),
|
||||
newDocsHelper(),
|
||||
},
|
||||
}
|
||||
var _ cmder = (*genCmd)(nil)
|
||||
|
||||
type genCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
type genCommand struct {
|
||||
rootCmd *rootCommand
|
||||
func newGenCmd() *genCmd {
|
||||
cc := &genCmd{}
|
||||
cc.baseCmd = newBaseCmd(&cobra.Command{
|
||||
Use: "gen",
|
||||
Short: "A collection of several useful generators.",
|
||||
})
|
||||
|
||||
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 = "A collection of several useful generators."
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *genCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
c.rootCmd = cd.Root.Command.(*rootCommand)
|
||||
return nil
|
||||
cc.cmd.AddCommand(
|
||||
newGenDocCmd().getCommand(),
|
||||
newGenManCmd().getCommand(),
|
||||
createGenDocsHelper().getCommand(),
|
||||
createGenChromaStyles().getCommand())
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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 (
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var _ cmder = (*genChromaStyles)(nil)
|
||||
|
||||
type genChromaStyles struct {
|
||||
style string
|
||||
highlightStyle string
|
||||
linesStyle string
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
// TODO(bep) highlight
|
||||
func createGenChromaStyles() *genChromaStyles {
|
||||
g := &genChromaStyles{
|
||||
baseCmd: newBaseCmd(&cobra.Command{
|
||||
Use: "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`,
|
||||
}),
|
||||
}
|
||||
|
||||
g.cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return g.generate()
|
||||
}
|
||||
|
||||
g.cmd.PersistentFlags().StringVar(&g.style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
|
||||
g.cmd.PersistentFlags().StringVar(&g.highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
|
||||
g.cmd.PersistentFlags().StringVar(&g.linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *genChromaStyles) generate() error {
|
||||
builder := styles.Get(g.style).Builder()
|
||||
if g.highlightStyle != "" {
|
||||
builder.Add(chroma.LineHighlight, g.highlightStyle)
|
||||
}
|
||||
if g.linesStyle != "" {
|
||||
builder.Add(chroma.LineNumbers, g.linesStyle)
|
||||
}
|
||||
style, err := builder.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatter := html.New(html.WithAllClasses(true))
|
||||
formatter.WriteCSS(os.Stdout, style)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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"
|
||||
|
||||
"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 _ cmder = (*genDocCmd)(nil)
|
||||
|
||||
type genDocCmd struct {
|
||||
gendocdir string
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newGenDocCmd() *genDocCmd {
|
||||
const gendocFrontmatterTemplate = `---
|
||||
title: "%s"
|
||||
slug: %s
|
||||
url: %s
|
||||
---
|
||||
`
|
||||
|
||||
cc := &genDocCmd{}
|
||||
|
||||
cc.baseCmd = newBaseCmd(&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 https://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 {
|
||||
cmd.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(cc.gendocdir, helpers.FilePathSeparator) {
|
||||
cc.gendocdir += helpers.FilePathSeparator
|
||||
}
|
||||
if found, _ := helpers.Exists(cc.gendocdir, hugofs.Os); !found {
|
||||
jww.FEEDBACK.Println("Directory", cc.gendocdir, "does not exist, creating...")
|
||||
if err := hugofs.Os.MkdirAll(cc.gendocdir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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, 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", cc.gendocdir, "...")
|
||||
doc.GenMarkdownTreeCustom(cmd.Root(), cc.gendocdir, prepender, linkHandler)
|
||||
jww.FEEDBACK.Println("Done.")
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
|
||||
|
||||
// For bash-completion
|
||||
cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
|
||||
return cc
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
var _ cmder = (*genDocsHelper)(nil)
|
||||
|
||||
type genDocsHelper struct {
|
||||
target string
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func createGenDocsHelper() *genDocsHelper {
|
||||
g := &genDocsHelper{
|
||||
baseCmd: newBaseCmd(&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.GetDocProvider()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Done!")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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/common/hugo"
|
||||
"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 _ cmder = (*genManCmd)(nil)
|
||||
|
||||
type genManCmd struct {
|
||||
genmandir string
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newGenManCmd() *genManCmd {
|
||||
cc := &genManCmd{}
|
||||
|
||||
cc.baseCmd = newBaseCmd(&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", hugo.CurrentVersion),
|
||||
}
|
||||
if !strings.HasSuffix(cc.genmandir, helpers.FilePathSeparator) {
|
||||
cc.genmandir += helpers.FilePathSeparator
|
||||
}
|
||||
if found, _ := helpers.Exists(cc.genmandir, hugofs.Os); !found {
|
||||
jww.FEEDBACK.Println("Directory", cc.genmandir, "does not exist, creating...")
|
||||
if err := hugofs.Os.MkdirAll(cc.genmandir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cmd.Root().DisableAutoGenTag = true
|
||||
|
||||
jww.FEEDBACK.Println("Generating Hugo man pages in", cc.genmandir, "...")
|
||||
doc.GenManTree(cmd.Root(), header, cc.genmandir)
|
||||
|
||||
jww.FEEDBACK.Println("Done.")
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.genmandir, "dir", "man/", "the directory to write the man pages.")
|
||||
|
||||
// For bash-completion
|
||||
cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
|
||||
return cc
|
||||
}
|
||||
+43
-86
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,19 +11,16 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package commands defines and implements command-line commands and flags
|
||||
// used by Hugo. Commands and flags are implemented using Cobra.
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"regexp"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,90 +30,50 @@ const (
|
||||
showCursor = ansiEsc + "[?25h"
|
||||
)
|
||||
|
||||
func newUserError(a ...any) *simplecobra.CommandError {
|
||||
return &simplecobra.CommandError{Err: errors.New(fmt.Sprint(a...))}
|
||||
type flagsToConfigHandler interface {
|
||||
flagsToConfig(cfg config.Provider)
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
}
|
||||
type cmder interface {
|
||||
flagsToConfigHandler
|
||||
getCommand() *cobra.Command
|
||||
}
|
||||
|
||||
func flagsToCfg(cd *simplecobra.Commandeer, cfg config.Provider) config.Provider {
|
||||
return flagsToCfgWithAdditionalConfigBase(cd, cfg, "")
|
||||
// commandError is an error used to signal different error situations in command handling.
|
||||
type commandError struct {
|
||||
s string
|
||||
userError bool
|
||||
}
|
||||
|
||||
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 (c commandError) Error() string {
|
||||
return c.s
|
||||
}
|
||||
|
||||
func mkdir(x ...string) {
|
||||
p := filepath.Join(x...)
|
||||
err := os.MkdirAll(p, 0777) // before umask
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
func (c commandError) isUserError() bool {
|
||||
return c.userError
|
||||
}
|
||||
|
||||
func newUserError(a ...any) commandError {
|
||||
return commandError{s: fmt.Sprintln(a...), userError: true}
|
||||
}
|
||||
|
||||
func newSystemError(a ...any) commandError {
|
||||
return commandError{s: fmt.Sprintln(a...), userError: false}
|
||||
}
|
||||
|
||||
func newSystemErrorF(format string, a ...any) commandError {
|
||||
return commandError{s: fmt.Sprintf(format, a...), userError: false}
|
||||
}
|
||||
|
||||
// Catch some of the obvious user errors from Cobra.
|
||||
// We don't want to show the usage message for every error.
|
||||
// The below may be to generic. Time will show.
|
||||
var userErrorRegexp = regexp.MustCompile("unknown flag")
|
||||
|
||||
func isUserError(err error) bool {
|
||||
if cErr, ok := err.(commandError); ok && cErr.isUserError() {
|
||||
return true
|
||||
}
|
||||
|
||||
return userErrorRegexp.MatchString(err.Error())
|
||||
}
|
||||
|
||||
+1253
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
||||
// 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 commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bep/clock"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
// Issue #5662
|
||||
func TestHugoWithContentDirOverride(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
files := `
|
||||
-- config.toml --
|
||||
baseURL = "https://example.org"
|
||||
title = "Hugo Commands"
|
||||
-- mycontent/p1.md --
|
||||
---
|
||||
title: "P1"
|
||||
---
|
||||
-- layouts/_default/single.html --
|
||||
Page: {{ .Title }}|
|
||||
|
||||
`
|
||||
s := newTestHugoCmdBuilder(c, files, []string{"-c", "mycontent"}).Build()
|
||||
s.AssertFileContent("public/p1/index.html", `Page: P1|`)
|
||||
|
||||
}
|
||||
|
||||
// Issue #9794
|
||||
func TestHugoStaticFilesMultipleStaticAndManyFolders(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
files := `
|
||||
-- config.toml --
|
||||
baseURL = "https://example.org"
|
||||
theme = "mytheme"
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
|
||||
`
|
||||
const (
|
||||
numDirs = 33
|
||||
numFilesMax = 12
|
||||
)
|
||||
|
||||
r := rand.New(rand.NewSource(32))
|
||||
|
||||
for i := 0; i < numDirs; i++ {
|
||||
for j := 0; j < r.Intn(numFilesMax); j++ {
|
||||
if j%3 == 0 {
|
||||
files += fmt.Sprintf("-- themes/mytheme/static/d%d/f%d.txt --\nHellot%d-%d\n", i, j, i, j)
|
||||
files += fmt.Sprintf("-- themes/mytheme/static/d%d/ft%d.txt --\nHellot%d-%d\n", i, j, i, j)
|
||||
}
|
||||
files += fmt.Sprintf("-- static/d%d/f%d.txt --\nHello%d-%d\n", i, j, i, j)
|
||||
}
|
||||
}
|
||||
|
||||
r = rand.New(rand.NewSource(32))
|
||||
|
||||
s := newTestHugoCmdBuilder(c, files, []string{"-c", "mycontent"}).Build()
|
||||
for i := 0; i < numDirs; i++ {
|
||||
for j := 0; j < r.Intn(numFilesMax); j++ {
|
||||
if j%3 == 0 {
|
||||
if j%3 == 0 {
|
||||
s.AssertFileContent(fmt.Sprintf("public/d%d/ft%d.txt", i, j), fmt.Sprintf("Hellot%d-%d", i, j))
|
||||
}
|
||||
s.AssertFileContent(fmt.Sprintf("public/d%d/f%d.txt", i, j), fmt.Sprintf("Hello%d-%d", i, j))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Issue #8787
|
||||
func TestHugoListCommandsWithClockFlag(t *testing.T) {
|
||||
t.Cleanup(func() { htime.Clock = clock.System() })
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
files := `
|
||||
-- config.toml --
|
||||
baseURL = "https://example.org"
|
||||
title = "Hugo Commands"
|
||||
timeZone = "UTC"
|
||||
-- content/past.md --
|
||||
---
|
||||
title: "Past"
|
||||
date: 2000-11-06
|
||||
---
|
||||
-- content/future.md --
|
||||
---
|
||||
title: "Future"
|
||||
date: 2200-11-06
|
||||
---
|
||||
-- layouts/_default/single.html --
|
||||
Page: {{ .Title }}|
|
||||
|
||||
`
|
||||
s := newTestHugoCmdBuilder(c, files, []string{"list", "future"})
|
||||
s.captureOut = true
|
||||
s.Build()
|
||||
p := filepath.Join("content", "future.md")
|
||||
s.AssertStdout(p + ",2200-11-06T00:00:00Z")
|
||||
|
||||
s = newTestHugoCmdBuilder(c, files, []string{"list", "future", "--clock", "2300-11-06"}).Build()
|
||||
s.AssertStdout("")
|
||||
}
|
||||
|
||||
type testHugoCmdBuilder struct {
|
||||
*qt.C
|
||||
|
||||
fs afero.Fs
|
||||
dir string
|
||||
files string
|
||||
args []string
|
||||
|
||||
captureOut bool
|
||||
out string
|
||||
}
|
||||
|
||||
func newTestHugoCmdBuilder(c *qt.C, files string, args []string) *testHugoCmdBuilder {
|
||||
s := &testHugoCmdBuilder{C: c, files: files, args: args}
|
||||
s.dir = s.TempDir()
|
||||
s.fs = afero.NewBasePathFs(hugofs.Os, s.dir)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testHugoCmdBuilder) Build() *testHugoCmdBuilder {
|
||||
data := txtar.Parse([]byte(s.files))
|
||||
|
||||
for _, f := range data.Files {
|
||||
filename := filepath.Clean(f.Name)
|
||||
data := bytes.TrimSuffix(f.Data, []byte("\n"))
|
||||
s.Assert(s.fs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil)
|
||||
s.Assert(afero.WriteFile(s.fs, filename, data, 0666), qt.IsNil)
|
||||
}
|
||||
|
||||
hugoCmd := newCommandsBuilder().addAll().build()
|
||||
cmd := hugoCmd.getCommand()
|
||||
args := append(s.args, "-s="+s.dir, "--quiet")
|
||||
cmd.SetArgs(args)
|
||||
|
||||
if s.captureOut {
|
||||
out, err := captureStdout(func() error {
|
||||
_, err := cmd.ExecuteC()
|
||||
return err
|
||||
})
|
||||
s.Assert(err, qt.IsNil)
|
||||
s.out = out
|
||||
} else {
|
||||
_, err := cmd.ExecuteC()
|
||||
s.Assert(err, qt.IsNil)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *testHugoCmdBuilder) AssertFileContent(filename string, matches ...string) {
|
||||
s.Helper()
|
||||
data, err := afero.ReadFile(s.fs, filename)
|
||||
s.Assert(err, qt.IsNil)
|
||||
content := strings.TrimSpace(string(data))
|
||||
for _, m := range matches {
|
||||
lines := strings.Split(m, "\n")
|
||||
for _, match := range lines {
|
||||
match = strings.TrimSpace(match)
|
||||
if match == "" || strings.HasPrefix(match, "#") {
|
||||
continue
|
||||
}
|
||||
s.Assert(content, qt.Contains, match, qt.Commentf(m))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *testHugoCmdBuilder) AssertStdout(match string) {
|
||||
s.Helper()
|
||||
content := strings.TrimSpace(s.out)
|
||||
s.Assert(content, qt.Contains, strings.TrimSpace(match))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 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.
|
||||
@@ -25,9 +25,9 @@ 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.
|
||||
|
||||
You need to open PowerShell and run Hugo from there.
|
||||
Hugo is a command-line tool for generating static website.
|
||||
|
||||
You need to open cmd.exe and run Hugo from there.
|
||||
|
||||
Visit https://gohugo.io/ for more information.`
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,97 +15,252 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/parser/pageparser"
|
||||
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
"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"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
func newImportCommand() *importCommand {
|
||||
var c *importCommand
|
||||
c = &importCommand{
|
||||
commands: []simplecobra.Commander{
|
||||
&simpleCommand{
|
||||
name: "jekyll",
|
||||
short: "hugo import from Jekyll",
|
||||
long: `hugo import from Jekyll.
|
||||
|
||||
var _ cmder = (*importCmd)(nil)
|
||||
|
||||
type importCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newImportCmd() *importCmd {
|
||||
cc := &importCmd{}
|
||||
|
||||
cc.baseCmd = newBaseCmd(&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,
|
||||
})
|
||||
|
||||
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`.",
|
||||
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`.")
|
||||
RunE: cc.importFromJekyll,
|
||||
}
|
||||
|
||||
importJekyllCmd.Flags().Bool("force", false, "allow import into non-empty target directory")
|
||||
|
||||
cc.cmd.AddCommand(importJekyllCmd)
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
func (i *importCmd) 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("abort: target path should not be inside the Jekyll root")
|
||||
}
|
||||
|
||||
forceImport, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
fs := afero.NewOsFs()
|
||||
jekyllPostDirs, hasAnyPost := i.getJekyllDirInfo(fs, jekyllRoot)
|
||||
if !hasAnyPost {
|
||||
return errors.New("abort: jekyll root contains neither posts nor drafts")
|
||||
}
|
||||
|
||||
err = i.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs, forceImport)
|
||||
|
||||
if err != nil {
|
||||
return newUserError(err)
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Importing...")
|
||||
|
||||
fileCount := 0
|
||||
callback := func(path string, fi hugofs.FileMetaInfo, 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(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")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *importCmd) 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 := i.retrieveJekyllPostDir(fs, subDir); isPostDir {
|
||||
postDirs[entry.Name()] = hasAnyPostInDir
|
||||
if hasAnyPostInDir {
|
||||
hasAnyPost = true
|
||||
}
|
||||
return c.importFromJekyll(args)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolVar(&c.force, "force", false, "allow import into non-empty target directory")
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return postDirs, hasAnyPost
|
||||
}
|
||||
|
||||
func (i *importCmd) 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 := i.retrieveJekyllPostDir(fs, subDir); isPostDir {
|
||||
return isPostDir, hasAnyPost
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (i *importCmd) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool, force 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 && !force {
|
||||
return errors.New("target path \"" + targetDir + "\" exists and is not empty")
|
||||
}
|
||||
}
|
||||
|
||||
jekyllConfig := i.loadJekyllConfig(fs, jekyllRoot)
|
||||
|
||||
mkdir(targetDir, "layouts")
|
||||
mkdir(targetDir, "content")
|
||||
mkdir(targetDir, "archetypes")
|
||||
mkdir(targetDir, "static")
|
||||
mkdir(targetDir, "data")
|
||||
mkdir(targetDir, "themes")
|
||||
|
||||
i.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
|
||||
|
||||
i.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *importCmd) 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 {
|
||||
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 := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 your site from others."
|
||||
cmd.Long = `Import your site from other web site generators like Jekyll.
|
||||
|
||||
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
|
||||
|
||||
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) {
|
||||
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
|
||||
title := "My New Hugo Site"
|
||||
baseURL := "http://example.org/"
|
||||
|
||||
@@ -138,115 +293,132 @@ func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind
|
||||
return err
|
||||
}
|
||||
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "hugo."+string(kind)), &buf, fs)
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "config."+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
|
||||
func (i *importCmd) 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 := ioutil.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 {
|
||||
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 = hugio.CopyFile(fs, sfp, dfp)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return postDirs, hasAnyPost
|
||||
return nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
func 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")
|
||||
}
|
||||
|
||||
jekyllConfig := c.loadJekyllConfig(fs, jekyllRoot)
|
||||
postDate, err := time.Parse("2006-1-2", r[0][1])
|
||||
if err != nil {
|
||||
return htime.Now(), "", err
|
||||
}
|
||||
|
||||
mkdir(targetDir, "layouts")
|
||||
mkdir(targetDir, "content")
|
||||
mkdir(targetDir, "archetypes")
|
||||
mkdir(targetDir, "static")
|
||||
mkdir(targetDir, "data")
|
||||
mkdir(targetDir, "themes")
|
||||
postName := r[0][2]
|
||||
|
||||
c.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
|
||||
return postDate, postName, nil
|
||||
}
|
||||
|
||||
c.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
|
||||
func convertJekyllPost(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
|
||||
}
|
||||
|
||||
pf, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(contentBytes))
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Parse file error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
newmetadata, err := convertJekyllMetaData(pf.FrontMatter, postName, postDate, draft)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Convert metadata error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := convertJekyllContent(newmetadata, string(pf.Content))
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Converting Jekyll error:", path)
|
||||
return 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) 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) {
|
||||
func convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
|
||||
metadata, err := maps.ToStringMapE(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -298,235 +470,63 @@ func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate t
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft bool) error {
|
||||
log.Println("Converting", path)
|
||||
func convertJekyllContent(m any, content string) (string, error) {
|
||||
metadata, _ := maps.ToStringMapE(m)
|
||||
|
||||
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
|
||||
lines := strings.Split(content, "\n")
|
||||
var resultLines []string
|
||||
for _, line := range lines {
|
||||
resultLines = append(resultLines, strings.Trim(line, "\r\n"))
|
||||
}
|
||||
|
||||
log.Println(filename, postDate, postName)
|
||||
content = strings.Join(resultLines, "\n")
|
||||
|
||||
targetFile := filepath.Join(targetDir, relPath)
|
||||
targetParentDir := filepath.Dir(targetFile)
|
||||
os.MkdirAll(targetParentDir, 0777)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
excerptSep := "<!--more-->"
|
||||
if value, ok := metadata["excerpt_separator"]; ok {
|
||||
if str, strOk := value.(string); strOk {
|
||||
content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1)
|
||||
}
|
||||
|
||||
}
|
||||
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])
|
||||
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 >}}"},
|
||||
}
|
||||
|
||||
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")
|
||||
for _, replace := range replaceList {
|
||||
content = replace.re.ReplaceAllString(content, replace.replace)
|
||||
}
|
||||
|
||||
fs := afero.NewOsFs()
|
||||
jekyllPostDirs, hasAnyPost := c.getJekyllDirInfo(fs, jekyllRoot)
|
||||
if !hasAnyPost {
|
||||
return errors.New("abort: jekyll root contains neither posts nor drafts")
|
||||
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},
|
||||
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), replaceHighlightTag},
|
||||
}
|
||||
|
||||
err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
|
||||
if err != nil {
|
||||
return newUserError(err)
|
||||
for _, replace := range replaceListFunc {
|
||||
content = replace.re.ReplaceAllStringFunc(content, replace.replace)
|
||||
}
|
||||
|
||||
c.r.Println("Importing...")
|
||||
|
||||
fileCount := 0
|
||||
callback := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
var buf bytes.Buffer
|
||||
if len(metadata) != 0 {
|
||||
err := parser.InterfaceToFrontMatter(m, metadecoders.YAML, &buf)
|
||||
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 c.convertJekyllPost(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
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
buf.WriteString(content)
|
||||
|
||||
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
|
||||
return buf.String(), 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 {
|
||||
func replaceHighlightTag(match string) string {
|
||||
r := regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`)
|
||||
parts := r.FindStringSubmatch(match)
|
||||
lastQuote := rune(0)
|
||||
@@ -570,55 +570,35 @@ func (c *importCommand) replaceHighlightTag(match string) string {
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (c *importCommand) replaceImageTag(match string) string {
|
||||
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
|
||||
c.replaceOptionalPart(result, "class", parts[1])
|
||||
c.replaceOptionalPart(result, "src", parts[2])
|
||||
c.replaceOptionalPart(result, "width", parts[3])
|
||||
c.replaceOptionalPart(result, "height", parts[4])
|
||||
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 {
|
||||
c.replaceOptionalPart(result, "title", splits[0])
|
||||
replaceOptionalPart(result, "title", splits[0])
|
||||
} else if lenSplits == 3 {
|
||||
c.replaceOptionalPart(result, "title", splits[1])
|
||||
replaceOptionalPart(result, "title", splits[1])
|
||||
} else if lenSplits == 5 {
|
||||
c.replaceOptionalPart(result, "title", splits[1])
|
||||
c.replaceOptionalPart(result, "alt", splits[3])
|
||||
replaceOptionalPart(result, "title", splits[1])
|
||||
replaceOptionalPart(result, "alt", splits[3])
|
||||
}
|
||||
}
|
||||
result.WriteString(">}}")
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (c *importCommand) replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) {
|
||||
func 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
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// 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"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestParseJekyllFilename(t *testing.T) {
|
||||
c := qt.New(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)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(expectResult[i].postDate.Format("2006-01-02"), qt.Equals, postDate.Format("2006-01-02"))
|
||||
c.Assert(expectResult[i].postName, qt.Equals, postName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertJekyllMetadata(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
testDataList := []struct {
|
||||
metadata any
|
||||
postName string
|
||||
postDate time.Time
|
||||
draft bool
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
map[any]any{},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z"}`,
|
||||
},
|
||||
{
|
||||
map[any]any{},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true,
|
||||
`{"date":"2015-10-01T00:00:00Z","draft":true}`,
|
||||
},
|
||||
{
|
||||
map[any]any{"Permalink": "/permalink.html", "layout": "post"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
|
||||
},
|
||||
{
|
||||
map[any]any{"permalink": "/permalink.html"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
|
||||
},
|
||||
{
|
||||
map[any]any{"category": nil, "permalink": 123},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z"}`,
|
||||
},
|
||||
{
|
||||
map[any]any{"Excerpt_Separator": "sep"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","excerpt_separator":"sep"}`,
|
||||
},
|
||||
{
|
||||
map[any]any{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"Others":"Goods","categories":["book"],"date":"2015-10-01T12:13:11Z"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range testDataList {
|
||||
result, err := convertJekyllMetaData(data.metadata, data.postName, data.postDate, data.draft)
|
||||
c.Assert(err, qt.IsNil)
|
||||
jsonResult, err := json.Marshal(result)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(jsonResult), qt.Equals, data.expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertJekyllContent(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
testDataList := []struct {
|
||||
metadata any
|
||||
content string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
map[any]any{},
|
||||
"Test content\r\n<!-- more -->\npart2 content", "Test content\n<!--more-->\npart2 content",
|
||||
},
|
||||
{
|
||||
map[any]any{},
|
||||
"Test content\n<!-- More -->\npart2 content", "Test content\n<!--more-->\npart2 content",
|
||||
},
|
||||
{
|
||||
map[any]any{"excerpt_separator": "<!--sep-->"},
|
||||
"Test content\n<!--sep-->\npart2 content",
|
||||
"---\nexcerpt_separator: <!--sep-->\n---\nTest content\n<!--more-->\npart2 content",
|
||||
},
|
||||
{map[any]any{}, "{% raw %}text{% endraw %}", "text"},
|
||||
{map[any]any{}, "{%raw%} text2 {%endraw %}", "text2"},
|
||||
{
|
||||
map[any]any{},
|
||||
"{% highlight go %}\nvar s int\n{% endhighlight %}",
|
||||
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}",
|
||||
},
|
||||
{
|
||||
map[any]any{},
|
||||
"{% highlight go linenos hl_lines=\"1 2\" %}\nvar s string\nvar i int\n{% endhighlight %}",
|
||||
"{{< highlight go \"linenos=table,hl_lines=1 2\" >}}\nvar s string\nvar i int\n{{< / highlight >}}",
|
||||
},
|
||||
|
||||
// Octopress image tag
|
||||
{
|
||||
map[any]any{},
|
||||
"{% img http://placekitten.com/890/280 %}",
|
||||
"{{< figure src=\"http://placekitten.com/890/280\" >}}",
|
||||
},
|
||||
{
|
||||
map[any]any{},
|
||||
"{% img left http://placekitten.com/320/250 Place Kitten #2 %}",
|
||||
"{{< figure class=\"left\" src=\"http://placekitten.com/320/250\" title=\"Place Kitten #2\" >}}",
|
||||
},
|
||||
{
|
||||
map[any]any{},
|
||||
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #3' %}",
|
||||
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #3\" >}}",
|
||||
},
|
||||
{
|
||||
map[any]any{},
|
||||
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
|
||||
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
|
||||
},
|
||||
{
|
||||
map[any]any{},
|
||||
"{% img http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
|
||||
"{{< figure src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
|
||||
},
|
||||
{
|
||||
map[any]any{},
|
||||
"{% img right /placekitten/300/500 'Place Kitten #4' 'An image of a very cute kitten' %}",
|
||||
"{{< figure class=\"right\" src=\"/placekitten/300/500\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
|
||||
},
|
||||
{
|
||||
map[any]any{"category": "book", "layout": "post", "Date": "2015-10-01 12:13:11"},
|
||||
"somecontent",
|
||||
"---\nDate: \"2015-10-01 12:13:11\"\ncategory: book\nlayout: post\n---\nsomecontent",
|
||||
},
|
||||
}
|
||||
for _, data := range testDataList {
|
||||
result, err := convertJekyllContent(data.metadata, data.content)
|
||||
c.Assert(result, qt.Equals, data.expect)
|
||||
c.Assert(err, qt.IsNil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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 commands
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var _ cmder = (*limitCmd)(nil)
|
||||
|
||||
type limitCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newLimitCmd() *limitCmd {
|
||||
ccmd := &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)
|
||||
|
||||
if rLimit.Cur >= newRlimit {
|
||||
return nil
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Attempting to increase limit")
|
||||
rLimit.Cur = newRlimit
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
return &limitCmd{baseCmd: newBaseCmd(ccmd)}
|
||||
}
|
||||
|
||||
const newRlimit = 10240
|
||||
|
||||
func tweakLimit() {
|
||||
var rLimit syscall.Rlimit
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
jww.WARN.Println("Unable to get rlimit:", err)
|
||||
return
|
||||
}
|
||||
if rLimit.Cur < newRlimit {
|
||||
rLimit.Cur = newRlimit
|
||||
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
// This may not succeed, see https://github.com/golang/go/issues/30401
|
||||
jww.INFO.Println("Unable to increase number of open files limit:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,5 +11,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package resources contains Resource related types.
|
||||
package resources
|
||||
//go:build !darwin
|
||||
// +build !darwin
|
||||
|
||||
package commands
|
||||
|
||||
func tweakLimit() {
|
||||
// nothing to do
|
||||
}
|
||||
+173
-154
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,178 +14,197 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"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 {
|
||||
var _ cmder = (*listCmd)(nil)
|
||||
|
||||
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(),
|
||||
type listCmd struct {
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (lc *listCmd) buildSites(config map[string]any) (*hugolib.HugoSites, error) {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
for key, value := range config {
|
||||
c.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
list := func(cd *simplecobra.Commandeer, r *rootCommand, shouldInclude func(page.Page) bool, opts ...any) error {
|
||||
bcfg := hugolib.BuildCfg{SkipRender: true}
|
||||
cfg := config.New()
|
||||
for i := 0; i < len(opts); i += 2 {
|
||||
cfg.Set(opts[i].(string), opts[i+1])
|
||||
}
|
||||
h, err := r.Build(cd, bcfg, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
writer := csv.NewWriter(r.Out)
|
||||
defer writer.Flush()
|
||||
|
||||
writer.Write([]string{
|
||||
"path",
|
||||
"slug",
|
||||
"title",
|
||||
"date",
|
||||
"expiryDate",
|
||||
"publishDate",
|
||||
"draft",
|
||||
"permalink",
|
||||
})
|
||||
|
||||
for _, p := range h.Pages() {
|
||||
if shouldInclude(p) {
|
||||
record := createRecord(h.Conf.BaseConfig().WorkingDir, p)
|
||||
if err := writer.Write(record); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
return &listCommand{
|
||||
commands: []simplecobra.Commander{
|
||||
&simpleCommand{
|
||||
name: "drafts",
|
||||
short: "List all drafts",
|
||||
long: `List all of the drafts in your content directory.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
if !p.Draft() || p.File().IsZero() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
c, err := initializeConfig(true, true, false, &lc.hugoBuilderCommon, lc, cfgInit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
return list(cd, r, shouldInclude,
|
||||
"buildDrafts", true,
|
||||
"buildFuture", true,
|
||||
"buildExpired", true,
|
||||
)
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "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.`,
|
||||
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().IsZero() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
sites, err := hugolib.NewHugoSites(*c.DepsCfg)
|
||||
if err != nil {
|
||||
return nil, newSystemError("Error creating sites", err)
|
||||
}
|
||||
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return nil, newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newListCmd() *listCmd {
|
||||
cc := &listCmd{}
|
||||
|
||||
cmd := &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,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
&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 {
|
||||
sites, err := cc.buildSites(map[string]any{"buildDrafts": true})
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
for _, p := range sites.Pages() {
|
||||
if p.Draft() {
|
||||
jww.FEEDBACK.Println(strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)))
|
||||
}
|
||||
return list(cd, r, shouldInclude,
|
||||
"buildFuture", true,
|
||||
"buildDrafts", true,
|
||||
)
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "expired",
|
||||
short: "List all posts already expired",
|
||||
long: `List all of the posts in your content directory which has already expired.`,
|
||||
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().IsZero() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return list(cd, r, shouldInclude,
|
||||
"buildExpired", true,
|
||||
"buildDrafts", true,
|
||||
)
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "all",
|
||||
short: "List all posts",
|
||||
long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
return !p.File().IsZero()
|
||||
}
|
||||
return list(cd, r, shouldInclude, "buildDrafts", true, "buildFuture", true, "buildExpired", true)
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
&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 {
|
||||
sites, err := cc.buildSites(map[string]any{"buildFuture": true})
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type listCommand struct {
|
||||
commands []simplecobra.Commander
|
||||
}
|
||||
|
||||
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 = "Listing out various types of content"
|
||||
cmd.Long = `Listing out various types of 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
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
for _, p := range sites.Pages() {
|
||||
if resource.IsFuture(p) {
|
||||
err := writer.Write([]string{
|
||||
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
|
||||
p.PublishDate().Format(time.RFC3339),
|
||||
})
|
||||
if err != nil {
|
||||
return newSystemError("Error writing future posts to stdout", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&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 {
|
||||
sites, err := cc.buildSites(map[string]any{"buildExpired": true})
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
for _, p := range sites.Pages() {
|
||||
if resource.IsExpired(p) {
|
||||
err := writer.Write([]string{
|
||||
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
|
||||
p.ExpiryDate().Format(time.RFC3339),
|
||||
})
|
||||
if err != nil {
|
||||
return newSystemError("Error writing expired posts to stdout", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "all",
|
||||
Short: "List all posts",
|
||||
Long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sites, err := cc.buildSites(map[string]any{
|
||||
"buildExpired": true,
|
||||
"buildDrafts": true,
|
||||
"buildFuture": true,
|
||||
})
|
||||
if err != nil {
|
||||
return newSystemError("Error building sites", err)
|
||||
}
|
||||
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
writer.Write([]string{
|
||||
"path",
|
||||
"slug",
|
||||
"title",
|
||||
"date",
|
||||
"expiryDate",
|
||||
"publishDate",
|
||||
"draft",
|
||||
"permalink",
|
||||
})
|
||||
for _, p := range sites.Pages() {
|
||||
if !p.IsPage() {
|
||||
continue
|
||||
}
|
||||
err := writer.Write([]string{
|
||||
strings.TrimPrefix(p.File().Filename(), sites.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(),
|
||||
})
|
||||
if err != nil {
|
||||
return newSystemError("Error writing posts to stdout", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func captureStdout(f func() error) (string, error) {
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err := f()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func TestListAll(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
dir := createSimpleTestSite(t, testSiteConfig{})
|
||||
|
||||
hugoCmd := newCommandsBuilder().addAll().build()
|
||||
cmd := hugoCmd.getCommand()
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(dir)
|
||||
})
|
||||
|
||||
cmd.SetArgs([]string{"-s=" + dir, "list", "all"})
|
||||
|
||||
out, err := captureStdout(func() error {
|
||||
_, err := cmd.ExecuteC()
|
||||
return err
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
r := csv.NewReader(strings.NewReader(out))
|
||||
|
||||
header, err := r.Read()
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(header, qt.DeepEquals, []string{
|
||||
"path", "slug", "title",
|
||||
"date", "expiryDate", "publishDate",
|
||||
"draft", "permalink",
|
||||
})
|
||||
|
||||
record, err := r.Read()
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(record, qt.DeepEquals, []string{
|
||||
filepath.Join("content", "p1.md"), "", "P1",
|
||||
"0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z",
|
||||
"false", "https://example.org/p1/",
|
||||
})
|
||||
}
|
||||
+220
-257
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -14,18 +14,87 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/modules/npm"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const commonUsageMod = `
|
||||
var _ cmder = (*modCmd)(nil)
|
||||
|
||||
type modCmd struct {
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (c *modCmd) newVerifyCmd() *cobra.Command {
|
||||
var clean bool
|
||||
|
||||
verifyCmd := &cobra.Command{
|
||||
Use: "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.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withModsClient(true, func(c *modules.Client) error {
|
||||
return c.Verify(clean)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
verifyCmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
|
||||
|
||||
return verifyCmd
|
||||
}
|
||||
|
||||
var moduleNotFoundRe = regexp.MustCompile("module.*not found")
|
||||
|
||||
func (c *modCmd) newCleanCmd() *cobra.Command {
|
||||
var pattern string
|
||||
var all bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "clean",
|
||||
Short: "Delete the Hugo Module cache for the current project.",
|
||||
Long: `Delete the Hugo Module cache for the current project.
|
||||
|
||||
Note that after you run this command, all of your dependencies will be re-downloaded next time you run "hugo".
|
||||
|
||||
Also note that if you configure a positive maxAge for the "modules" file cache, it will also be cleaned as part of "hugo --gc".
|
||||
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if all {
|
||||
com, err := c.initConfig(false)
|
||||
|
||||
if err != nil && com == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count, err := com.hugo().FileCaches.ModulesCache().Prune(true)
|
||||
com.logger.Printf("Deleted %d files from module cache.", count)
|
||||
return err
|
||||
}
|
||||
return c.withModsClient(true, func(c *modules.Client) error {
|
||||
return c.Clean(pattern)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`)
|
||||
cmd.Flags().BoolVarP(&all, "all", "", false, "clean entire module cache")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newModCmd() *modCmd {
|
||||
c := &modCmd{}
|
||||
|
||||
const commonUsage = `
|
||||
Note that Hugo will always start out by resolving the components defined in the site
|
||||
configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided),
|
||||
Go Modules, or a folder inside the themes directory, in that order.
|
||||
@@ -34,171 +103,27 @@ 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
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "mod",
|
||||
Short: "Various Hugo Modules helpers.",
|
||||
Long: `Various helpers to help manage the modules in your project's dependency graph.
|
||||
|
||||
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.
|
||||
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".
|
||||
|
||||
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.
|
||||
` + commonUsage,
|
||||
|
||||
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) {
|
||||
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.SourceFs, h.BaseFs.Assets.Dirs)
|
||||
},
|
||||
},
|
||||
},
|
||||
RunE: nil,
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
var initPath string
|
||||
if len(args) >= 1 {
|
||||
initPath = args[0]
|
||||
}
|
||||
return h.Configs.ModulesClient.Init(initPath)
|
||||
},
|
||||
},
|
||||
&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) {
|
||||
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(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) {
|
||||
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(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) {
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`)
|
||||
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
|
||||
}
|
||||
cmd.AddCommand(newModNPMCmd(c))
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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: `
|
||||
cmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "get",
|
||||
DisableFlagParsing: true,
|
||||
Short: "Resolves dependencies in your current Hugo Project.",
|
||||
Long: `
|
||||
Resolves dependencies in your current Hugo Project.
|
||||
|
||||
Some examples:
|
||||
@@ -211,120 +136,158 @@ 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):
|
||||
Install the latest versions of all module dependencies:
|
||||
|
||||
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
|
||||
},
|
||||
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
|
||||
` + commonUsage,
|
||||
RunE: func(cmd *cobra.Command, 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" {
|
||||
return cmd.Help()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var lastArg string
|
||||
if len(args) != 0 {
|
||||
lastArg = args[len(args)-1]
|
||||
// Sanity check. 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")
|
||||
}
|
||||
|
||||
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)
|
||||
r.Println("Update module in", dir)
|
||||
cfg := config.New()
|
||||
cfg.Set("workingDir", dir)
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := conf.configs.ModulesClient
|
||||
return client.Get(args...)
|
||||
|
||||
}
|
||||
filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
} else {
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := conf.configs.ModulesClient
|
||||
return client.Get(args...)
|
||||
}
|
||||
},
|
||||
|
||||
if info.Name() == "go.mod" {
|
||||
// Found a module.
|
||||
dir := filepath.Dir(path)
|
||||
fmt.Println("Update module in", dir)
|
||||
c.source = dir
|
||||
err := c.withModsClient(false, func(c *modules.Client) error {
|
||||
if len(args) == 1 && args[0] == "-h" {
|
||||
return cmd.Help()
|
||||
}
|
||||
return c.Get(args...)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.withModsClient(false, func(c *modules.Client) error {
|
||||
return c.Get(args...)
|
||||
})
|
||||
},
|
||||
npmCommand,
|
||||
},
|
||||
}
|
||||
&cobra.Command{
|
||||
Use: "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.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withModsClient(true, func(c *modules.Client) error {
|
||||
return c.Graph(os.Stdout)
|
||||
})
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "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.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var path string
|
||||
if len(args) >= 1 {
|
||||
path = args[0]
|
||||
}
|
||||
return c.withModsClient(false, func(c *modules.Client) error {
|
||||
return c.Init(path)
|
||||
})
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "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.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withModsClient(true, func(c *modules.Client) error {
|
||||
return c.Vendor()
|
||||
})
|
||||
},
|
||||
},
|
||||
c.newVerifyCmd(),
|
||||
&cobra.Command{
|
||||
Use: "tidy",
|
||||
Short: "Remove unused entries in go.mod and go.sum.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withModsClient(true, func(c *modules.Client) error {
|
||||
return c.Tidy()
|
||||
})
|
||||
},
|
||||
},
|
||||
c.newCleanCmd(),
|
||||
)
|
||||
|
||||
c.baseBuilderCmd = b.newBuilderCmd(cmd)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
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(c.r.configVersionID.Load(), nil)
|
||||
func (c *modCmd) withModsClient(failOnMissingConfig bool, f func(*modules.Client) error) error {
|
||||
com, err := c.initConfig(failOnMissingConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// config := conf.configs.Base
|
||||
|
||||
return nil
|
||||
return f(com.hugo().ModulesClient)
|
||||
}
|
||||
|
||||
func (c *modCommands) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Various Hugo Modules helpers."
|
||||
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".
|
||||
func (c *modCmd) withHugo(f func(*hugolib.HugoSites) error) error {
|
||||
com, err := c.initConfig(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
` + commonUsageMod
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
return f(com.hugo())
|
||||
}
|
||||
|
||||
func (c *modCommands) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
c.r = cd.Root.Command.(*rootCommand)
|
||||
return nil
|
||||
func (c *modCmd) initConfig(failOnNoConfig bool) (*commandeer, error) {
|
||||
com, err := initializeConfig(failOnNoConfig, false, false, &c.hugoBuilderCommon, c, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return com, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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 commands
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/modules/npm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newModNPMCmd(c *modCmd) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "npm",
|
||||
Short: "Various npm helpers.",
|
||||
Long: `Various npm (Node package manager) helpers.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withHugo(func(h *hugolib.HugoSites) error {
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(&cobra.Command{
|
||||
Use: "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.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return c.withHugo(func(h *hugolib.HugoSites) error {
|
||||
return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return cmd
|
||||
}
|
||||
+82
-165
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,33 +15,32 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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/hugolib"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
func newNewCommand() *newCommand {
|
||||
var (
|
||||
force bool
|
||||
contentType string
|
||||
format string
|
||||
)
|
||||
var _ cmder = (*newCmd)(nil)
|
||||
|
||||
var c *newCommand
|
||||
c = &newCommand{
|
||||
commands: []simplecobra.Commander{
|
||||
&simpleCommand{
|
||||
name: "content",
|
||||
use: "content [path]",
|
||||
short: "Create new content for your site",
|
||||
long: `Create a new content file and automatically set the date and title.
|
||||
type newCmd struct {
|
||||
contentEditor string
|
||||
contentType string
|
||||
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newNewCmd() *newCmd {
|
||||
cmd := &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`" + `.
|
||||
@@ -49,161 +48,79 @@ 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.`,
|
||||
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.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create")
|
||||
cmd.Flags().String("editor", "", "edit new content with this editor, if provided")
|
||||
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 (skeleton)",
|
||||
long: `Create a new site in the provided directory.
|
||||
The new site will have the correct structure, but no content or theme yet.
|
||||
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
createpath, err := filepath.Abs(filepath.Clean(args[0]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := config.New()
|
||||
cfg.Set("workingDir", createpath)
|
||||
cfg.Set("publishDir", "public")
|
||||
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceFs := conf.fs.Source
|
||||
|
||||
err = skeletons.CreateSite(createpath, sourceFs, force, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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.Flags().BoolVarP(&force, "force", "f", false, "init inside non-empty directory")
|
||||
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "theme",
|
||||
use: "theme [name]",
|
||||
short: "Create a new theme (skeleton)",
|
||||
long: `Create a new theme (skeleton) called [name] in ./themes.
|
||||
New theme is a skeleton. Please add content to the touched files. Add your
|
||||
name to the copyright line in the license and adjust the theme.toml file
|
||||
according to your needs.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return newUserError("theme name needs to be provided")
|
||||
}
|
||||
cfg := config.New()
|
||||
cfg.Set("publishDir", "public")
|
||||
|
||||
conf, err := r.ConfigFromProvider(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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c
|
||||
cc := &newCmd{baseBuilderCmd: b.newBuilderCmd(cmd)}
|
||||
|
||||
cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create")
|
||||
cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
|
||||
|
||||
cmd.AddCommand(b.newNewSiteCmd().getCommand())
|
||||
cmd.AddCommand(b.newNewThemeCmd().getCommand())
|
||||
|
||||
cmd.RunE = cc.newContent
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
type newCommand struct {
|
||||
rootCmd *rootCommand
|
||||
func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
if cmd.Flags().Changed("editor") {
|
||||
c.Set("newContentEditor", n.contentEditor)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
commands []simplecobra.Commander
|
||||
c, err := initializeConfig(true, true, false, &n.hugoBuilderCommon, n, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
|
||||
return create.NewContent(c.hugo(), n.contentType, args[0])
|
||||
}
|
||||
|
||||
func (c *newCommand) Commands() []simplecobra.Commander {
|
||||
return c.commands
|
||||
func mkdir(x ...string) {
|
||||
p := filepath.Join(x...)
|
||||
|
||||
err := os.MkdirAll(p, 0777) // before umask
|
||||
if err != nil {
|
||||
jww.FATAL.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *newCommand) Name() string {
|
||||
return "new"
|
||||
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 (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 for your site"
|
||||
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)
|
||||
var nextStepsText bytes.Buffer
|
||||
|
||||
nextStepsText.WriteString(`Just a few more steps...
|
||||
|
||||
1. Change the current directory to ` + path + `.
|
||||
2. Create or install a theme:
|
||||
- Create a new theme with the command "hugo new theme <THEMENAME>"
|
||||
- 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 `)
|
||||
|
||||
nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>"))
|
||||
|
||||
nextStepsText.WriteString(`".
|
||||
5. Start the embedded web server with the command "hugo server --buildDrafts".
|
||||
|
||||
See documentation at https://gohugo.io/.`)
|
||||
|
||||
return nextStepsText.String()
|
||||
func newContentPathSection(h *hugolib.HugoSites, path string) (string, string) {
|
||||
// Forward slashes is used in all examples. Convert if needed.
|
||||
// Issue #1133
|
||||
createpath := filepath.FromSlash(path)
|
||||
|
||||
if h != nil {
|
||||
for _, dir := range h.BaseFs.Content.Dirs {
|
||||
createpath = strings.TrimPrefix(createpath, dir.Meta().Filename)
|
||||
}
|
||||
}
|
||||
|
||||
var section string
|
||||
// assume the first directory is the section (kind)
|
||||
if strings.Contains(createpath[1:], helpers.FilePathSeparator) {
|
||||
parts := strings.Split(strings.TrimPrefix(createpath, helpers.FilePathSeparator), helpers.FilePathSeparator)
|
||||
if len(parts) > 0 {
|
||||
section = parts[0]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return createpath, section
|
||||
}
|
||||
|
||||
+8
-23
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,34 +11,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sass
|
||||
package commands
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestIsUnquotedCSSValue(t *testing.T) {
|
||||
// Issue #1133
|
||||
func TestNewContentPathSectionWithForwardSlashes(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
in any
|
||||
out bool
|
||||
}{
|
||||
{"24px", true},
|
||||
{"1.5rem", true},
|
||||
{"10%", true},
|
||||
{"hsl(0, 0%, 100%)", true},
|
||||
{"calc(24px + 36px)", true},
|
||||
{"24xxx", true}, // a false positive.
|
||||
{123, true},
|
||||
{123.12, true},
|
||||
{"#fff", true},
|
||||
{"#ffffff", true},
|
||||
{"#ffffffff", false},
|
||||
} {
|
||||
c.Assert(isTypedCSSValue(test.in), qt.Equals, test.out)
|
||||
}
|
||||
|
||||
p, s := newContentPathSection(nil, "/post/new.md")
|
||||
c.Assert(p, qt.Equals, filepath.FromSlash("/post/new.md"))
|
||||
c.Assert(s, qt.Equals, "post")
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// 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 commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
"github.com/gohugoio/hugo/create"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var _ cmder = (*newSiteCmd)(nil)
|
||||
|
||||
type newSiteCmd struct {
|
||||
configFormat string
|
||||
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newNewSiteCmd() *newSiteCmd {
|
||||
cc := &newSiteCmd{}
|
||||
|
||||
cmd := &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: cc.newSite,
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&cc.configFormat, "format", "f", "toml", "config file format")
|
||||
cmd.Flags().Bool("force", false, "init inside non-empty directory")
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
func (n *newSiteCmd) 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"),
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
isEmpty, _ := helpers.IsEmpty(basepath, fs.Source)
|
||||
|
||||
switch {
|
||||
case !isEmpty && !force:
|
||||
return errors.New(basepath + " already exists and is not empty. See --force.")
|
||||
|
||||
case !isEmpty && force:
|
||||
all := append(dirs, filepath.Join(basepath, "config."+n.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: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
createConfig(fs, basepath, n.configFormat)
|
||||
|
||||
// Create a default archetype file.
|
||||
helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"),
|
||||
strings.NewReader(create.DefaultArchetypeTemplateTemplate), fs.Source)
|
||||
|
||||
jww.FEEDBACK.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", basepath)
|
||||
jww.FEEDBACK.Println(nextStepsText())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newSite creates a new Hugo site and initializes a structured Hugo directory.
|
||||
func (n *newSiteCmd) 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")
|
||||
cfg := config.New()
|
||||
cfg.Set("workingDir", createpath)
|
||||
cfg.Set("publishDir", "public")
|
||||
return n.doNewSite(hugofs.NewDefault(cfg), createpath, forceNew)
|
||||
}
|
||||
|
||||
func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
|
||||
in := map[string]string{
|
||||
"baseURL": "http://example.org/",
|
||||
"title": "My New Hugo Site",
|
||||
"languageCode": "en-us",
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source)
|
||||
}
|
||||
|
||||
func nextStepsText() string {
|
||||
var nextStepsText bytes.Buffer
|
||||
|
||||
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
|
||||
|
||||
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(`".
|
||||
3. Start the built-in live server via "hugo server".
|
||||
|
||||
Visit https://gohugo.io/ for quickstart guide and full documentation.`)
|
||||
|
||||
return nextStepsText.String()
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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 commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var _ cmder = (*newThemeCmd)(nil)
|
||||
|
||||
type newThemeCmd struct {
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newNewThemeCmd() *newThemeCmd {
|
||||
cc := &newThemeCmd{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "theme [name]",
|
||||
Short: "Create a new theme",
|
||||
Long: `Create a new theme (skeleton) called [name] in ./themes.
|
||||
New theme is a skeleton. Please add content to the touched files. Add your
|
||||
name to the copyright line in the license and adjust the theme.toml file
|
||||
as you see fit.`,
|
||||
RunE: cc.newTheme,
|
||||
}
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
// newTheme creates a new Hugo theme template
|
||||
func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
|
||||
c, err := initializeConfig(false, false, false, &n.hugoBuilderCommon, n, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
return newUserError("theme name needs to be provided")
|
||||
}
|
||||
|
||||
createpath := c.hugo().PathSpec.AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0]))
|
||||
jww.FEEDBACK.Println("Creating theme at", createpath)
|
||||
|
||||
cfg := c.DepsCfg
|
||||
|
||||
if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x {
|
||||
return errors.New(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")
|
||||
|
||||
baseofDefault := []byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
{{- partial "head.html" . -}}
|
||||
<body>
|
||||
{{- partial "header.html" . -}}
|
||||
<div id="content">
|
||||
{{- block "main" . }}{{- end }}
|
||||
</div>
|
||||
{{- partial "footer.html" . -}}
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), cfg.Fs.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "head.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) ` + htime.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"), bytes.NewReader(by), cfg.Fs.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.createThemeMD(cfg.Fs, createpath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *newThemeCmd) 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"
|
||||
description = ""
|
||||
homepage = "http://example.com/"
|
||||
tags = []
|
||||
features = []
|
||||
min_version = "0.41.0"
|
||||
|
||||
[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
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.
|
||||
|
||||
//go:build nodeploy
|
||||
// +build nodeploy
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var _ cmder = (*deployCmd)(nil)
|
||||
|
||||
// deployCmd supports deploying sites to Cloud providers.
|
||||
type deployCmd struct {
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newDeployCmd() *deployCmd {
|
||||
cc := &deployCmd{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "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.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return errors.New("build without HUGO_BUILD_TAGS=nodeploy to use this command")
|
||||
},
|
||||
}
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
|
||||
|
||||
return cc
|
||||
}
|
||||
+46
-26
@@ -1,4 +1,7 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
//go:build release
|
||||
// +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,39 +17,56 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"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 _ cmder = (*releaseCommandeer)(nil)
|
||||
|
||||
var (
|
||||
step int
|
||||
skipPush bool
|
||||
try bool
|
||||
)
|
||||
type releaseCommandeer struct {
|
||||
cmd *cobra.Command
|
||||
|
||||
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
|
||||
}
|
||||
version string
|
||||
|
||||
return rel.Run()
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Hidden = true
|
||||
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)")
|
||||
skipPublish bool
|
||||
try bool
|
||||
}
|
||||
|
||||
func createReleaser() cmder {
|
||||
// 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().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 (c *releaseCommandeer) getCommand() *cobra.Command {
|
||||
return c.cmd
|
||||
}
|
||||
|
||||
func (c *releaseCommandeer) flagsToConfig(cfg config.Provider) {
|
||||
}
|
||||
|
||||
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.skipPublish, r.try).Run()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
//go:build !release
|
||||
// +build !release
|
||||
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,10 +14,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package css
|
||||
package commands
|
||||
|
||||
// QuotedString is a string that needs to be quoted in CSS.
|
||||
type QuotedString string
|
||||
|
||||
// UnquotedString is a string that does not need to be quoted in CSS.
|
||||
type UnquotedString string
|
||||
func createReleaser() cmder {
|
||||
return &nilCommand{}
|
||||
}
|
||||
+435
-914
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
// 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 commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/gohugoio/hugo/transform"
|
||||
"github.com/gohugoio/hugo/transform/livereloadinject"
|
||||
)
|
||||
|
||||
func injectLiveReloadScript(src io.Reader, baseURL url.URL) string {
|
||||
var b bytes.Buffer
|
||||
chain := transform.Chain{livereloadinject.New(baseURL)}
|
||||
chain.Apply(&b, src)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
// Issue 9518
|
||||
func TestServerPanicOnConfigError(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
config := `
|
||||
[markup]
|
||||
[markup.highlight]
|
||||
linenos='table'
|
||||
`
|
||||
|
||||
r := runServerTest(c, 0, config)
|
||||
|
||||
c.Assert(r.err, qt.IsNotNil)
|
||||
c.Assert(r.err.Error(), qt.Contains, "cannot parse 'Highlight.LineNos' as bool:")
|
||||
}
|
||||
|
||||
func TestServerFlags(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
assertPublic := func(c *qt.C, r serverTestResult, renderStaticToDisk bool) {
|
||||
c.Assert(r.err, qt.IsNil)
|
||||
c.Assert(r.homesContent[0], qt.Contains, "Environment: development")
|
||||
c.Assert(r.publicDirnames["myfile.txt"], qt.Equals, renderStaticToDisk)
|
||||
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
flag string
|
||||
assert func(c *qt.C, r serverTestResult)
|
||||
}{
|
||||
{"", func(c *qt.C, r serverTestResult) {
|
||||
assertPublic(c, r, false)
|
||||
}},
|
||||
{"--renderToDisk", func(c *qt.C, r serverTestResult) {
|
||||
assertPublic(c, r, true)
|
||||
}},
|
||||
{"--renderStaticToDisk", func(c *qt.C, r serverTestResult) {
|
||||
assertPublic(c, r, true)
|
||||
}},
|
||||
} {
|
||||
c.Run(test.flag, func(c *qt.C) {
|
||||
config := `
|
||||
baseURL="https://example.org"
|
||||
`
|
||||
|
||||
var args []string
|
||||
if test.flag != "" {
|
||||
args = strings.Split(test.flag, "=")
|
||||
}
|
||||
|
||||
r := runServerTest(c, 1, config, args...)
|
||||
|
||||
test.assert(c, r)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestServerBugs(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
config string
|
||||
flag string
|
||||
numservers int
|
||||
assert func(c *qt.C, r serverTestResult)
|
||||
}{
|
||||
// Issue 9788
|
||||
{"PostProcess, memory", "", "", 1, func(c *qt.C, r serverTestResult) {
|
||||
c.Assert(r.err, qt.IsNil)
|
||||
c.Assert(r.homesContent[0], qt.Contains, "PostProcess: /foo.min.css")
|
||||
}},
|
||||
{"PostProcess, disk", "", "--renderToDisk", 1, func(c *qt.C, r serverTestResult) {
|
||||
c.Assert(r.err, qt.IsNil)
|
||||
c.Assert(r.homesContent[0], qt.Contains, "PostProcess: /foo.min.css")
|
||||
}},
|
||||
// Isue 9901
|
||||
{"Multihost", `
|
||||
defaultContentLanguage = 'en'
|
||||
[languages]
|
||||
[languages.en]
|
||||
baseURL = 'https://example.com'
|
||||
title = 'My blog'
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
baseURL = 'https://example.fr'
|
||||
title = 'Mon blogue'
|
||||
weight = 2
|
||||
`, "", 2, func(c *qt.C, r serverTestResult) {
|
||||
c.Assert(r.err, qt.IsNil)
|
||||
for i, s := range []string{"My blog", "Mon blogue"} {
|
||||
c.Assert(r.homesContent[i], qt.Contains, s)
|
||||
}
|
||||
}},
|
||||
} {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
if test.config == "" {
|
||||
test.config = `
|
||||
baseURL="https://example.org"
|
||||
`
|
||||
}
|
||||
|
||||
var args []string
|
||||
if test.flag != "" {
|
||||
args = strings.Split(test.flag, "=")
|
||||
}
|
||||
r := runServerTest(c, test.numservers, test.config, args...)
|
||||
test.assert(c, r)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type serverTestResult struct {
|
||||
err error
|
||||
homesContent []string
|
||||
publicDirnames map[string]bool
|
||||
}
|
||||
|
||||
func runServerTest(c *qt.C, getNumHomes int, config string, args ...string) (result serverTestResult) {
|
||||
dir := createSimpleTestSite(c, testSiteConfig{configTOML: config})
|
||||
|
||||
sp, err := helpers.FindAvailablePort()
|
||||
c.Assert(err, qt.IsNil)
|
||||
port := sp.Port
|
||||
|
||||
defer func() {
|
||||
os.RemoveAll(dir)
|
||||
}()
|
||||
|
||||
stop := make(chan bool)
|
||||
|
||||
b := newCommandsBuilder()
|
||||
scmd := b.newServerCmdSignaled(stop)
|
||||
|
||||
cmd := scmd.getCommand()
|
||||
args = append([]string{"-s=" + dir, fmt.Sprintf("-p=%d", port)}, args...)
|
||||
cmd.SetArgs(args)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
wg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
wg.Go(func() error {
|
||||
_, err := cmd.ExecuteC()
|
||||
return err
|
||||
})
|
||||
|
||||
if getNumHomes > 0 {
|
||||
// Esp. on slow CI machines, we need to wait a little before the web
|
||||
// server is ready.
|
||||
time.Sleep(567 * time.Millisecond)
|
||||
result.homesContent = make([]string, getNumHomes)
|
||||
for i := 0; i < getNumHomes; i++ {
|
||||
func() {
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port+i))
|
||||
c.Check(err, qt.IsNil)
|
||||
c.Check(resp.StatusCode, qt.Equals, http.StatusOK)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
result.homesContent[i] = helpers.ReaderToString(resp.Body)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
case stop <- true:
|
||||
}
|
||||
|
||||
pubFiles, err := os.ReadDir(filepath.Join(dir, "public"))
|
||||
c.Check(err, qt.IsNil)
|
||||
result.publicDirnames = make(map[string]bool)
|
||||
for _, f := range pubFiles {
|
||||
result.publicDirnames[f.Name()] = true
|
||||
}
|
||||
|
||||
result.err = wg.Wait()
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
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 _, test := range tests {
|
||||
t.Run(test.TestName, func(t *testing.T) {
|
||||
b := newCommandsBuilder()
|
||||
s := b.newServerCmd()
|
||||
v := config.NewWithTestDefaults()
|
||||
baseURL := test.CLIBaseURL
|
||||
v.Set("baseURL", test.CfgBaseURL)
|
||||
s.serverAppend = test.AppendPort
|
||||
s.serverPort = test.Port
|
||||
result, err := s.fixURL(v, baseURL, s.serverPort)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %s", err)
|
||||
}
|
||||
if result != test.Result {
|
||||
t.Errorf("Expected %q, got %q", test.Result, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveErrorPrefixFromLog(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
content := `ERROR 2018/10/07 13:11:12 Error while rendering "home": template: _default/baseof.html:4:3: executing "main" at <partial "logo" .>: error calling partial: template: partials/logo.html:5:84: executing "partials/logo.html" at <$resized.AHeight>: can't evaluate field AHeight in type *resource.Image
|
||||
ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s)
|
||||
`
|
||||
|
||||
withoutError := removeErrorPrefixFromLog(content)
|
||||
|
||||
c.Assert(strings.Contains(withoutError, "ERROR"), qt.Equals, false)
|
||||
}
|
||||
|
||||
func isWindowsCI() bool {
|
||||
return runtime.GOOS == "windows" && os.Getenv("CI") != ""
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// 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.
|
||||
// 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"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib/filesystems"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/fsync"
|
||||
)
|
||||
|
||||
type staticSyncer struct {
|
||||
c *commandeer
|
||||
}
|
||||
|
||||
func newStaticSyncer(c *commandeer) (*staticSyncer, error) {
|
||||
return &staticSyncer{c: c}, nil
|
||||
}
|
||||
|
||||
func (s *staticSyncer) isStatic(filename string) bool {
|
||||
return s.c.hugo().BaseFs.SourceFilesystems.IsStatic(filename)
|
||||
}
|
||||
|
||||
func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
c := s.c
|
||||
|
||||
syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
|
||||
publishDir := helpers.FilePathSeparator
|
||||
|
||||
if sourceFs.PublishFolder != "" {
|
||||
publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
|
||||
}
|
||||
|
||||
syncer := fsync.NewSyncer()
|
||||
syncer.NoTimes = c.Cfg.GetBool("noTimes")
|
||||
syncer.NoChmod = c.Cfg.GetBool("noChmod")
|
||||
syncer.ChmodFilter = chmodFilter
|
||||
syncer.SrcFs = sourceFs.Fs
|
||||
syncer.DestFs = c.Fs.PublishDir
|
||||
if c.renderStaticToDisk {
|
||||
syncer.DestFs = c.Fs.PublishDirStatic
|
||||
}
|
||||
|
||||
// prevent spamming the log on changes
|
||||
logger := helpers.NewDistinctErrorLogger()
|
||||
|
||||
for _, ev := range staticEvents {
|
||||
// Due to our approach of layering both directories and the content's rendered output
|
||||
// into one we can't accurately remove a file not in one of the source directories.
|
||||
// If a file is in the local static dir and also in the theme static dir and we remove
|
||||
// it from one of those locations we expect it to still exist in the destination
|
||||
//
|
||||
// If Hugo generates a file (from the content dir) over a static file
|
||||
// the content generated file should take precedence.
|
||||
//
|
||||
// Because we are now watching and handling individual events it is possible that a static
|
||||
// event that occupies the same path as a content generated file will take precedence
|
||||
// until a regeneration of the content takes places.
|
||||
//
|
||||
// Hugo assumes that these cases are very rare and will permit this bad behavior
|
||||
// The alternative is to track every single file and which pipeline rendered it
|
||||
// and then to handle conflict resolution on every event.
|
||||
|
||||
fromPath := ev.Name
|
||||
|
||||
relPath, found := sourceFs.MakePathRelative(fromPath)
|
||||
|
||||
if !found {
|
||||
// Not member of this virtual host.
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove || rename is harder and will require an assumption.
|
||||
// Hugo takes the following approach:
|
||||
// If the static file exists in any of the static source directories after this event
|
||||
// Hugo will re-sync it.
|
||||
// If it does not exist in all of the static directories Hugo will remove it.
|
||||
//
|
||||
// This assumes that Hugo has not generated content on top of a static file and then removed
|
||||
// the source of that static file. In this case Hugo will incorrectly remove that file
|
||||
// from the published directory.
|
||||
if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
|
||||
if _, err := sourceFs.Fs.Stat(relPath); os.IsNotExist(err) {
|
||||
// If file doesn't exist in any static dir, remove it
|
||||
logger.Println("File no longer exists in static dir, removing", relPath)
|
||||
_ = c.Fs.PublishDirStatic.RemoveAll(relPath)
|
||||
|
||||
} else if err == nil {
|
||||
// If file still exists, sync it
|
||||
logger.Println("Syncing", relPath, "to", publishDir)
|
||||
|
||||
if err := syncer.Sync(relPath, relPath); err != nil {
|
||||
c.logger.Errorln(err)
|
||||
}
|
||||
} else {
|
||||
c.logger.Errorln(err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// For all other event operations Hugo will sync static.
|
||||
logger.Println("Syncing", relPath, "to", publishDir)
|
||||
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
|
||||
c.logger.Errorln(err)
|
||||
}
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
_, err := c.doWithPublishDirs(syncFn)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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/gohugoio/hugo/common/hugo"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var _ cmder = (*versionCmd)(nil)
|
||||
|
||||
type versionCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newVersionCmd() *versionCmd {
|
||||
return &versionCmd{
|
||||
newBaseCmd(&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() {
|
||||
jww.FEEDBACK.Println(hugo.BuildVersionString())
|
||||
}
|
||||
@@ -22,64 +22,29 @@ import (
|
||||
// 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
|
||||
}
|
||||
|
||||
fromt := reflect.TypeOf(from[0]).Elem()
|
||||
|
||||
// If we get []string []string, we append the from slice to to
|
||||
if tot == fromt {
|
||||
return reflect.AppendSlice(tov, fromv).Interface(), nil
|
||||
@@ -87,7 +52,6 @@ func Append(to any, from ...any) (any, error) {
|
||||
// Fall back to a []interface{} slice.
|
||||
return appendToInterfaceSliceFromValues(tov, fromv)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +62,7 @@ func Append(to any, from ...any) (any, error) {
|
||||
|
||||
for _, f := range from {
|
||||
fv := reflect.ValueOf(f)
|
||||
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
|
||||
if !fv.Type().AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
tov, _ := indirect(reflect.ValueOf(to))
|
||||
return appendToInterfaceSlice(tov, from...)
|
||||
@@ -113,10 +77,6 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
|
||||
var tos []any
|
||||
|
||||
for _, slice := range []reflect.Value{slice1, slice2} {
|
||||
if !slice.IsValid() {
|
||||
tos = append(tos, nil)
|
||||
continue
|
||||
}
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
tos = append(tos, slice.Index(i).Interface())
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestAppend(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
for i, test := range []struct {
|
||||
for _, test := range []struct {
|
||||
start any
|
||||
addend []any
|
||||
expected any
|
||||
@@ -74,9 +74,6 @@ func TestAppend(t *testing.T) {
|
||||
[]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"}},
|
||||
} {
|
||||
|
||||
result, err := Append(test.start, test.addend...)
|
||||
@@ -88,59 +85,6 @@ func TestAppend(t *testing.T) {
|
||||
}
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(result, qt.DeepEquals, test.expected, qt.Commentf("test: [%d] %v", i, test))
|
||||
c.Assert(result, qt.DeepEquals, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
// #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"})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ package collections
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Slicer defines a very generic way to create a typed slice. This is used
|
||||
@@ -75,22 +74,3 @@ func StringSliceToInterfaceSlice(ss []string) []any {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -122,18 +122,3 @@ func TestSlice(t *testing.T) {
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ package constants
|
||||
// Error IDs.
|
||||
// Do not change these values.
|
||||
const (
|
||||
ErrIDAmbigousDisableKindTaxonomy = "error-disable-taxonomy"
|
||||
ErrIDAmbigousOutputKindTaxonomy = "error-output-taxonomy"
|
||||
|
||||
// IDs for remote errors in tpl/data.
|
||||
ErrRemoteGetJSON = "error-remote-getjson"
|
||||
ErrRemoteGetCSV = "error-remote-getcsv"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package common provides common helper functionality for Hugo.
|
||||
package common
|
||||
@@ -16,6 +16,7 @@ package herrors
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -33,7 +34,7 @@ type LineMatcher struct {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// It returns the column number or 0 if the line was found, but column could not be determinde. Returns -1 if no line match.
|
||||
type LineMatcherFn func(m LineMatcher) int
|
||||
|
||||
// SimpleLineMatcher simply matches by line number.
|
||||
@@ -61,16 +62,6 @@ var OffsetMatcher = func(m LineMatcher) int {
|
||||
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 {
|
||||
@@ -123,7 +114,7 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
|
||||
|
||||
ectx := &ErrorContext{LinesPos: -1, Position: text.Position{Offset: -1}}
|
||||
|
||||
b, err := io.ReadAll(r)
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return ectx
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
@@ -39,8 +38,7 @@ type ErrorSender interface {
|
||||
|
||||
// Recover is a helper function that can be used to capture panics.
|
||||
// Put this at the top of a method/function that crashes in a template:
|
||||
//
|
||||
// defer herrors.Recover()
|
||||
// defer herrors.Recover()
|
||||
func Recover(args ...any) {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("ERR:", r)
|
||||
@@ -49,7 +47,7 @@ func Recover(args ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetGID the current goroutine id. Used only for debugging.
|
||||
// Get the current goroutine id. Used only for debugging.
|
||||
func GetGID() uint64 {
|
||||
b := make([]byte, 64)
|
||||
b = b[:runtime.Stack(b, false)]
|
||||
@@ -59,34 +57,11 @@ func GetGID() uint64 {
|
||||
return n
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
|
||||
|
||||
// Must panics if err != nil.
|
||||
func Must(err error) {
|
||||
@@ -94,18 +69,3 @@ func Must(err error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// 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)
|
||||
|
||||
}
|
||||
@@ -15,14 +15,11 @@ package herrors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
godartsassv1 "github.com/bep/godartsass"
|
||||
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
"github.com/bep/godartsass"
|
||||
"github.com/bep/golibsass/libsass/libsasserrors"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
@@ -38,7 +35,7 @@ import (
|
||||
type FileError interface {
|
||||
error
|
||||
|
||||
// ErrorContext holds some context information about the error.
|
||||
// ErroContext holds some context information about the error.
|
||||
ErrorContext() *ErrorContext
|
||||
|
||||
text.Positioner
|
||||
@@ -148,8 +145,6 @@ func (e *fileError) causeString() string {
|
||||
// Avoid repeating the file info in the error message.
|
||||
case godartsass.SassError:
|
||||
return v.Message
|
||||
case godartsassv1.SassError:
|
||||
return v.Message
|
||||
case libsasserrors.Error:
|
||||
return v.Message
|
||||
default:
|
||||
@@ -297,7 +292,7 @@ func extractFileTypePos(err error) (string, text.Position) {
|
||||
}
|
||||
|
||||
// The error type from the minifier contains line number and column number.
|
||||
if line, col := extractLineNumberAndColumnNumber(err); line >= 0 {
|
||||
if line, col := exctractLineNumberAndColumnNumber(err); line >= 0 {
|
||||
pos.LineNumber = line
|
||||
pos.ColumnNumber = col
|
||||
return fileType, pos
|
||||
@@ -369,7 +364,7 @@ func extractOffsetAndType(e error) (int, string) {
|
||||
}
|
||||
}
|
||||
|
||||
func extractLineNumberAndColumnNumber(e error) (int, int) {
|
||||
func exctractLineNumberAndColumnNumber(e error) (int, int) {
|
||||
switch v := e.(type) {
|
||||
case *parse.Error:
|
||||
return v.Line, v.Column
|
||||
@@ -390,13 +385,6 @@ func extractPosition(e error) (pos text.Position) {
|
||||
pos.Filename = filename
|
||||
pos.Offset = start.Offset
|
||||
pos.ColumnNumber = start.Column
|
||||
case godartsassv1.SassError:
|
||||
span := v.Span
|
||||
start := span.Start
|
||||
filename, _ := paths.UrlToFilename(span.Url)
|
||||
pos.Filename = filename
|
||||
pos.Offset = start.Offset
|
||||
pos.ColumnNumber = start.Column
|
||||
case libsasserrors.Error:
|
||||
pos.Filename = v.File
|
||||
pos.LineNumber = v.Line
|
||||
@@ -404,17 +392,3 @@ func extractPosition(e error) (pos text.Position) {
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
|
||||
return exec.Command(bin, arg...), nil
|
||||
}
|
||||
|
||||
// Exec enforces a security policy for commands run via os/exec.
|
||||
// Exec encorces a security policy for commands run via os/exec.
|
||||
type Exec struct {
|
||||
sc security.Config
|
||||
|
||||
|
||||
@@ -208,23 +208,6 @@ func AsTime(v reflect.Value, loc *time.Location) (time.Time, bool) {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
|
||||
fn := v.MethodByName(name)
|
||||
var args []reflect.Value
|
||||
tp := fn.Type()
|
||||
if tp.NumIn() > 0 {
|
||||
if tp.NumIn() > 1 {
|
||||
panic("not supported")
|
||||
}
|
||||
first := tp.In(0)
|
||||
if first.Implements(ContextInterface) {
|
||||
args = append(args, reflect.ValueOf(cxt))
|
||||
}
|
||||
}
|
||||
|
||||
return fn.Call(args)
|
||||
}
|
||||
|
||||
// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
|
||||
func indirectInterface(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Interface {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright 2023 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 hstrings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/compare"
|
||||
)
|
||||
|
||||
var _ compare.Eqer = StringEqualFold("")
|
||||
|
||||
// StringEqualFold is a string that implements the compare.Eqer interface and considers
|
||||
// two strings equal if they are equal when folded to lower case.
|
||||
// The compare.Eqer interface is used in Hugo to compare values in templates (e.g. using the eq template function).
|
||||
type StringEqualFold string
|
||||
|
||||
func (s StringEqualFold) EqualFold(s2 string) bool {
|
||||
return strings.EqualFold(string(s), s2)
|
||||
}
|
||||
|
||||
func (s StringEqualFold) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s StringEqualFold) Eq(s2 any) bool {
|
||||
switch ss := s2.(type) {
|
||||
case string:
|
||||
return s.EqualFold(ss)
|
||||
case fmt.Stringer:
|
||||
return s.EqualFold(ss.String())
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// EqualAny returns whether a string is equal to any of the given strings.
|
||||
func EqualAny(a string, b ...string) bool {
|
||||
for _, s := range b {
|
||||
if a == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// regexpCache represents a cache of regexp objects protected by a mutex.
|
||||
type regexpCache struct {
|
||||
mu sync.RWMutex
|
||||
re map[string]*regexp.Regexp
|
||||
}
|
||||
|
||||
func (rc *regexpCache) getOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
|
||||
var ok bool
|
||||
|
||||
if re, ok = rc.get(pattern); !ok {
|
||||
re, err = regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc.set(pattern, re)
|
||||
}
|
||||
|
||||
return re, nil
|
||||
}
|
||||
|
||||
func (rc *regexpCache) get(key string) (re *regexp.Regexp, ok bool) {
|
||||
rc.mu.RLock()
|
||||
re, ok = rc.re[key]
|
||||
rc.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (rc *regexpCache) set(key string, re *regexp.Regexp) {
|
||||
rc.mu.Lock()
|
||||
rc.re[key] = re
|
||||
rc.mu.Unlock()
|
||||
}
|
||||
|
||||
var reCache = regexpCache{re: make(map[string]*regexp.Regexp)}
|
||||
|
||||
// GetOrCompileRegexp retrieves a regexp object from the cache based upon the pattern.
|
||||
// If the pattern is not found in the cache, the pattern is compiled and added to
|
||||
// the cache.
|
||||
func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
|
||||
return reCache.getOrCompileRegexp(pattern)
|
||||
}
|
||||
|
||||
// InSlice checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
func InSlice(arr []string, el string) bool {
|
||||
for _, v := range arr {
|
||||
if v == el {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InSlicEqualFold checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
// It uses strings.EqualFold to compare.
|
||||
func InSlicEqualFold(arr []string, el string) bool {
|
||||
for _, v := range arr {
|
||||
if strings.EqualFold(v, el) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright 2023 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 hstrings
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestStringEqualFold(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
s1 := "A"
|
||||
s2 := "a"
|
||||
|
||||
c.Assert(StringEqualFold(s1).EqualFold(s2), qt.Equals, true)
|
||||
c.Assert(StringEqualFold(s1).EqualFold(s1), qt.Equals, true)
|
||||
c.Assert(StringEqualFold(s2).EqualFold(s1), qt.Equals, true)
|
||||
c.Assert(StringEqualFold(s2).EqualFold(s2), qt.Equals, true)
|
||||
c.Assert(StringEqualFold(s1).EqualFold("b"), qt.Equals, false)
|
||||
c.Assert(StringEqualFold(s1).Eq(s2), qt.Equals, true)
|
||||
c.Assert(StringEqualFold(s1).Eq("b"), qt.Equals, false)
|
||||
|
||||
}
|
||||
|
||||
func TestGetOrCompileRegexp(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
re, err := GetOrCompileRegexp(`\d+`)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(re.MatchString("123"), qt.Equals, true)
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkGetOrCompileRegexp(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
GetOrCompileRegexp(`\d+`)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompileRegexp(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
regexp.MustCompile(`\d+`)
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// 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 htime_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
// Issue #11267
|
||||
func TestApplyWithContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- config.toml --
|
||||
defaultContentLanguage = 'it'
|
||||
-- layouts/index.html --
|
||||
{{ $dates := slice
|
||||
"2022-01-03"
|
||||
"2022-02-01"
|
||||
"2022-03-02"
|
||||
"2022-04-07"
|
||||
"2022-05-06"
|
||||
"2022-06-04"
|
||||
"2022-07-03"
|
||||
"2022-08-01"
|
||||
"2022-09-06"
|
||||
"2022-10-05"
|
||||
"2022-11-03"
|
||||
"2022-12-02"
|
||||
}}
|
||||
{{ range $dates }}
|
||||
{{ . | time.Format "month: _January_ weekday: _Monday_" }}
|
||||
{{ . | time.Format "month: _Jan_ weekday: _Mon_" }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", `
|
||||
month: _gennaio_ weekday: _lunedì_
|
||||
month: _gen_ weekday: _lun_
|
||||
month: _febbraio_ weekday: _martedì_
|
||||
month: _feb_ weekday: _mar_
|
||||
month: _marzo_ weekday: _mercoledì_
|
||||
month: _mar_ weekday: _mer_
|
||||
month: _aprile_ weekday: _giovedì_
|
||||
month: _apr_ weekday: _gio_
|
||||
month: _maggio_ weekday: _venerdì_
|
||||
month: _mag_ weekday: _ven_
|
||||
month: _giugno_ weekday: _sabato_
|
||||
month: _giu_ weekday: _sab_
|
||||
month: _luglio_ weekday: _domenica_
|
||||
month: _lug_ weekday: _dom_
|
||||
month: _agosto_ weekday: _lunedì_
|
||||
month: _ago_ weekday: _lun_
|
||||
month: _settembre_ weekday: _martedì_
|
||||
month: _set_ weekday: _mar_
|
||||
month: _ottobre_ weekday: _mercoledì_
|
||||
month: _ott_ weekday: _mer_
|
||||
month: _novembre_ weekday: _giovedì_
|
||||
month: _nov_ weekday: _gio_
|
||||
month: _dicembre_ weekday: _venerdì_
|
||||
month: _dic_ weekday: _ven_
|
||||
`)
|
||||
}
|
||||
+6
-18
@@ -14,11 +14,10 @@
|
||||
package htime
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/clocks"
|
||||
"github.com/bep/clock"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/locales"
|
||||
@@ -75,7 +74,7 @@ var (
|
||||
"December",
|
||||
}
|
||||
|
||||
Clock = clocks.System()
|
||||
Clock = clock.System()
|
||||
)
|
||||
|
||||
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
|
||||
@@ -124,15 +123,12 @@ func (f TimeFormatter) Format(t time.Time, layout string) string {
|
||||
monthIdx := t.Month() - 1 // Month() starts at 1.
|
||||
dayIdx := t.Weekday()
|
||||
|
||||
if strings.Contains(layout, "January") {
|
||||
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
|
||||
} else if strings.Contains(layout, "Jan") {
|
||||
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
|
||||
if !strings.Contains(s, f.ltr.MonthWide(t.Month())) {
|
||||
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthAbbreviated(t.Month()))
|
||||
}
|
||||
|
||||
if strings.Contains(layout, "Monday") {
|
||||
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
|
||||
} else if strings.Contains(layout, "Mon") {
|
||||
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
|
||||
if !strings.Contains(s, f.ltr.WeekdayWide(t.Weekday())) {
|
||||
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday()))
|
||||
}
|
||||
|
||||
@@ -167,11 +163,3 @@ func Since(t time.Time) time.Duration {
|
||||
type AsTimeProvider interface {
|
||||
AsTime(zone *time.Location) time.Time
|
||||
}
|
||||
|
||||
// StopWatch is a simple helper to measure time during development.
|
||||
func StopWatch(name string) func() {
|
||||
start := time.Now()
|
||||
return func() {
|
||||
log.Printf("StopWatch %q took %s", name, time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ package hugio
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -51,7 +53,7 @@ func CopyFile(fs afero.Fs, from, to string) error {
|
||||
|
||||
// CopyDir copies a directory.
|
||||
func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool) error {
|
||||
fi, err := fs.Stat(from)
|
||||
fi, err := os.Stat(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -65,7 +67,7 @@ func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool
|
||||
return err
|
||||
}
|
||||
|
||||
entries, _ := afero.ReadDir(fs, from)
|
||||
entries, _ := ioutil.ReadDir(from)
|
||||
for _, entry := range entries {
|
||||
fromFilename := filepath.Join(from, entry.Name())
|
||||
toFilename := filepath.Join(to, entry.Name())
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// 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 hugio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// HasBytesWriter is a writer that will set Match to true if the given pattern
|
||||
// is found in the stream.
|
||||
type HasBytesWriter struct {
|
||||
Match bool
|
||||
Pattern []byte
|
||||
|
||||
i int
|
||||
done bool
|
||||
buff []byte
|
||||
}
|
||||
|
||||
func (h *HasBytesWriter) Write(p []byte) (n int, err error) {
|
||||
if h.done {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
if len(h.buff) == 0 {
|
||||
h.buff = make([]byte, len(h.Pattern)*2)
|
||||
}
|
||||
|
||||
for i := range p {
|
||||
h.buff[h.i] = p[i]
|
||||
h.i++
|
||||
if h.i == len(h.buff) {
|
||||
// Shift left.
|
||||
copy(h.buff, h.buff[len(h.buff)/2:])
|
||||
h.i = len(h.buff) / 2
|
||||
}
|
||||
|
||||
if bytes.Contains(h.buff, h.Pattern) {
|
||||
h.Match = true
|
||||
h.done = true
|
||||
return len(p), nil
|
||||
}
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// 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 hugio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestHasBytesWriter(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
c := qt.New((t))
|
||||
|
||||
neww := func() (*HasBytesWriter, io.Writer) {
|
||||
var b bytes.Buffer
|
||||
|
||||
h := &HasBytesWriter{
|
||||
Pattern: []byte("__foo"),
|
||||
}
|
||||
return h, io.MultiWriter(&b, h)
|
||||
}
|
||||
|
||||
rndStr := func() string {
|
||||
return strings.Repeat("ab cfo", r.Intn(33))
|
||||
}
|
||||
|
||||
for i := 0; i < 22; i++ {
|
||||
h, w := neww()
|
||||
fmt.Fprintf(w, rndStr()+"abc __foobar"+rndStr())
|
||||
c.Assert(h.Match, qt.Equals, true)
|
||||
|
||||
h, w = neww()
|
||||
fmt.Fprintf(w, rndStr()+"abc __f")
|
||||
fmt.Fprintf(w, "oo bar"+rndStr())
|
||||
c.Assert(h.Match, qt.Equals, true)
|
||||
|
||||
h, w = neww()
|
||||
fmt.Fprintf(w, rndStr()+"abc __moo bar")
|
||||
c.Assert(h.Match, qt.Equals, false)
|
||||
}
|
||||
|
||||
h, w := neww()
|
||||
fmt.Fprintf(w, "__foo")
|
||||
c.Assert(h.Match, qt.Equals, true)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ package hugio
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// As implemented by strings.Builder.
|
||||
@@ -33,7 +34,7 @@ type multiWriteCloser struct {
|
||||
func (m multiWriteCloser) Close() error {
|
||||
var err error
|
||||
for _, c := range m.closers {
|
||||
if closeErr := c.Close(); closeErr != nil {
|
||||
if closeErr := c.Close(); err != nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
@@ -62,7 +63,7 @@ func ToWriteCloser(w io.Writer) io.WriteCloser {
|
||||
io.Closer
|
||||
}{
|
||||
w,
|
||||
io.NopCloser(nil),
|
||||
ioutil.NopCloser(nil),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +79,6 @@ func ToReadCloser(r io.Reader) io.ReadCloser {
|
||||
io.Closer
|
||||
}{
|
||||
r,
|
||||
io.NopCloser(nil),
|
||||
ioutil.NopCloser(nil),
|
||||
}
|
||||
}
|
||||
|
||||
+32
-173
@@ -24,13 +24,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
godartsassv1 "github.com/bep/godartsass"
|
||||
"github.com/bep/logg"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -51,8 +44,8 @@ var (
|
||||
vendorInfo string
|
||||
)
|
||||
|
||||
// HugoInfo contains information about the current Hugo environment
|
||||
type HugoInfo struct {
|
||||
// Info contains information about the current Hugo environment
|
||||
type Info struct {
|
||||
CommitHash string
|
||||
BuildDate string
|
||||
|
||||
@@ -65,61 +58,36 @@ type HugoInfo struct {
|
||||
// version of go that the Hugo binary was built with
|
||||
GoVersion string
|
||||
|
||||
conf ConfigProvider
|
||||
deps []*Dependency
|
||||
}
|
||||
|
||||
// Version returns the current version as a comparable version string.
|
||||
func (i HugoInfo) Version() VersionString {
|
||||
func (i Info) Version() VersionString {
|
||||
return CurrentVersion.Version()
|
||||
}
|
||||
|
||||
// Generator a Hugo meta generator HTML tag.
|
||||
func (i HugoInfo) Generator() template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s">`, CurrentVersion.String()))
|
||||
func (i Info) Generator() template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, CurrentVersion.String()))
|
||||
}
|
||||
|
||||
// IsDevelopment reports whether the current running environment is "development".
|
||||
func (i HugoInfo) IsDevelopment() bool {
|
||||
return i.Environment == EnvironmentDevelopment
|
||||
}
|
||||
|
||||
// IsProduction reports whether the current running environment is "production".
|
||||
func (i HugoInfo) IsProduction() bool {
|
||||
func (i Info) IsProduction() bool {
|
||||
return i.Environment == EnvironmentProduction
|
||||
}
|
||||
|
||||
// IsServer reports whether the built-in server is running.
|
||||
func (i HugoInfo) IsServer() bool {
|
||||
return i.conf.Running()
|
||||
}
|
||||
|
||||
// IsExtended reports whether the Hugo binary is the extended version.
|
||||
func (i HugoInfo) IsExtended() bool {
|
||||
func (i Info) IsExtended() bool {
|
||||
return IsExtended
|
||||
}
|
||||
|
||||
// WorkingDir returns the project working directory.
|
||||
func (i HugoInfo) WorkingDir() string {
|
||||
return i.conf.WorkingDir()
|
||||
}
|
||||
|
||||
// Deps gets a list of dependencies for this Hugo build.
|
||||
func (i HugoInfo) Deps() []*Dependency {
|
||||
func (i Info) Deps() []*Dependency {
|
||||
return i.deps
|
||||
}
|
||||
|
||||
// ConfigProvider represents the config options that are relevant for HugoInfo.
|
||||
type ConfigProvider interface {
|
||||
Environment() string
|
||||
Running() bool
|
||||
WorkingDir() string
|
||||
}
|
||||
|
||||
// NewInfo creates a new Hugo Info object.
|
||||
func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
|
||||
if conf.Environment() == "" {
|
||||
panic("environment not set")
|
||||
func NewInfo(environment string, deps []*Dependency) Info {
|
||||
if environment == "" {
|
||||
environment = EnvironmentProduction
|
||||
}
|
||||
var (
|
||||
commitHash string
|
||||
@@ -134,11 +102,10 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
|
||||
goVersion = bi.GoVersion
|
||||
}
|
||||
|
||||
return HugoInfo{
|
||||
return Info{
|
||||
CommitHash: commitHash,
|
||||
BuildDate: buildDate,
|
||||
Environment: conf.Environment(),
|
||||
conf: conf,
|
||||
Environment: environment,
|
||||
deps: deps,
|
||||
GoVersion: goVersion,
|
||||
}
|
||||
@@ -146,7 +113,7 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
|
||||
|
||||
// GetExecEnviron creates and gets the common os/exec environment used in the
|
||||
// external programs we interact with via os/exec, e.g. postcss.
|
||||
func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []string {
|
||||
func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
|
||||
var env []string
|
||||
nodepath := filepath.Join(workDir, "node_modules")
|
||||
if np := os.Getenv("NODE_PATH"); np != "" {
|
||||
@@ -154,9 +121,8 @@ func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []strin
|
||||
}
|
||||
config.SetEnvVars(&env, "NODE_PATH", nodepath)
|
||||
config.SetEnvVars(&env, "PWD", workDir)
|
||||
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.Environment())
|
||||
config.SetEnvVars(&env, "HUGO_ENV", cfg.Environment())
|
||||
config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.BaseConfig().PublishDir))
|
||||
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.GetString("environment"))
|
||||
config.SetEnvVars(&env, "HUGO_ENV", cfg.GetString("environment"))
|
||||
|
||||
if fs != nil {
|
||||
fis, err := afero.ReadDir(fs, files.FolderJSConfig)
|
||||
@@ -184,10 +150,8 @@ type buildInfo struct {
|
||||
*debug.BuildInfo
|
||||
}
|
||||
|
||||
var (
|
||||
bInfo *buildInfo
|
||||
bInfoInit sync.Once
|
||||
)
|
||||
var bInfo *buildInfo
|
||||
var bInfoInit sync.Once
|
||||
|
||||
func getBuildInfo() *buildInfo {
|
||||
bInfoInit.Do(func() {
|
||||
@@ -214,20 +178,30 @@ func getBuildInfo() *buildInfo {
|
||||
bInfo.GoArch = s.Value
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return bInfo
|
||||
}
|
||||
|
||||
func formatDep(path, version string) string {
|
||||
return fmt.Sprintf("%s=%q", path, version)
|
||||
}
|
||||
|
||||
// GetDependencyList returns a sorted dependency list on the format package="version".
|
||||
// It includes both Go dependencies and (a manually maintained) list of C(++) dependencies.
|
||||
func GetDependencyList() []string {
|
||||
var deps []string
|
||||
|
||||
formatDep := func(path, version string) string {
|
||||
return fmt.Sprintf("%s=%q", path, version)
|
||||
}
|
||||
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
deps,
|
||||
// TODO(bep) consider adding a DepsNonGo() method to these upstream projects.
|
||||
formatDep("github.com/sass/libsass", "3.6.5"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.2.0"),
|
||||
)
|
||||
}
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi == nil {
|
||||
return deps
|
||||
@@ -237,39 +211,11 @@ func GetDependencyList() []string {
|
||||
deps = append(deps, formatDep(dep.Path, dep.Version))
|
||||
}
|
||||
|
||||
deps = append(deps, GetDependencyListNonGo()...)
|
||||
|
||||
sort.Strings(deps)
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
// GetDependencyListNonGo returns a list of non-Go dependencies.
|
||||
func GetDependencyListNonGo() []string {
|
||||
var deps []string
|
||||
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
deps,
|
||||
formatDep("github.com/sass/libsass", "3.6.5"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.2.4"),
|
||||
)
|
||||
}
|
||||
|
||||
if dartSass := dartSassVersion(); dartSass.ProtocolVersion != "" {
|
||||
dartSassPath := "github.com/sass/dart-sass-embedded"
|
||||
if IsDartSassV2() {
|
||||
dartSassPath = "github.com/sass/dart-sass"
|
||||
}
|
||||
deps = append(deps,
|
||||
formatDep(dartSassPath+"/protocol", dartSass.ProtocolVersion),
|
||||
formatDep(dartSassPath+"/compiler", dartSass.CompilerVersion),
|
||||
formatDep(dartSassPath+"/implementation", dartSass.ImplementationVersion),
|
||||
)
|
||||
}
|
||||
return deps
|
||||
}
|
||||
|
||||
// IsRunningAsTest reports whether we are running as a test.
|
||||
func IsRunningAsTest() bool {
|
||||
for _, arg := range os.Args {
|
||||
@@ -303,90 +249,3 @@ type Dependency struct {
|
||||
// Replaced by this dependency.
|
||||
Replace *Dependency
|
||||
}
|
||||
|
||||
func dartSassVersion() godartsass.DartSassVersion {
|
||||
if DartSassBinaryName == "" {
|
||||
return godartsass.DartSassVersion{}
|
||||
}
|
||||
if IsDartSassV2() {
|
||||
v, _ := godartsass.Version(DartSassBinaryName)
|
||||
return v
|
||||
}
|
||||
|
||||
v, _ := godartsassv1.Version(DartSassBinaryName)
|
||||
var vv godartsass.DartSassVersion
|
||||
mapstructure.WeakDecode(v, &vv)
|
||||
return vv
|
||||
}
|
||||
|
||||
// DartSassBinaryName is the name of the Dart Sass binary to use.
|
||||
// TODO(beop) find a better place for this.
|
||||
var DartSassBinaryName string
|
||||
|
||||
func init() {
|
||||
DartSassBinaryName = os.Getenv("DART_SASS_BINARY")
|
||||
if DartSassBinaryName == "" {
|
||||
for _, name := range dartSassBinaryNamesV2 {
|
||||
if hexec.InPath(name) {
|
||||
DartSassBinaryName = name
|
||||
break
|
||||
}
|
||||
}
|
||||
if DartSassBinaryName == "" {
|
||||
if hexec.InPath(dartSassBinaryNameV1) {
|
||||
DartSassBinaryName = dartSassBinaryNameV1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
dartSassBinaryNameV1 = "dart-sass-embedded"
|
||||
dartSassBinaryNamesV2 = []string{"dart-sass", "sass"}
|
||||
)
|
||||
|
||||
func IsDartSassV2() bool {
|
||||
return !strings.Contains(DartSassBinaryName, "embedded")
|
||||
}
|
||||
|
||||
// Deprecate informs about a deprecation starting at the given version.
|
||||
//
|
||||
// A deprecation typically needs a simple change in the template, but doing so will make the template incompatible with older versions.
|
||||
// Theme maintainers generally want
|
||||
// 1. No warnings or errors in the console when building a Hugo site.
|
||||
// 2. Their theme to work for at least the last few Hugo versions.
|
||||
func Deprecate(item, alternative string, version string) {
|
||||
level := deprecationLogLevelFromVersion(version)
|
||||
DeprecateLevel(item, alternative, version, level)
|
||||
}
|
||||
|
||||
// DeprecateLevel informs about a deprecation logging at the given level.
|
||||
func DeprecateLevel(item, alternative, version string, level logg.Level) {
|
||||
var msg string
|
||||
if level == logg.LevelError {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in Hugo %s. %s", item, version, CurrentVersion.Next().ReleaseVersion(), alternative)
|
||||
} else {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
|
||||
}
|
||||
|
||||
loggers.Log().Logger().WithLevel(level).Logf(msg)
|
||||
}
|
||||
|
||||
// We ususally do about one minor version a month.
|
||||
// We want people to run at least the current and previous version without any warnings.
|
||||
// We want people who don't update Hugo that often to see the warnings and errors before we remove the feature.
|
||||
func deprecationLogLevelFromVersion(ver string) logg.Level {
|
||||
from := MustParseVersion(ver)
|
||||
to := CurrentVersion
|
||||
minorDiff := to.Minor - from.Minor
|
||||
switch {
|
||||
case minorDiff >= 12:
|
||||
// Start failing the build after about a year.
|
||||
return logg.LevelError
|
||||
case minorDiff >= 6:
|
||||
// Start printing warnings after about six months.
|
||||
return logg.LevelWarn
|
||||
default:
|
||||
return logg.LevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,19 +17,16 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestHugoInfo(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
|
||||
hugoInfo := NewInfo(conf, nil)
|
||||
hugoInfo := NewInfo("", nil)
|
||||
|
||||
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
|
||||
c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi != nil {
|
||||
@@ -39,45 +36,9 @@ func TestHugoInfo(t *testing.T) {
|
||||
}
|
||||
c.Assert(hugoInfo.Environment, qt.Equals, "production")
|
||||
c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.IsDevelopment(), qt.Equals, false)
|
||||
c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
|
||||
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
|
||||
c.Assert(hugoInfo.IsServer(), qt.Equals, false)
|
||||
|
||||
devHugoInfo := NewInfo(testConfig{environment: "development", running: true}, nil)
|
||||
c.Assert(devHugoInfo.IsDevelopment(), qt.Equals, true)
|
||||
devHugoInfo := NewInfo("development", nil)
|
||||
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
|
||||
c.Assert(devHugoInfo.IsServer(), qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestDeprecationLogLevelFromVersion(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError)
|
||||
ver := CurrentVersion
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
|
||||
ver.Minor -= 1
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
|
||||
ver.Minor -= 6
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
|
||||
ver.Minor -= 6
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
|
||||
}
|
||||
|
||||
type testConfig struct {
|
||||
environment string
|
||||
running bool
|
||||
workingDir string
|
||||
}
|
||||
|
||||
func (c testConfig) Environment() string {
|
||||
return c.environment
|
||||
}
|
||||
|
||||
func (c testConfig) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c testConfig) WorkingDir() string {
|
||||
return c.workingDir
|
||||
}
|
||||
|
||||
@@ -67,11 +67,8 @@ func (h VersionString) String() string {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other any) int {
|
||||
return compareVersions(h.Version(), other)
|
||||
}
|
||||
|
||||
func (h VersionString) Version() Version {
|
||||
return MustParseVersion(h.String())
|
||||
v := MustParseVersion(h.String())
|
||||
return compareVersions(v, other)
|
||||
}
|
||||
|
||||
// Eq implements the compare.Eqer interface.
|
||||
@@ -267,6 +264,7 @@ func compareFloatWithVersion(v1 float64, v2 Version) int {
|
||||
|
||||
if v1maj > v2.Major {
|
||||
return 1
|
||||
|
||||
}
|
||||
|
||||
if v1maj < v2.Major {
|
||||
@@ -278,6 +276,7 @@ func compareFloatWithVersion(v1 float64, v2 Version) int {
|
||||
}
|
||||
|
||||
return -1
|
||||
|
||||
}
|
||||
|
||||
func GoMinorVersion() int {
|
||||
|
||||
@@ -17,7 +17,7 @@ package hugo
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Major: 0,
|
||||
Minor: 120,
|
||||
Minor: 102,
|
||||
PatchLevel: 0,
|
||||
Suffix: "",
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// package loggers contains some basic logging setup.
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
var bold = color.New(color.Bold)
|
||||
|
||||
// levelColor mapping.
|
||||
var levelColor = [...]*color.Color{
|
||||
logg.LevelDebug: color.New(color.FgWhite),
|
||||
logg.LevelInfo: color.New(color.FgBlue),
|
||||
logg.LevelWarn: color.New(color.FgYellow),
|
||||
logg.LevelError: color.New(color.FgRed),
|
||||
}
|
||||
|
||||
// levelString mapping.
|
||||
var levelString = [...]string{
|
||||
logg.LevelDebug: "DEBUG",
|
||||
logg.LevelInfo: "INFO ",
|
||||
logg.LevelWarn: "WARN ",
|
||||
logg.LevelError: "ERROR",
|
||||
}
|
||||
|
||||
// newDefaultHandler handler.
|
||||
func newDefaultHandler(outWriter, errWriter io.Writer) logg.Handler {
|
||||
return &defaultHandler{
|
||||
outWriter: outWriter,
|
||||
errWriter: errWriter,
|
||||
Padding: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Default Handler implementation.
|
||||
// Based on https://github.com/apex/log/blob/master/handlers/cli/cli.go
|
||||
type defaultHandler struct {
|
||||
mu sync.Mutex
|
||||
outWriter io.Writer // Defaults to os.Stdout.
|
||||
errWriter io.Writer // Defaults to os.Stderr.
|
||||
|
||||
Padding int
|
||||
}
|
||||
|
||||
// HandleLog implements logg.Handler.
|
||||
func (h *defaultHandler) HandleLog(e *logg.Entry) error {
|
||||
color := levelColor[e.Level]
|
||||
level := levelString[e.Level]
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
var w io.Writer
|
||||
if e.Level > logg.LevelInfo {
|
||||
w = h.errWriter
|
||||
} else {
|
||||
w = h.outWriter
|
||||
}
|
||||
|
||||
var prefix string
|
||||
for _, field := range e.Fields {
|
||||
if field.Name == FieldNameCmd {
|
||||
prefix = fmt.Sprint(field.Value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
prefix = prefix + ": "
|
||||
}
|
||||
|
||||
color.Fprintf(w, "%s %s%s", fmt.Sprintf("%*s", h.Padding+1, level), color.Sprint(prefix), e.Message)
|
||||
|
||||
for _, field := range e.Fields {
|
||||
if strings.HasPrefix(field.Name, reservedFieldNamePrefix) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " %s %v", color.Sprint(field.Name), field.Value)
|
||||
}
|
||||
|
||||
fmt.Fprintln(w)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
)
|
||||
|
||||
// PanicOnWarningHook panics on warnings.
|
||||
var PanicOnWarningHook = func(e *logg.Entry) error {
|
||||
if e.Level != logg.LevelWarn {
|
||||
return nil
|
||||
}
|
||||
panic(e.Message)
|
||||
}
|
||||
|
||||
func newLogLevelCounter() *logLevelCounter {
|
||||
return &logLevelCounter{
|
||||
counters: make(map[logg.Level]int),
|
||||
}
|
||||
}
|
||||
|
||||
func newLogOnceHandler(threshold logg.Level) *logOnceHandler {
|
||||
return &logOnceHandler{
|
||||
threshold: threshold,
|
||||
seen: make(map[uint64]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func newStopHandler(h ...logg.Handler) *stopHandler {
|
||||
return &stopHandler{
|
||||
handlers: h,
|
||||
}
|
||||
}
|
||||
|
||||
func newSuppressStatementsHandler(statements map[string]bool) *suppressStatementsHandler {
|
||||
return &suppressStatementsHandler{
|
||||
statements: statements,
|
||||
}
|
||||
}
|
||||
|
||||
type logLevelCounter struct {
|
||||
mu sync.RWMutex
|
||||
counters map[logg.Level]int
|
||||
}
|
||||
|
||||
func (h *logLevelCounter) HandleLog(e *logg.Entry) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.counters[e.Level]++
|
||||
return nil
|
||||
}
|
||||
|
||||
var stopError = fmt.Errorf("stop")
|
||||
|
||||
type logOnceHandler struct {
|
||||
threshold logg.Level
|
||||
mu sync.Mutex
|
||||
seen map[uint64]bool
|
||||
}
|
||||
|
||||
func (h *logOnceHandler) HandleLog(e *logg.Entry) error {
|
||||
if e.Level < h.threshold {
|
||||
// We typically only want to enable this for warnings and above.
|
||||
// The common use case is that many go routines may log the same error.
|
||||
return nil
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
hash := identity.HashUint64(e.Level, e.Message, e.Fields)
|
||||
if h.seen[hash] {
|
||||
return stopError
|
||||
}
|
||||
h.seen[hash] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *logOnceHandler) reset() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.seen = make(map[uint64]bool)
|
||||
}
|
||||
|
||||
type stopHandler struct {
|
||||
handlers []logg.Handler
|
||||
}
|
||||
|
||||
// HandleLog implements logg.Handler.
|
||||
func (h *stopHandler) HandleLog(e *logg.Entry) error {
|
||||
for _, handler := range h.handlers {
|
||||
if err := handler.HandleLog(e); err != nil {
|
||||
if err == stopError {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type suppressStatementsHandler struct {
|
||||
statements map[string]bool
|
||||
}
|
||||
|
||||
func (h *suppressStatementsHandler) HandleLog(e *logg.Entry) error {
|
||||
for _, field := range e.Fields {
|
||||
if field.Name == FieldNameStatementID {
|
||||
if h.statements[field.Value.(string)] {
|
||||
return stopError
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// replacer creates a new log handler that does string replacement in log messages.
|
||||
func replacer(repl *strings.Replacer) logg.Handler {
|
||||
return logg.HandlerFunc(func(e *logg.Entry) error {
|
||||
e.Message = repl.Replace(e.Message)
|
||||
for i, field := range e.Fields {
|
||||
if s, ok := field.Value.(string); ok {
|
||||
e.Fields[i].Value = repl.Replace(s)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// whiteSpaceTrimmer creates a new log handler that trims whitespace from log messages and string fields.
|
||||
func whiteSpaceTrimmer() logg.Handler {
|
||||
return logg.HandlerFunc(func(e *logg.Entry) error {
|
||||
e.Message = strings.TrimSpace(e.Message)
|
||||
for i, field := range e.Fields {
|
||||
if s, ok := field.Value.(string); ok {
|
||||
e.Fields[i].Value = strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
)
|
||||
|
||||
// newNoColoursHandler creates a new NoColoursHandler
|
||||
func newNoColoursHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noColoursHandler {
|
||||
if predicate == nil {
|
||||
predicate = func(e *logg.Entry) bool { return true }
|
||||
}
|
||||
return &noColoursHandler{
|
||||
noLevelPrefix: noLevelPrefix,
|
||||
outWriter: outWriter,
|
||||
errWriter: errWriter,
|
||||
predicate: predicate,
|
||||
}
|
||||
}
|
||||
|
||||
type noColoursHandler struct {
|
||||
mu sync.Mutex
|
||||
outWriter io.Writer // Defaults to os.Stdout.
|
||||
errWriter io.Writer // Defaults to os.Stderr.
|
||||
predicate func(*logg.Entry) bool
|
||||
noLevelPrefix bool
|
||||
}
|
||||
|
||||
func (h *noColoursHandler) HandleLog(e *logg.Entry) error {
|
||||
if !h.predicate(e) {
|
||||
return nil
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
var w io.Writer
|
||||
if e.Level > logg.LevelInfo {
|
||||
w = h.errWriter
|
||||
} else {
|
||||
w = h.outWriter
|
||||
}
|
||||
|
||||
var prefix string
|
||||
for _, field := range e.Fields {
|
||||
if field.Name == FieldNameCmd {
|
||||
prefix = fmt.Sprint(field.Value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
prefix = prefix + ": "
|
||||
}
|
||||
|
||||
if h.noLevelPrefix {
|
||||
fmt.Fprintf(w, "%s%s", prefix, e.Message)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s %s%s", levelString[e.Level], prefix, e.Message)
|
||||
}
|
||||
|
||||
for _, field := range e.Fields {
|
||||
if strings.HasPrefix(field.Name, reservedFieldNamePrefix) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " %s %q", field.Name, field.Value)
|
||||
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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 loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IgnorableLogger is a logger that ignores certain log statements.
|
||||
type IgnorableLogger interface {
|
||||
Logger
|
||||
Errorsf(statementID, format string, v ...any)
|
||||
Apply(logger Logger) IgnorableLogger
|
||||
}
|
||||
|
||||
type ignorableLogger struct {
|
||||
Logger
|
||||
statements map[string]bool
|
||||
}
|
||||
|
||||
// NewIgnorableLogger wraps the given logger and ignores the log statement IDs given.
|
||||
func NewIgnorableLogger(logger Logger, statements ...string) IgnorableLogger {
|
||||
statementsSet := make(map[string]bool)
|
||||
for _, s := range statements {
|
||||
statementsSet[strings.ToLower(s)] = true
|
||||
}
|
||||
return ignorableLogger{
|
||||
Logger: logger,
|
||||
statements: statementsSet,
|
||||
}
|
||||
}
|
||||
|
||||
// Errorsf logs statementID as an ERROR if not configured as ignoreable.
|
||||
func (l ignorableLogger) Errorsf(statementID, format string, v ...any) {
|
||||
if l.statements[statementID] {
|
||||
// Ignore.
|
||||
return
|
||||
}
|
||||
ignoreMsg := fmt.Sprintf(`
|
||||
If you feel that this should not be logged as an ERROR, you can ignore it by adding this to your site config:
|
||||
ignoreErrors = [%q]`, statementID)
|
||||
|
||||
format += ignoreMsg
|
||||
|
||||
l.Errorf(format, v...)
|
||||
}
|
||||
|
||||
func (l ignorableLogger) Apply(logger Logger) IgnorableLogger {
|
||||
return ignorableLogger{
|
||||
Logger: logger,
|
||||
statements: l.statements,
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/bep/logg/handlers/multi"
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
)
|
||||
|
||||
var (
|
||||
reservedFieldNamePrefix = "__h_field_"
|
||||
// FieldNameCmd is the name of the field that holds the command name.
|
||||
FieldNameCmd = reservedFieldNamePrefix + "_cmd"
|
||||
// Used to suppress statements.
|
||||
FieldNameStatementID = reservedFieldNamePrefix + "__h_field_statement_id"
|
||||
)
|
||||
|
||||
// Options defines options for the logger.
|
||||
type Options struct {
|
||||
Level logg.Level
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
Distinct bool
|
||||
StoreErrors bool
|
||||
HandlerPost func(e *logg.Entry) error
|
||||
SuppressStatements map[string]bool
|
||||
}
|
||||
|
||||
// New creates a new logger with the given options.
|
||||
func New(opts Options) Logger {
|
||||
if opts.Stdout == nil {
|
||||
opts.Stdout = os.Stdout
|
||||
}
|
||||
if opts.Stderr == nil {
|
||||
opts.Stderr = os.Stdout
|
||||
}
|
||||
if opts.Level == 0 {
|
||||
opts.Level = logg.LevelWarn
|
||||
}
|
||||
|
||||
var logHandler logg.Handler
|
||||
if terminal.PrintANSIColors(os.Stdout) {
|
||||
logHandler = newDefaultHandler(opts.Stdout, opts.Stderr)
|
||||
} else {
|
||||
logHandler = newNoColoursHandler(opts.Stdout, opts.Stderr, false, nil)
|
||||
}
|
||||
|
||||
errorsw := &strings.Builder{}
|
||||
logCounters := newLogLevelCounter()
|
||||
handlers := []logg.Handler{
|
||||
whiteSpaceTrimmer(),
|
||||
logHandler,
|
||||
logCounters,
|
||||
}
|
||||
|
||||
if opts.HandlerPost != nil {
|
||||
var hookHandler logg.HandlerFunc = func(e *logg.Entry) error {
|
||||
opts.HandlerPost(e)
|
||||
return nil
|
||||
}
|
||||
handlers = append(handlers, hookHandler)
|
||||
}
|
||||
|
||||
if opts.StoreErrors {
|
||||
h := newNoColoursHandler(io.Discard, errorsw, true, func(e *logg.Entry) bool {
|
||||
return e.Level >= logg.LevelError
|
||||
})
|
||||
|
||||
handlers = append(handlers, h)
|
||||
}
|
||||
|
||||
logHandler = multi.New(handlers...)
|
||||
|
||||
var logOnce *logOnceHandler
|
||||
if opts.Distinct {
|
||||
logOnce = newLogOnceHandler(logg.LevelWarn)
|
||||
logHandler = newStopHandler(logOnce, logHandler)
|
||||
}
|
||||
|
||||
if opts.SuppressStatements != nil && len(opts.SuppressStatements) > 0 {
|
||||
logHandler = newStopHandler(newSuppressStatementsHandler(opts.SuppressStatements), logHandler)
|
||||
}
|
||||
|
||||
logger := logg.New(
|
||||
logg.Options{
|
||||
Level: opts.Level,
|
||||
Handler: logHandler,
|
||||
},
|
||||
)
|
||||
|
||||
l := logger.WithLevel(opts.Level)
|
||||
|
||||
reset := func() {
|
||||
logCounters.mu.Lock()
|
||||
defer logCounters.mu.Unlock()
|
||||
logCounters.counters = make(map[logg.Level]int)
|
||||
errorsw.Reset()
|
||||
if logOnce != nil {
|
||||
logOnce.reset()
|
||||
}
|
||||
}
|
||||
|
||||
return &logAdapter{
|
||||
logCounters: logCounters,
|
||||
errors: errorsw,
|
||||
reset: reset,
|
||||
out: opts.Stdout,
|
||||
level: opts.Level,
|
||||
logger: logger,
|
||||
debugl: l.WithLevel(logg.LevelDebug),
|
||||
infol: l.WithLevel(logg.LevelInfo),
|
||||
warnl: l.WithLevel(logg.LevelWarn),
|
||||
errorl: l.WithLevel(logg.LevelError),
|
||||
}
|
||||
}
|
||||
|
||||
// NewDefault creates a new logger with the default options.
|
||||
func NewDefault() Logger {
|
||||
opts := Options{
|
||||
Distinct: true,
|
||||
Level: logg.LevelWarn,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
}
|
||||
return New(opts)
|
||||
}
|
||||
|
||||
func LevelLoggerToWriter(l logg.LevelLogger) io.Writer {
|
||||
return logWriter{l: l}
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debugf(format string, v ...any)
|
||||
Debugln(v ...any)
|
||||
Error() logg.LevelLogger
|
||||
Errorf(format string, v ...any)
|
||||
Errorln(v ...any)
|
||||
Errors() string
|
||||
Errorsf(id, format string, v ...any)
|
||||
Info() logg.LevelLogger
|
||||
InfoCommand(command string) logg.LevelLogger
|
||||
Infof(format string, v ...any)
|
||||
Infoln(v ...any)
|
||||
Level() logg.Level
|
||||
LoggCount(logg.Level) int
|
||||
Logger() logg.Logger
|
||||
Out() io.Writer
|
||||
Printf(format string, v ...any)
|
||||
Println(v ...any)
|
||||
PrintTimerIfDelayed(start time.Time, name string)
|
||||
Reset()
|
||||
Warn() logg.LevelLogger
|
||||
WarnCommand(command string) logg.LevelLogger
|
||||
Warnf(format string, v ...any)
|
||||
Warnln(v ...any)
|
||||
Deprecatef(fail bool, format string, v ...any)
|
||||
}
|
||||
|
||||
type logAdapter struct {
|
||||
logCounters *logLevelCounter
|
||||
errors *strings.Builder
|
||||
reset func()
|
||||
out io.Writer
|
||||
level logg.Level
|
||||
logger logg.Logger
|
||||
debugl logg.LevelLogger
|
||||
infol logg.LevelLogger
|
||||
warnl logg.LevelLogger
|
||||
errorl logg.LevelLogger
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debugf(format string, v ...any) {
|
||||
l.debugl.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debugln(v ...any) {
|
||||
l.debugl.Logf(l.sprint(v...))
|
||||
}
|
||||
|
||||
func (l *logAdapter) Info() logg.LevelLogger {
|
||||
return l.infol
|
||||
}
|
||||
|
||||
func (l *logAdapter) InfoCommand(command string) logg.LevelLogger {
|
||||
return l.infol.WithField(FieldNameCmd, command)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Infof(format string, v ...any) {
|
||||
l.infol.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Infoln(v ...any) {
|
||||
l.infol.Logf(l.sprint(v...))
|
||||
}
|
||||
|
||||
func (l *logAdapter) Level() logg.Level {
|
||||
return l.level
|
||||
}
|
||||
|
||||
func (l *logAdapter) LoggCount(level logg.Level) int {
|
||||
l.logCounters.mu.RLock()
|
||||
defer l.logCounters.mu.RUnlock()
|
||||
return l.logCounters.counters[level]
|
||||
}
|
||||
|
||||
func (l *logAdapter) Logger() logg.Logger {
|
||||
return l.logger
|
||||
}
|
||||
|
||||
func (l *logAdapter) Out() io.Writer {
|
||||
return l.out
|
||||
}
|
||||
|
||||
// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
|
||||
// if considerable time is spent.
|
||||
func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
milli := int(1000 * elapsed.Seconds())
|
||||
if milli < 500 {
|
||||
return
|
||||
}
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Printf(format string, v ...any) {
|
||||
// Add trailing newline if not present.
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format += "\n"
|
||||
}
|
||||
fmt.Fprintf(l.out, format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Println(v ...any) {
|
||||
fmt.Fprintln(l.out, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Reset() {
|
||||
l.reset()
|
||||
}
|
||||
|
||||
func (l *logAdapter) Warn() logg.LevelLogger {
|
||||
return l.warnl
|
||||
}
|
||||
|
||||
func (l *logAdapter) Warnf(format string, v ...any) {
|
||||
l.warnl.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) WarnCommand(command string) logg.LevelLogger {
|
||||
return l.warnl.WithField(FieldNameCmd, command)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Warnln(v ...any) {
|
||||
l.warnl.Logf(l.sprint(v...))
|
||||
}
|
||||
|
||||
func (l *logAdapter) Error() logg.LevelLogger {
|
||||
return l.errorl
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errorf(format string, v ...any) {
|
||||
l.errorl.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errorln(v ...any) {
|
||||
l.errorl.Logf(l.sprint(v...))
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errors() string {
|
||||
return l.errors.String()
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errorsf(id, format string, v ...any) {
|
||||
l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) sprint(v ...any) string {
|
||||
return strings.TrimRight(fmt.Sprintln(v...), "\n")
|
||||
}
|
||||
|
||||
func (l *logAdapter) Deprecatef(fail bool, format string, v ...any) {
|
||||
format = "DEPRECATED: " + format
|
||||
if fail {
|
||||
l.errorl.Logf(format, v...)
|
||||
} else {
|
||||
l.warnl.Logf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
l logg.LevelLogger
|
||||
}
|
||||
|
||||
func (w logWriter) Write(p []byte) (n int, err error) {
|
||||
w.l.Log(logg.String(string(p)))
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package loggers_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
)
|
||||
|
||||
func TestLogDistinct(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
Distinct: true,
|
||||
StoreErrors: true,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Warnln("warn 1")
|
||||
}
|
||||
c.Assert(strings.Count(l.Errors(), "error 1"), qt.Equals, 1)
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2)
|
||||
c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1)
|
||||
}
|
||||
|
||||
func TestHookLast(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
HandlerPost: func(e *logg.Entry) error {
|
||||
panic(e.Message)
|
||||
},
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
c.Assert(func() { l.Warnln("warn 1") }, qt.PanicMatches, "warn 1")
|
||||
}
|
||||
|
||||
func TestOptionStoreErrors(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
Stderr: &sb,
|
||||
Stdout: &sb,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
|
||||
errorsStr := l.Errors()
|
||||
|
||||
c.Assert(errorsStr, qt.Contains, "error 1")
|
||||
c.Assert(errorsStr, qt.Not(qt.Contains), "ERROR")
|
||||
|
||||
c.Assert(sb.String(), qt.Contains, "error 1")
|
||||
c.Assert(sb.String(), qt.Contains, "ERROR")
|
||||
|
||||
}
|
||||
|
||||
func TestLogCount(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Warnln("warn 1")
|
||||
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2)
|
||||
c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1)
|
||||
c.Assert(l.LoggCount(logg.LevelInfo), qt.Equals, 0)
|
||||
}
|
||||
|
||||
func TestSuppressStatements(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
SuppressStatements: map[string]bool{
|
||||
"error-1": true,
|
||||
},
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
l.Error().WithField(loggers.FieldNameStatementID, "error-1").Logf("error 1")
|
||||
l.Errorln("error 2")
|
||||
|
||||
errorsStr := l.Errors()
|
||||
|
||||
c.Assert(errorsStr, qt.Not(qt.Contains), "error 1")
|
||||
c.Assert(errorsStr, qt.Contains, "error 2")
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 1)
|
||||
|
||||
}
|
||||
|
||||
func TestReset(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
Distinct: true,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Errorln("error 1")
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2)
|
||||
|
||||
l.Reset()
|
||||
|
||||
errorsStr := l.Errors()
|
||||
|
||||
c.Assert(errorsStr, qt.Equals, "")
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 0)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
)
|
||||
|
||||
func InitGlobalLogger(panicOnWarnings bool) {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
var logHookLast func(e *logg.Entry) error
|
||||
if panicOnWarnings {
|
||||
logHookLast = PanicOnWarningHook
|
||||
}
|
||||
|
||||
log = New(
|
||||
Options{
|
||||
Distinct: true,
|
||||
HandlerPost: logHookLast,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var logMu sync.Mutex
|
||||
|
||||
func Log() Logger {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
return log
|
||||
}
|
||||
|
||||
// The global logger.
|
||||
var log Logger
|
||||
|
||||
func init() {
|
||||
InitGlobalLogger(false)
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
// 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 loggers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var (
|
||||
// Counts ERROR logs to the global jww logger.
|
||||
GlobalErrorCounter *jww.Counter
|
||||
PanicOnWarning bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
GlobalErrorCounter = &jww.Counter{}
|
||||
jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError))
|
||||
}
|
||||
|
||||
func LoggerToWriterWithPrefix(logger *log.Logger, prefix string) io.Writer {
|
||||
return prefixWriter{
|
||||
logger: logger,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
type prefixWriter struct {
|
||||
logger *log.Logger
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (w prefixWriter) Write(p []byte) (n int, err error) {
|
||||
w.logger.Printf("%s: %s", w.prefix, p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...any)
|
||||
Println(v ...any)
|
||||
PrintTimerIfDelayed(start time.Time, name string)
|
||||
Debug() *log.Logger
|
||||
Debugf(format string, v ...any)
|
||||
Debugln(v ...any)
|
||||
Info() *log.Logger
|
||||
Infof(format string, v ...any)
|
||||
Infoln(v ...any)
|
||||
Warn() *log.Logger
|
||||
Warnf(format string, v ...any)
|
||||
Warnln(v ...any)
|
||||
Error() *log.Logger
|
||||
Errorf(format string, v ...any)
|
||||
Errorln(v ...any)
|
||||
Errors() string
|
||||
|
||||
Out() io.Writer
|
||||
|
||||
Reset()
|
||||
|
||||
// Used in tests.
|
||||
LogCounters() *LogCounters
|
||||
}
|
||||
|
||||
type LogCounters struct {
|
||||
ErrorCounter *jww.Counter
|
||||
WarnCounter *jww.Counter
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
*jww.Notepad
|
||||
|
||||
// The writer that represents stdout.
|
||||
// Will be ioutil.Discard when in quiet mode.
|
||||
out io.Writer
|
||||
|
||||
logCounters *LogCounters
|
||||
|
||||
// This is only set in server mode.
|
||||
errors *bytes.Buffer
|
||||
}
|
||||
|
||||
func (l *logger) Printf(format string, v ...any) {
|
||||
l.FEEDBACK.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Println(v ...any) {
|
||||
l.FEEDBACK.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Debug() *log.Logger {
|
||||
return l.DEBUG
|
||||
}
|
||||
|
||||
func (l *logger) Debugf(format string, v ...any) {
|
||||
l.DEBUG.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Debugln(v ...any) {
|
||||
l.DEBUG.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Infof(format string, v ...any) {
|
||||
l.INFO.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Infoln(v ...any) {
|
||||
l.INFO.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Info() *log.Logger {
|
||||
return l.INFO
|
||||
}
|
||||
|
||||
const panicOnWarningMessage = "Warning trapped. Remove the --panicOnWarning flag to continue."
|
||||
|
||||
func (l *logger) Warnf(format string, v ...any) {
|
||||
l.WARN.Printf(format, v...)
|
||||
if PanicOnWarning {
|
||||
panic(panicOnWarningMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) Warnln(v ...any) {
|
||||
l.WARN.Println(v...)
|
||||
if PanicOnWarning {
|
||||
panic(panicOnWarningMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) Warn() *log.Logger {
|
||||
return l.WARN
|
||||
}
|
||||
|
||||
func (l *logger) Errorf(format string, v ...any) {
|
||||
l.ERROR.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Errorln(v ...any) {
|
||||
l.ERROR.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Error() *log.Logger {
|
||||
return l.ERROR
|
||||
}
|
||||
|
||||
func (l *logger) LogCounters() *LogCounters {
|
||||
return l.logCounters
|
||||
}
|
||||
|
||||
func (l *logger) Out() io.Writer {
|
||||
return l.out
|
||||
}
|
||||
|
||||
// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
|
||||
// if considerable time is spent.
|
||||
func (l *logger) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
milli := int(1000 * elapsed.Seconds())
|
||||
if milli < 500 {
|
||||
return
|
||||
}
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *logger) PrintTimer(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
milli := int(1000 * elapsed.Seconds())
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *logger) Errors() string {
|
||||
if l.errors == nil {
|
||||
return ""
|
||||
}
|
||||
return ansiColorRe.ReplaceAllString(l.errors.String(), "")
|
||||
}
|
||||
|
||||
// Reset resets the logger's internal state.
|
||||
func (l *logger) Reset() {
|
||||
l.logCounters.ErrorCounter.Reset()
|
||||
if l.errors != nil {
|
||||
l.errors.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// NewLogger creates a new Logger for the given thresholds
|
||||
func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) Logger {
|
||||
return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors)
|
||||
}
|
||||
|
||||
// NewDebugLogger is a convenience function to create a debug logger.
|
||||
func NewDebugLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelDebug)
|
||||
}
|
||||
|
||||
// NewWarningLogger is a convenience function to create a warning logger.
|
||||
func NewWarningLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelWarn)
|
||||
}
|
||||
|
||||
// NewInfoLogger is a convenience function to create a info logger.
|
||||
func NewInfoLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelInfo)
|
||||
}
|
||||
|
||||
// NewErrorLogger is a convenience function to create an error logger.
|
||||
func NewErrorLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelError)
|
||||
}
|
||||
|
||||
// NewBasicLogger creates a new basic logger writing to Stdout.
|
||||
func NewBasicLogger(t jww.Threshold) Logger {
|
||||
return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false)
|
||||
}
|
||||
|
||||
// NewBasicLoggerForWriter creates a new basic logger writing to w.
|
||||
func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) Logger {
|
||||
return newLogger(t, jww.LevelError, w, ioutil.Discard, false)
|
||||
}
|
||||
|
||||
// RemoveANSIColours removes all ANSI colours from the given string.
|
||||
func RemoveANSIColours(s string) string {
|
||||
return ansiColorRe.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
var (
|
||||
ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m")
|
||||
errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)")
|
||||
)
|
||||
|
||||
type ansiCleaner struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (a ansiCleaner) Write(p []byte) (n int, err error) {
|
||||
return a.w.Write(ansiColorRe.ReplaceAll(p, []byte("")))
|
||||
}
|
||||
|
||||
type labelColorizer struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (a labelColorizer) Write(p []byte) (n int, err error) {
|
||||
replaced := errorRe.ReplaceAllStringFunc(string(p), func(m string) string {
|
||||
switch m {
|
||||
case "ERROR", "FATAL":
|
||||
return terminal.Error(m)
|
||||
case "WARN":
|
||||
return terminal.Warning(m)
|
||||
default:
|
||||
return m
|
||||
}
|
||||
})
|
||||
// io.MultiWriter will abort if we return a bigger write count than input
|
||||
// bytes, so we lie a little.
|
||||
_, err = a.w.Write([]byte(replaced))
|
||||
return len(p), err
|
||||
}
|
||||
|
||||
// InitGlobalLogger initializes the global logger, used in some rare cases.
|
||||
func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer) {
|
||||
outHandle, logHandle = getLogWriters(outHandle, logHandle)
|
||||
|
||||
jww.SetStdoutOutput(outHandle)
|
||||
jww.SetLogOutput(logHandle)
|
||||
jww.SetLogThreshold(logThreshold)
|
||||
jww.SetStdoutThreshold(stdoutThreshold)
|
||||
}
|
||||
|
||||
func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) {
|
||||
isTerm := terminal.PrintANSIColors(os.Stdout)
|
||||
if logHandle != ioutil.Discard && isTerm {
|
||||
// Remove any Ansi coloring from log output
|
||||
logHandle = ansiCleaner{w: logHandle}
|
||||
}
|
||||
|
||||
if isTerm {
|
||||
outHandle = labelColorizer{w: outHandle}
|
||||
}
|
||||
|
||||
return outHandle, logHandle
|
||||
}
|
||||
|
||||
type fatalLogWriter int
|
||||
|
||||
func (s fatalLogWriter) Write(p []byte) (n int, err error) {
|
||||
trace := make([]byte, 1500)
|
||||
runtime.Stack(trace, true)
|
||||
fmt.Printf("\n===========\n\n%s\n", trace)
|
||||
os.Exit(-1)
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var fatalLogListener = func(t jww.Threshold) io.Writer {
|
||||
if t != jww.LevelError {
|
||||
// Only interested in ERROR
|
||||
return nil
|
||||
}
|
||||
|
||||
return new(fatalLogWriter)
|
||||
}
|
||||
|
||||
func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *logger {
|
||||
errorCounter := &jww.Counter{}
|
||||
warnCounter := &jww.Counter{}
|
||||
outHandle, logHandle = getLogWriters(outHandle, logHandle)
|
||||
|
||||
listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError), jww.LogCounter(warnCounter, jww.LevelWarn)}
|
||||
var errorBuff *bytes.Buffer
|
||||
if saveErrors {
|
||||
errorBuff = new(bytes.Buffer)
|
||||
errorCapture := func(t jww.Threshold) io.Writer {
|
||||
if t != jww.LevelError {
|
||||
// Only interested in ERROR
|
||||
return nil
|
||||
}
|
||||
return errorBuff
|
||||
}
|
||||
|
||||
listeners = append(listeners, errorCapture)
|
||||
}
|
||||
|
||||
return &logger{
|
||||
Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
|
||||
out: outHandle,
|
||||
logCounters: &LogCounters{
|
||||
ErrorCounter: errorCounter,
|
||||
WarnCounter: warnCounter,
|
||||
},
|
||||
errors: errorBuff,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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 loggers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
l := NewWarningLogger()
|
||||
|
||||
l.Errorln("One error")
|
||||
l.Errorln("Two error")
|
||||
l.Warnln("A warning")
|
||||
|
||||
c.Assert(l.LogCounters().ErrorCounter.Count(), qt.Equals, uint64(2))
|
||||
}
|
||||
|
||||
func TestLoggerToWriterWithPrefix(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
logger := log.New(&b, "", 0)
|
||||
|
||||
w := LoggerToWriterWithPrefix(logger, "myprefix")
|
||||
|
||||
fmt.Fprint(w, "Hello Hugo!")
|
||||
|
||||
c.Assert(b.String(), qt.Equals, "myprefix: Hello Hugo!\n")
|
||||
}
|
||||
|
||||
func TestRemoveANSIColours(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(RemoveANSIColours(""), qt.Equals, "")
|
||||
c.Assert(RemoveANSIColours("\033[31m"), qt.Equals, "")
|
||||
c.Assert(RemoveANSIColours("\033[31mHello"), qt.Equals, "Hello")
|
||||
c.Assert(RemoveANSIColours("\033[31mHello\033[0m"), qt.Equals, "Hello")
|
||||
c.Assert(RemoveANSIColours("\033[31mHello\033[0m World"), qt.Equals, "Hello World")
|
||||
c.Assert(RemoveANSIColours("\033[31mHello\033[0m World\033[31m!"), qt.Equals, "Hello World!")
|
||||
c.Assert(RemoveANSIColours("\x1b[90m 5 |"), qt.Equals, " 5 |")
|
||||
}
|
||||
+8
-52
@@ -43,25 +43,25 @@ func ToStringMapE(in any) (map[string]any, error) {
|
||||
// ToParamsAndPrepare converts in to Params and prepares it for use.
|
||||
// If in is nil, an empty map is returned.
|
||||
// See PrepareParams.
|
||||
func ToParamsAndPrepare(in any) (Params, error) {
|
||||
func ToParamsAndPrepare(in any) (Params, bool) {
|
||||
if types.IsNil(in) {
|
||||
return Params{}, nil
|
||||
return Params{}, true
|
||||
}
|
||||
m, err := ToStringMapE(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false
|
||||
}
|
||||
PrepareParams(m)
|
||||
return m, nil
|
||||
return m, true
|
||||
}
|
||||
|
||||
// MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails.
|
||||
func MustToParamsAndPrepare(in any) Params {
|
||||
p, err := ToParamsAndPrepare(in)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("cannot convert %T to maps.Params: %s", in, err))
|
||||
if p, ok := ToParamsAndPrepare(in); ok {
|
||||
return p
|
||||
} else {
|
||||
panic(fmt.Sprintf("cannot convert %T to maps.Params", in))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ToStringMap converts in to map[string]interface{}.
|
||||
@@ -96,8 +96,6 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
|
||||
switch v := in.(type) {
|
||||
case []map[string]any:
|
||||
return v, nil
|
||||
case Params:
|
||||
return []map[string]any{v}, nil
|
||||
case []any:
|
||||
var s []map[string]any
|
||||
for _, entry := range v {
|
||||
@@ -125,23 +123,6 @@ func LookupEqualFold[T any | string](m map[string]T, key string) (T, bool) {
|
||||
return s, false
|
||||
}
|
||||
|
||||
// MergeShallow merges src into dst, but only if the key does not already exist in dst.
|
||||
// The keys are compared case insensitively.
|
||||
func MergeShallow(dst, src map[string]any) {
|
||||
for k, v := range src {
|
||||
found := false
|
||||
for dk := range dst {
|
||||
if strings.EqualFold(dk, k) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
dst[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type keyRename struct {
|
||||
pattern glob.Glob
|
||||
newKey string
|
||||
@@ -210,28 +191,3 @@ func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]any) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertFloat64WithNoDecimalsToInt converts float64 values with no decimals to int recursively.
|
||||
func ConvertFloat64WithNoDecimalsToInt(m map[string]any) {
|
||||
for k, v := range m {
|
||||
switch vv := v.(type) {
|
||||
case float64:
|
||||
if v == float64(int64(vv)) {
|
||||
m[k] = int64(vv)
|
||||
}
|
||||
case map[string]any:
|
||||
ConvertFloat64WithNoDecimalsToInt(vv)
|
||||
case []any:
|
||||
for i, vvv := range vv {
|
||||
switch vvvv := vvv.(type) {
|
||||
case float64:
|
||||
if vvv == float64(int64(vvvv)) {
|
||||
vv[i] = int64(vvvv)
|
||||
}
|
||||
case map[string]any:
|
||||
ConvertFloat64WithNoDecimalsToInt(vvvv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,11 +116,11 @@ func TestToSliceStringMap(t *testing.T) {
|
||||
|
||||
func TestToParamsAndPrepare(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
_, err := ToParamsAndPrepare(map[string]any{"A": "av"})
|
||||
c.Assert(err, qt.IsNil)
|
||||
_, ok := ToParamsAndPrepare(map[string]any{"A": "av"})
|
||||
c.Assert(ok, qt.IsTrue)
|
||||
|
||||
params, err := ToParamsAndPrepare(nil)
|
||||
c.Assert(err, qt.IsNil)
|
||||
params, ok := ToParamsAndPrepare(nil)
|
||||
c.Assert(ok, qt.IsTrue)
|
||||
c.Assert(params, qt.DeepEquals, Params{})
|
||||
}
|
||||
|
||||
|
||||
+29
-87
@@ -23,37 +23,30 @@ import (
|
||||
// Params is a map where all keys are lower case.
|
||||
type Params map[string]any
|
||||
|
||||
// KeyParams is an utility struct for the WalkParams method.
|
||||
type KeyParams struct {
|
||||
Key string
|
||||
Params Params
|
||||
}
|
||||
|
||||
// GetNested does a lower case and nested search in this map.
|
||||
// Get does a lower case and nested search in this map.
|
||||
// It will return nil if none found.
|
||||
// Make all of these methods internal somehow.
|
||||
func (p Params) GetNested(indices ...string) any {
|
||||
func (p Params) Get(indices ...string) any {
|
||||
v, _, _ := getNested(p, indices)
|
||||
return v
|
||||
}
|
||||
|
||||
// SetParams overwrites values in dst with values in src for common or new keys.
|
||||
// Set overwrites values in p with values in pp for common or new keys.
|
||||
// This is done recursively.
|
||||
func SetParams(dst, src Params) {
|
||||
for k, v := range src {
|
||||
vv, found := dst[k]
|
||||
func (p Params) Set(pp Params) {
|
||||
for k, v := range pp {
|
||||
vv, found := p[k]
|
||||
if !found {
|
||||
dst[k] = v
|
||||
p[k] = v
|
||||
} else {
|
||||
switch vvv := vv.(type) {
|
||||
case Params:
|
||||
if pv, ok := v.(Params); ok {
|
||||
SetParams(vvv, pv)
|
||||
vvv.Set(pv)
|
||||
} else {
|
||||
dst[k] = v
|
||||
p[k] = v
|
||||
}
|
||||
default:
|
||||
dst[k] = v
|
||||
p[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,25 +62,26 @@ func (p Params) IsZero() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
for k := range p {
|
||||
return k == MergeStrategyKey
|
||||
for k, _ := range p {
|
||||
return k == mergeStrategyKey
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge strategy given.
|
||||
// Merge transfers values from pp to p for new keys.
|
||||
// This is done recursively.
|
||||
func MergeParamsWithStrategy(strategy string, dst, src Params) {
|
||||
dst.merge(ParamsMergeStrategy(strategy), src)
|
||||
func (p Params) Merge(pp Params) {
|
||||
p.merge("", pp)
|
||||
}
|
||||
|
||||
// MergeParams transfers values from src to dst for new keys using the merge encoded in dst.
|
||||
// MergeRoot transfers values from pp to p for new keys where p is the
|
||||
// root of the tree.
|
||||
// This is done recursively.
|
||||
func MergeParams(dst, src Params) {
|
||||
ms, _ := dst.GetMergeStrategy()
|
||||
dst.merge(ms, src)
|
||||
func (p Params) MergeRoot(pp Params) {
|
||||
ms, _ := p.GetMergeStrategy()
|
||||
p.merge(ms, pp)
|
||||
}
|
||||
|
||||
func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
|
||||
@@ -103,7 +97,7 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
|
||||
|
||||
for k, v := range pp {
|
||||
|
||||
if k == MergeStrategyKey {
|
||||
if k == mergeStrategyKey {
|
||||
continue
|
||||
}
|
||||
vv, found := p[k]
|
||||
@@ -122,9 +116,8 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
|
||||
}
|
||||
}
|
||||
|
||||
// For internal use.
|
||||
func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
|
||||
if v, found := p[MergeStrategyKey]; found {
|
||||
if v, found := p[mergeStrategyKey]; found {
|
||||
if s, ok := v.(ParamsMergeStrategy); ok {
|
||||
return s, true
|
||||
}
|
||||
@@ -132,23 +125,21 @@ func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
|
||||
return ParamsMergeStrategyShallow, false
|
||||
}
|
||||
|
||||
// For internal use.
|
||||
func (p Params) DeleteMergeStrategy() bool {
|
||||
if _, found := p[MergeStrategyKey]; found {
|
||||
delete(p, MergeStrategyKey)
|
||||
if _, found := p[mergeStrategyKey]; found {
|
||||
delete(p, mergeStrategyKey)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// For internal use.
|
||||
func (p Params) SetMergeStrategy(s ParamsMergeStrategy) {
|
||||
func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) {
|
||||
switch s {
|
||||
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid merge strategy %q", s))
|
||||
}
|
||||
p[MergeStrategyKey] = s
|
||||
p[mergeStrategyKey] = s
|
||||
}
|
||||
|
||||
func getNested(m map[string]any, indices []string) (any, string, map[string]any) {
|
||||
@@ -196,7 +187,7 @@ func GetNestedParam(keyStr, separator string, candidates ...Params) (any, error)
|
||||
|
||||
keySegments := strings.Split(keyStr, separator)
|
||||
for _, m := range candidates {
|
||||
if v := m.GetNested(keySegments...); v != nil {
|
||||
if v := m.Get(keySegments...); v != nil {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
@@ -242,58 +233,9 @@ const (
|
||||
// Add new keys, merge existing.
|
||||
ParamsMergeStrategyDeep ParamsMergeStrategy = "deep"
|
||||
|
||||
MergeStrategyKey = "_merge"
|
||||
mergeStrategyKey = "_merge"
|
||||
)
|
||||
|
||||
// CleanConfigStringMapString removes any processing instructions from m,
|
||||
// m will never be modified.
|
||||
func CleanConfigStringMapString(m map[string]string) map[string]string {
|
||||
if m == nil || len(m) == 0 {
|
||||
return m
|
||||
}
|
||||
if _, found := m[MergeStrategyKey]; !found {
|
||||
return m
|
||||
}
|
||||
// Create a new map and copy all the keys except the merge strategy key.
|
||||
m2 := make(map[string]string, len(m)-1)
|
||||
for k, v := range m {
|
||||
if k != MergeStrategyKey {
|
||||
m2[k] = v
|
||||
}
|
||||
}
|
||||
return m2
|
||||
}
|
||||
|
||||
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
|
||||
// map[string]any.
|
||||
func CleanConfigStringMap(m map[string]any) map[string]any {
|
||||
if m == nil || len(m) == 0 {
|
||||
return m
|
||||
}
|
||||
if _, found := m[MergeStrategyKey]; !found {
|
||||
return m
|
||||
}
|
||||
// Create a new map and copy all the keys except the merge strategy key.
|
||||
m2 := make(map[string]any, len(m)-1)
|
||||
for k, v := range m {
|
||||
if k != MergeStrategyKey {
|
||||
m2[k] = v
|
||||
}
|
||||
switch v2 := v.(type) {
|
||||
case map[string]any:
|
||||
m2[k] = CleanConfigStringMap(v2)
|
||||
case Params:
|
||||
var p Params = CleanConfigStringMap(v2)
|
||||
m2[k] = p
|
||||
case map[string]string:
|
||||
m2[k] = CleanConfigStringMapString(v2)
|
||||
}
|
||||
|
||||
}
|
||||
return m2
|
||||
|
||||
}
|
||||
|
||||
func toMergeStrategy(v any) ParamsMergeStrategy {
|
||||
s := ParamsMergeStrategy(cast.ToString(v))
|
||||
switch s {
|
||||
@@ -313,7 +255,7 @@ func PrepareParams(m Params) {
|
||||
for k, v := range m {
|
||||
var retyped bool
|
||||
lKey := strings.ToLower(k)
|
||||
if lKey == MergeStrategyKey {
|
||||
if lKey == mergeStrategyKey {
|
||||
v = toMergeStrategy(v)
|
||||
retyped = true
|
||||
} else {
|
||||
|
||||
+10
-10
@@ -75,13 +75,13 @@ func TestParamsSetAndMerge(t *testing.T) {
|
||||
|
||||
createParamsPair := func() (Params, Params) {
|
||||
p1 := Params{"a": "av", "c": "cv", "nested": Params{"al2": "al2v", "cl2": "cl2v"}}
|
||||
p2 := Params{"b": "bv", "a": "abv", "nested": Params{"bl2": "bl2v", "al2": "al2bv"}, MergeStrategyKey: ParamsMergeStrategyDeep}
|
||||
p2 := Params{"b": "bv", "a": "abv", "nested": Params{"bl2": "bl2v", "al2": "al2bv"}, mergeStrategyKey: ParamsMergeStrategyDeep}
|
||||
return p1, p2
|
||||
}
|
||||
|
||||
p1, p2 := createParamsPair()
|
||||
|
||||
SetParams(p1, p2)
|
||||
p1.Set(p2)
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"a": "abv",
|
||||
@@ -92,12 +92,12 @@ func TestParamsSetAndMerge(t *testing.T) {
|
||||
"bl2": "bl2v",
|
||||
},
|
||||
"b": "bv",
|
||||
MergeStrategyKey: ParamsMergeStrategyDeep,
|
||||
mergeStrategyKey: ParamsMergeStrategyDeep,
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
|
||||
MergeParamsWithStrategy("", p1, p2)
|
||||
p1.Merge(p2)
|
||||
|
||||
// Default is to do a shallow merge.
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
@@ -111,8 +111,8 @@ func TestParamsSetAndMerge(t *testing.T) {
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
p1.SetMergeStrategy(ParamsMergeStrategyNone)
|
||||
MergeParamsWithStrategy("", p1, p2)
|
||||
p1.SetDefaultMergeStrategy(ParamsMergeStrategyNone)
|
||||
p1.Merge(p2)
|
||||
p1.DeleteMergeStrategy()
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
@@ -125,8 +125,8 @@ func TestParamsSetAndMerge(t *testing.T) {
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
p1.SetMergeStrategy(ParamsMergeStrategyShallow)
|
||||
MergeParamsWithStrategy("", p1, p2)
|
||||
p1.SetDefaultMergeStrategy(ParamsMergeStrategyShallow)
|
||||
p1.Merge(p2)
|
||||
p1.DeleteMergeStrategy()
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
@@ -140,8 +140,8 @@ func TestParamsSetAndMerge(t *testing.T) {
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
p1.SetMergeStrategy(ParamsMergeStrategyDeep)
|
||||
MergeParamsWithStrategy("", p1, p2)
|
||||
p1.SetDefaultMergeStrategy(ParamsMergeStrategyDeep)
|
||||
p1.Merge(p2)
|
||||
p1.DeleteMergeStrategy()
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
|
||||
@@ -30,7 +30,6 @@ type Scratch struct {
|
||||
|
||||
// Scratcher provides a scratching service.
|
||||
type Scratcher interface {
|
||||
// Scratch returns a "scratch pad" that can be used to store state.
|
||||
Scratch() *Scratch
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ type Workers struct {
|
||||
|
||||
// Runner wraps the lifecycle methods of a new task set.
|
||||
//
|
||||
// Run will block until a worker is available or the context is cancelled,
|
||||
// Run wil block until a worker is available or the context is cancelled,
|
||||
// and then run the given func in a new goroutine.
|
||||
// Wait will wait for all the running goroutines to finish.
|
||||
type Runner interface {
|
||||
|
||||
+8
-19
@@ -100,25 +100,25 @@ var isFileRe = regexp.MustCompile(`.*\..{1,6}$`)
|
||||
// GetDottedRelativePath expects a relative path starting after the content directory.
|
||||
// It returns a relative path with dots ("..") navigating up the path structure.
|
||||
func GetDottedRelativePath(inPath string) string {
|
||||
inPath = path.Clean(filepath.ToSlash(inPath))
|
||||
inPath = filepath.Clean(filepath.FromSlash(inPath))
|
||||
|
||||
if inPath == "." {
|
||||
return "./"
|
||||
}
|
||||
|
||||
if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, "/") {
|
||||
inPath += "/"
|
||||
if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) {
|
||||
inPath += FilePathSeparator
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(inPath, "/") {
|
||||
inPath = "/" + inPath
|
||||
if !strings.HasPrefix(inPath, FilePathSeparator) {
|
||||
inPath = FilePathSeparator + inPath
|
||||
}
|
||||
|
||||
dir, _ := filepath.Split(inPath)
|
||||
|
||||
sectionCount := strings.Count(dir, "/")
|
||||
sectionCount := strings.Count(dir, FilePathSeparator)
|
||||
|
||||
if sectionCount == 0 || dir == "/" {
|
||||
if sectionCount == 0 || dir == FilePathSeparator {
|
||||
return "./"
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ func extractFilename(in, ext, base, pathSeparator string) (name string) {
|
||||
// return the filename minus the extension (and the ".")
|
||||
name = base[:strings.LastIndex(base, ".")]
|
||||
} else {
|
||||
// no extension case so just return base, which will
|
||||
// no extension case so just return base, which willi
|
||||
// be the filename
|
||||
name = base
|
||||
}
|
||||
@@ -263,14 +263,3 @@ func (n NamedSlice) String() string {
|
||||
}
|
||||
return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ","))
|
||||
}
|
||||
|
||||
// DirFile holds the result from path.Split.
|
||||
type DirFile struct {
|
||||
Dir string
|
||||
File string
|
||||
}
|
||||
|
||||
// Used in test.
|
||||
func (df DirFile) String() string {
|
||||
return fmt.Sprintf("%s|%s", df.Dir, df.File)
|
||||
}
|
||||
|
||||
+9
-12
@@ -51,10 +51,9 @@ var pb pathBridge
|
||||
|
||||
// MakePermalink combines base URL with content path to create full URL paths.
|
||||
// Example
|
||||
//
|
||||
// base: http://spf13.com/
|
||||
// path: post/how-i-blog
|
||||
// result: http://spf13.com/post/how-i-blog
|
||||
// base: http://spf13.com/
|
||||
// path: post/how-i-blog
|
||||
// result: http://spf13.com/post/how-i-blog
|
||||
func MakePermalink(host, plink string) *url.URL {
|
||||
base, err := url.Parse(host)
|
||||
if err != nil {
|
||||
@@ -118,19 +117,17 @@ func PrettifyURL(in string) string {
|
||||
|
||||
// PrettifyURLPath takes a URL path to a content and converts it
|
||||
// to enable pretty URLs.
|
||||
//
|
||||
// /section/name.html becomes /section/name/index.html
|
||||
// /section/name/ becomes /section/name/index.html
|
||||
// /section/name/index.html becomes /section/name/index.html
|
||||
// /section/name.html becomes /section/name/index.html
|
||||
// /section/name/ becomes /section/name/index.html
|
||||
// /section/name/index.html becomes /section/name/index.html
|
||||
func PrettifyURLPath(in string) string {
|
||||
return prettifyPath(in, pb)
|
||||
}
|
||||
|
||||
// Uglify does the opposite of PrettifyURLPath().
|
||||
//
|
||||
// /section/name/index.html becomes /section/name.html
|
||||
// /section/name/ becomes /section/name.html
|
||||
// /section/name.html becomes /section/name.html
|
||||
// /section/name/index.html becomes /section/name.html
|
||||
// /section/name/ becomes /section/name.html
|
||||
// /section/name.html becomes /section/name.html
|
||||
func Uglify(in string) string {
|
||||
if path.Ext(in) == "" {
|
||||
if len(in) < 2 {
|
||||
|
||||
@@ -24,8 +24,6 @@ import (
|
||||
// Positioner represents a thing that knows its position in a text file or stream,
|
||||
// typically an error.
|
||||
type Positioner interface {
|
||||
// Position returns the current position.
|
||||
// Useful in error logging, e.g. {{ errorf "error in code block: %s" .Position }}.
|
||||
Position() Position
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ package types
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
@@ -91,6 +90,3 @@ func IsNil(v any) bool {
|
||||
type DevMarker interface {
|
||||
DevOnly()
|
||||
}
|
||||
|
||||
// This is only used for debugging purposes.
|
||||
var InvocationCounter atomic.Int64
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user